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..c0469d10a 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.6' 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 deleted file mode 100644 index b71eb20c9..000000000 --- a/distribution/debian/install.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/bin/bash -### Description: Lidarr .NET Debian install -### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0 -### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN -### Version v1.0.0 2023-12-29 - StevieTV - adapted from servarr script for Lidarr installs -### Version V1.0.1 2024-01-02 - StevieTV - remove UTF8-BOM -### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty -### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory - -### Boilerplate Warning -#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -scriptversion="1.0.3" -scriptdate="2024-01-06" - -set -euo pipefail - -echo "Running Lidarr Install Script - Version [$scriptversion] as of [$scriptdate]" - -# Am I root?, need root! - -if [ "$EUID" -ne 0 ]; then - echo "Please run as root." - exit -fi - -app="lidarr" -app_port="8686" -app_prereq="curl sqlite3 wget" -app_umask="0002" -branch="main" - -# Constants -### Update these variables as required for your specific instance -installdir="/opt" # {Update me if needed} Install Location -bindir="${installdir}/${app^}" # Full Path to Install Location -datadir="/var/lib/$app/" # {Update me if needed} AppData directory to use -app_bin=${app^} # Binary Name of the app - -# This script should not be ran from installdir, otherwise later in the script the extracted files will be removed before they can be moved to installdir. -if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ]; then - echo "You should not run this script from the intended install directory. The script will exit. Please re-run it from another directory" - exit -fi - -# Prompt User -read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty -app_uid=$(echo "$app_uid" | tr -d ' ') -app_uid=${app_uid:-$app} -# Prompt Group -read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty -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" -read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty - -# Create User / Group as needed -if [ "$app_guid" != "$app_uid" ]; then - if ! getent group "$app_guid" >/dev/null; then - groupadd "$app_guid" - fi -fi -if ! getent passwd "$app_uid" >/dev/null; then - adduser --system --no-create-home --ingroup "$app_guid" "$app_uid" - echo "Created and added User [$app_uid] to Group [$app_guid]" -fi -if ! getent group "$app_guid" | grep -qw "$app_uid"; then - echo "User [$app_uid] did not exist in Group [$app_guid]" - usermod -a -G "$app_guid" "$app_uid" - echo "Added User [$app_uid] to Group [$app_guid]" -fi - -# Stop the App if running -if service --status-all | grep -Fq "$app"; then - systemctl stop "$app" - systemctl disable "$app".service - echo "Stopped existing $app" -fi - -# Create Appdata Directory - -# AppData -mkdir -p "$datadir" -chown -R "$app_uid":"$app_guid" "$datadir" -chmod 775 "$datadir" -echo "Directories created" -# Download and install the App - -# prerequisite packages -echo "" -echo "Installing pre-requisite Packages" -# shellcheck disable=SC2086 -apt update && apt install -y $app_prereq -echo "" -ARCH=$(dpkg --print-architecture) -# get arch -dlbase="https://lidarr.servarr.com/v1/update/$branch/updatefile?os=linux&runtime=netcore" -case "$ARCH" in -"amd64") DLURL="${dlbase}&arch=x64" ;; -"armhf") DLURL="${dlbase}&arch=arm" ;; -"arm64") DLURL="${dlbase}&arch=arm64" ;; -*) - echo "Arch not supported" - exit 1 - ;; -esac -echo "" -echo "Removing previous tarballs" -# -f to Force so we fail if it doesn't exist -rm -f "${app^}".*.tar.gz -echo "" -echo "Downloading..." -wget --content-disposition "$DLURL" -tar -xvzf "${app^}".*.tar.gz -echo "" -echo "Installation files downloaded and extracted" - -# remove existing installs -echo "Removing existing installation" -rm -rf "$bindir" -echo "Installing..." -mv "${app^}" $installdir -chown "$app_uid":"$app_guid" -R "$bindir" -chmod 775 "$bindir" -rm -rf "${app^}.*.tar.gz" -# Ensure we check for an update in case user installs older version or different branch -touch "$datadir"/update_required -chown "$app_uid":"$app_guid" "$datadir"/update_required -echo "App Installed" -# Configure Autostart - -# Remove any previous app .service -echo "Removing old service file" -rm -rf /etc/systemd/system/"$app".service - -# Create app .service with correct user startup -echo "Creating service file" -cat </dev/null -[Unit] -Description=${app^} Daemon -After=syslog.target network.target -[Service] -User=$app_uid -Group=$app_guid -UMask=$app_umask -Type=simple -ExecStart=$bindir/$app_bin -nobrowser -data=$datadir -TimeoutStopSec=20 -KillMode=process -Restart=on-failure -[Install] -WantedBy=multi-user.target -EOF - -# Start the App -echo "Service file created. Attempting to start the app" -systemctl -q daemon-reload -systemctl enable --now -q "$app" - -# Finish Update/Installation -host=$(hostname -I) -ip_local=$(grep -oP '^\S*' <<<"$host") -echo "" -echo "Install complete" -sleep 10 -STATUS="$(systemctl is-active "$app")" -if [ "${STATUS}" = "active" ]; then - echo "Browse to http://$ip_local:$app_port for the ${app^} GUI" -else - echo "${app^} failed to start" -fi - -# Exit -exit 0 diff --git a/distribution/debian/lidarr.service b/distribution/debian/lidarr.service deleted file mode 100644 index 8ec5b5b1d..000000000 --- a/distribution/debian/lidarr.service +++ /dev/null @@ -1,20 +0,0 @@ -# This file is owned by the lidarr package, DO NOT MODIFY MANUALLY -# Instead use 'dpkg-reconfigure -plow lidarr' to modify User/Group/UMask/-data -# Or use systemd built-in override functionality using 'systemctl edit lidarr' -[Unit] -Description=Lidarr Daemon -After=network.target - -[Service] -User=lidarr -Group=lidarr -UMask=002 - -Type=simple -ExecStart=/opt/Lidarr/Lidarr -nobrowser -data=/var/lib/lidarr -TimeoutStopSec=20 -KillMode=process -Restart=on-failure - -[Install] -WantedBy=multi-user.target 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++; @@ -172,7 +175,7 @@ class AlbumDetailsMedium extends Component { :
- {translate('NoTracksInThisMedium')} + No tracks in this medium
}
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..61266a8d4 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; @@ -35,7 +36,6 @@ interface Artist extends ModelBase { nextAlbum?: Album; qualityProfileId: number; metadataProfileId: number; - monitorNewItems: string; ratings: Ratings; rootFolderPath: string; sortName: string; 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 +158,7 @@ class AlbumRow extends Component { return ( { - totalTrackCount + statistics.totalTrackCount } ); @@ -196,17 +196,6 @@ class AlbumRow extends Component { ); } - if (name === 'size') { - return ( - - {!!sizeOnDisk && formatBytes(sizeOnDisk)} - - ); - } - if (name === 'status') { return ( { - 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,12 +192,11 @@ class ArtistDetailsSeason extends Component { const { albumCount, - albumFileCount, totalAlbumCount, trackFileCount, monitoredAlbumCount, hasMonitoredAlbums, - sizeOnDisk = 0 + sizeOnDisk } = getAlbumStatistics(items); const { @@ -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
diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.tsx b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.tsx index 5d9b4a069..931d7053c 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.tsx +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.tsx @@ -1,12 +1,11 @@ -import { IconDefinition } from '@fortawesome/free-regular-svg-icons'; import React from 'react'; import Icon from 'Components/Icon'; import styles from './ArtistIndexOverviewInfoRow.css'; interface ArtistIndexOverviewInfoRowProps { title?: string; - iconName?: IconDefinition; - label: string | null; + iconName: object; + label: string; } function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) { diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.tsx b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.tsx index 11285c1b3..3f1197ef4 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.tsx +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.tsx @@ -1,5 +1,5 @@ import { throttle } from 'lodash'; -import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import Artist from 'Artist/Artist'; @@ -33,11 +33,11 @@ interface RowItemData { interface ArtistIndexOverviewsProps { items: Artist[]; - sortKey: string; + sortKey?: string; sortDirection?: string; jumpToCharacter?: string; scrollTop?: number; - scrollerRef: RefObject; + scrollerRef: React.MutableRefObject; isSelectMode: boolean; isSmallScreen: boolean; } @@ -79,7 +79,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) { const { size: posterSize, detailedProgressBar } = useSelector( selectOverviewOptions ); - const listRef = useRef(null); + const listRef: React.MutableRefObject = useRef(); const [measureRef, bounds] = useMeasure(); const [size, setSize] = useState({ width: 0, height: 0 }); @@ -136,8 +136,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) { }, [isSmallScreen, scrollerRef, bounds]); useEffect(() => { - const currentScrollerRef = scrollerRef.current as HTMLElement; - const currentScrollListener = isSmallScreen ? window : currentScrollerRef; + const currentScrollListener = isSmallScreen ? window : scrollerRef.current; + const currentScrollerRef = scrollerRef.current; const handleScroll = throttle(() => { const { offsetTop = 0 } = currentScrollerRef; @@ -146,7 +146,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - listRef.current?.scrollTo(scrollTop); + listRef.current.scrollTo(scrollTop); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -175,8 +175,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) { scrollTop += offset; } - listRef.current?.scrollTo(scrollTop); - scrollerRef.current?.scrollTo(0, scrollTop); + listRef.current.scrollTo(scrollTop); + scrollerRef.current.scrollTo(0, scrollTop); } } }, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]); diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx index 4ab9391e3..e19692e41 100644 --- a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx +++ b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx @@ -60,7 +60,7 @@ function ArtistIndexOverviewOptionsModalContent( const dispatch = useDispatch(); const onOverviewOptionChange = useCallback( - ({ name, value }: { name: string; value: unknown }) => { + ({ name, value }) => { dispatch(setArtistOverviewOption({ [name]: value })); }, [dispatch] diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx index 67c37c00d..156368d9b 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx @@ -206,7 +206,7 @@ function ArtistIndexPoster(props: ArtistIndexPosterProps) {
) : null} - {showQualityProfile && !!qualityProfile?.name ? ( + {showQualityProfile ? (
{qualityProfile.name}
diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.tsx b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.tsx index c478ac1ae..9e5c3e885 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.tsx +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.tsx @@ -1,9 +1,8 @@ import { throttle } from 'lodash'; -import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window'; import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import Artist from 'Artist/Artist'; import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster'; import useMeasure from 'Helpers/Hooks/useMeasure'; @@ -22,7 +21,7 @@ const columnPaddingSmallScreen = parseInt( const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); -const ADDITIONAL_COLUMN_COUNT: Record = { +const ADDITIONAL_COLUMN_COUNT = { small: 3, medium: 2, large: 1, @@ -42,17 +41,17 @@ interface CellItemData { interface ArtistIndexPostersProps { items: Artist[]; - sortKey: string; + sortKey?: string; sortDirection?: SortDirection; jumpToCharacter?: string; scrollTop?: number; - scrollerRef: RefObject; + scrollerRef: React.MutableRefObject; isSelectMode: boolean; isSmallScreen: boolean; } const artistIndexSelector = createSelector( - (state: AppState) => state.artistIndex.posterOptions, + (state) => state.artistIndex.posterOptions, (posterOptions) => { return { posterOptions, @@ -109,7 +108,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) { } = props; const { posterOptions } = useSelector(artistIndexSelector); - const ref = useRef(null); + const ref: React.MutableRefObject = useRef(); const [measureRef, bounds] = useMeasure(); const [size, setSize] = useState({ width: 0, height: 0 }); @@ -232,8 +231,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) { }, [isSmallScreen, size, scrollerRef, bounds]); useEffect(() => { - const currentScrollerRef = scrollerRef.current as HTMLElement; - const currentScrollListener = isSmallScreen ? window : currentScrollerRef; + const currentScrollListener = isSmallScreen ? window : scrollerRef.current; + const currentScrollerRef = scrollerRef.current; const handleScroll = throttle(() => { const { offsetTop = 0 } = currentScrollerRef; @@ -242,7 +241,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - ref.current?.scrollTo({ scrollLeft: 0, scrollTop }); + ref.current.scrollTo({ scrollLeft: 0, scrollTop }); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -265,8 +264,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) { const scrollTop = rowIndex * rowHeight + padding; - ref.current?.scrollTo({ scrollLeft: 0, scrollTop }); - scrollerRef.current?.scrollTo(0, scrollTop); + ref.current.scrollTo({ scrollLeft: 0, scrollTop }); + scrollerRef.current.scrollTo(0, scrollTop); } } }, [ diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx index 2560d855a..e1e60801c 100644 --- a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx @@ -59,7 +59,7 @@ function ArtistIndexPosterOptionsModalContent( const dispatch = useDispatch(); const onPosterOptionChange = useCallback( - ({ name, value }: { name: string; value: unknown }) => { + ({ name, value }) => { dispatch(setArtistPosterOption({ [name]: value })); }, [dispatch] diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx index 255bd9ba4..bd6a01e10 100644 --- a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx +++ b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx @@ -1,7 +1,6 @@ import _ from 'lodash'; import React, { useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Statistics } from 'Album/Album'; import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import { kinds } from 'Helpers/Props'; @@ -57,8 +56,8 @@ function AlbumDetails(props: AlbumDetailsProps) { disambiguation, albumType, monitored, - statistics = {} as Statistics, - isSaving = false, + statistics, + isSaving, } = album; return ( diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx index 3e7e0578f..ec4ef9074 100644 --- a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx +++ b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx @@ -11,7 +11,7 @@ interface AlbumStudioAlbumProps { artistId: number; albumId: number; title: string; - disambiguation?: string; + disambiguation: string; albumType: string; monitored: boolean; statistics: Statistics; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx index b3c2abbbe..c21c9a8af 100644 --- a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx +++ b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx @@ -33,7 +33,7 @@ function ChangeMonitoringModalContent( const [monitor, setMonitor] = useState(NO_CHANGE); const onInputChange = useCallback( - ({ value }: { value: string }) => { + ({ value }) => { setMonitor(value); }, [setMonitor] diff --git a/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx b/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx index 86b41e8ba..cc072f41a 100644 --- a/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx +++ b/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx @@ -1,4 +1,4 @@ -import React, { SyntheticEvent, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { useSelect } from 'App/SelectContext'; import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; @@ -15,9 +15,8 @@ function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) { const isSelected = selectState.selectedState[artistId]; const onSelectPress = useCallback( - (event: SyntheticEvent) => { - const nativeEvent = event.nativeEvent as PointerEvent; - const shiftKey = nativeEvent.shiftKey; + (event) => { + const shiftKey = event.nativeEvent.shiftKey; selectDispatch({ type: 'toggleSelected', diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectAllButton.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectAllButton.tsx index 2b3e9c01c..7229dbdc0 100644 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectAllButton.tsx +++ b/frontend/src/Artist/Index/Select/ArtistIndexSelectAllButton.tsx @@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props'; interface ArtistIndexSelectAllButtonProps { label: string; isSelectMode: boolean; - overflowComponent: React.FunctionComponent; + overflowComponent: React.FunctionComponent; } function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) { diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx index f0569d607..ffa017a78 100644 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx +++ b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx @@ -24,14 +24,6 @@ import OrganizeArtistModal from './Organize/OrganizeArtistModal'; import TagsModal from './Tags/TagsModal'; import styles from './ArtistIndexSelectFooter.css'; -interface SavePayload { - monitored?: boolean; - qualityProfileId?: number; - metadataProfileId?: number; - rootFolderPath?: string; - moveFiles?: boolean; -} - const artistEditorSelector = createSelector( (state: AppState) => state.artist, (artist) => { @@ -87,7 +79,7 @@ function ArtistIndexSelectFooter() { }, [setIsEditModalOpen]); const onSavePress = useCallback( - (payload: SavePayload) => { + (payload) => { setIsSavingArtist(true); setIsEditModalOpen(false); @@ -126,7 +118,7 @@ function ArtistIndexSelectFooter() { }, [setIsTagsModalOpen]); const onApplyTagsPress = useCallback( - (tags: number[], applyTags: string) => { + (tags, applyTags) => { setIsSavingTags(true); setIsTagsModalOpen(false); diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx index 8679bba99..45fe78536 100644 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx +++ b/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx @@ -7,7 +7,7 @@ interface ArtistIndexSelectModeButtonProps { label: string; iconName: IconDefinition; isSelectMode: boolean; - overflowComponent: React.FunctionComponent; + overflowComponent: React.FunctionComponent; onPress: () => void; } diff --git a/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx b/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx index b67ee60aa..5e7d1f1ff 100644 --- a/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx +++ b/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx @@ -28,15 +28,9 @@ function RetagArtistModalContent(props: RetagArtistModalContentProps) { const dispatch = useDispatch(); const artistNames = useMemo(() => { - const artists = artistIds.reduce((acc: Artist[], id) => { - const a = allArtists.find((a) => a.id === id); - - if (a) { - acc.push(a); - } - - return acc; - }, []); + const artists = artistIds.map((id) => { + return allArtists.find((a) => a.id === id); + }); const sorted = orderBy(artists, ['sortName']); diff --git a/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx b/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx index 4accc9f0e..c367a4550 100644 --- a/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx +++ b/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx @@ -15,7 +15,6 @@ import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds } from 'Helpers/Props'; import { bulkDeleteArtist, setDeleteOption } from 'Store/Actions/artistActions'; import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; -import { CheckInputChanged } from 'typings/inputs'; import translate from 'Utilities/String/translate'; import styles from './DeleteArtistModalContent.css'; @@ -38,16 +37,16 @@ function DeleteArtistModalContent(props: DeleteArtistModalContentProps) { const [deleteFiles, setDeleteFiles] = useState(false); - const artists = useMemo((): Artist[] => { - const artistList = artistIds.map((id) => { + const artists = useMemo(() => { + const artists = artistIds.map((id) => { return allArtists.find((a) => a.id === id); - }) as Artist[]; + }); - return orderBy(artistList, ['sortName']); + return orderBy(artists, ['sortName']); }, [artistIds, allArtists]); const onDeleteFilesChange = useCallback( - ({ value }: CheckInputChanged) => { + ({ value }) => { setDeleteFiles(value); }, [setDeleteFiles] diff --git a/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx b/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx index 993be8ce5..94d1e87d2 100644 --- a/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx +++ b/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx @@ -35,7 +35,7 @@ const monitoredOptions = [ get value() { return translate('NoChange'); }, - isDisabled: true, + disabled: true, }, { key: 'monitored', @@ -66,7 +66,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) { const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false); const save = useCallback( - (moveFiles: boolean) => { + (moveFiles) => { let hasChanges = false; const payload: SavePayload = {}; @@ -114,7 +114,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) { ); const onInputChange = useCallback( - ({ name, value }: { name: string; value: string }) => { + ({ name, value }) => { switch (name) { case 'monitored': setMonitored(value); diff --git a/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx b/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx index 95a7eaae2..c41c0c896 100644 --- a/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx +++ b/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx @@ -1,7 +1,6 @@ import { uniq } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; -import { Tag } from 'App/State/TagsAppState'; import Artist from 'Artist/Artist'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; @@ -29,7 +28,7 @@ function TagsModalContent(props: TagsModalContentProps) { const { artistIds, onModalClose, onApplyTagsPress } = props; const allArtists: Artist[] = useSelector(createAllArtistSelector()); - const tagList: Tag[] = useSelector(createTagsSelector()); + const tagList = useSelector(createTagsSelector()); const [tags, setTags] = useState([]); const [applyTags, setApplyTags] = useState('add'); @@ -49,14 +48,14 @@ function TagsModalContent(props: TagsModalContentProps) { }, [artistIds, allArtists]); const onTagsChange = useCallback( - ({ value }: { value: number[] }) => { + ({ value }) => { setTags(value); }, [setTags] ); const onApplyTagsChange = useCallback( - ({ value }: { value: string }) => { + ({ value }) => { setApplyTags(value); }, [setApplyTags] diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.css b/frontend/src/Artist/Index/Table/ArtistIndexRow.css index 35d03c263..b75ad6afd 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.css +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.css @@ -67,7 +67,6 @@ flex: 1 0 125px; } -.monitorNewItems, .nextAlbum, .lastAlbum, .added, diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.css.d.ts b/frontend/src/Artist/Index/Table/ArtistIndexRow.css.d.ts index 4855aec75..fd8d84e17 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.css.d.ts +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.css.d.ts @@ -14,7 +14,6 @@ interface CssExports { 'lastAlbum': string; 'link': string; 'metadataProfileId': string; - 'monitorNewItems': string; 'nextAlbum': string; 'overlayTitle': string; 'path': string; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx b/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx index 0398f5502..e87545093 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx @@ -23,9 +23,7 @@ import Column from 'Components/Table/Column'; import TagListConnector from 'Components/TagListConnector'; import { icons } from 'Helpers/Props'; import { executeCommand } from 'Store/Actions/commandActions'; -import { SelectStateInputProps } from 'typings/props'; import formatBytes from 'Utilities/Number/formatBytes'; -import firstCharToUpper from 'Utilities/String/firstCharToUpper'; import translate from 'Utilities/String/translate'; import AlbumsCell from './AlbumsCell'; import hasGrowableColumns from './hasGrowableColumns'; @@ -58,7 +56,6 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { monitored, status, path, - monitorNewItems, nextAlbum, lastAlbum, added, @@ -129,7 +126,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { }, [setIsDeleteArtistModalOpen]); const onSelectedChange = useCallback( - ({ id, value, shiftKey }: SelectStateInputProps) => { + ({ id, value, shiftKey }) => { selectDispatch({ type: 'toggleSelected', id, @@ -220,7 +217,15 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { if (name === 'qualityProfileId') { return ( - {qualityProfile?.name ?? ''} + {qualityProfile.name} + + ); + } + + if (name === 'qualityProfileId') { + return ( + + {qualityProfile.name} ); } @@ -228,15 +233,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { if (name === 'metadataProfileId') { return ( - {metadataProfile?.name ?? ''} - - ); - } - - if (name === 'monitorNewItems') { - return ( - - {translate(firstCharToUpper(monitorNewItems))} + {metadataProfile.name} ); } @@ -255,7 +252,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { } return ( - {translate('None')} + None ); } @@ -274,15 +271,13 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { } return ( - {translate('None')} + None ); } if (name === 'added') { return ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ts(2739) - {path} + {path} ); } diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx b/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx index c3c8044ce..08f5a9680 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx @@ -1,9 +1,8 @@ import { throttle } from 'lodash'; -import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import Artist from 'Artist/Artist'; import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow'; import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader'; @@ -31,17 +30,17 @@ interface RowItemData { interface ArtistIndexTableProps { items: Artist[]; - sortKey: string; + sortKey?: string; sortDirection?: SortDirection; jumpToCharacter?: string; scrollTop?: number; - scrollerRef: RefObject; + scrollerRef: React.MutableRefObject; isSelectMode: boolean; isSmallScreen: boolean; } const columnsSelector = createSelector( - (state: AppState) => state.artistIndex.columns, + (state) => state.artistIndex.columns, (columns) => columns ); @@ -94,7 +93,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) { const columns = useSelector(columnsSelector); const { showBanners } = useSelector(selectTableOptions); - const listRef = useRef>(null); + const listRef: React.MutableRefObject = useRef(); const [measureRef, bounds] = useMeasure(); const [size, setSize] = useState({ width: 0, height: 0 }); const windowWidth = window.innerWidth; @@ -105,7 +104,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) { }, [showBanners]); useEffect(() => { - const current = scrollerRef?.current as HTMLElement; + const current = scrollerRef.current as HTMLElement; if (isSmallScreen) { setSize({ @@ -129,8 +128,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) { }, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]); useEffect(() => { - const currentScrollerRef = scrollerRef.current as HTMLElement; - const currentScrollListener = isSmallScreen ? window : currentScrollerRef; + const currentScrollListener = isSmallScreen ? window : scrollerRef.current; + const currentScrollerRef = scrollerRef.current; const handleScroll = throttle(() => { const { offsetTop = 0 } = currentScrollerRef; @@ -139,7 +138,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - listRef.current?.scrollTo(scrollTop); + listRef.current.scrollTo(scrollTop); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -168,8 +167,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) { scrollTop += offset; } - listRef.current?.scrollTo(scrollTop); - scrollerRef?.current?.scrollTo(0, scrollTop); + listRef.current.scrollTo(scrollTop); + scrollerRef.current.scrollTo(0, scrollTop); } } }, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]); diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css b/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css index 7ea4e94aa..6da0be920 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css +++ b/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css @@ -31,7 +31,6 @@ flex: 1 0 125px; } -.monitorNewItems, .nextAlbum, .lastAlbum, .added, diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css.d.ts b/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css.d.ts index 467b401bb..4d9dcd20b 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css.d.ts +++ b/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css.d.ts @@ -11,7 +11,6 @@ interface CssExports { 'lastAlbum': string; 'latestAlbum': string; 'metadataProfileId': string; - 'monitorNewItems': string; 'nextAlbum': string; 'path': string; 'qualityProfileId': string; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.tsx b/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.tsx index 1b325c225..2e903574d 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.tsx +++ b/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.tsx @@ -15,7 +15,6 @@ import { setArtistSort, setArtistTableOption, } from 'Store/Actions/artistIndexActions'; -import { CheckInputChanged } from 'typings/inputs'; import hasGrowableColumns from './hasGrowableColumns'; import styles from './ArtistIndexTableHeader.css'; @@ -33,21 +32,21 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) { const [selectState, selectDispatch] = useSelect(); const onSortPress = useCallback( - (value: string) => { + (value) => { dispatch(setArtistSort({ sortKey: value })); }, [dispatch] ); const onTableOptionChange = useCallback( - (payload: unknown) => { + (payload) => { dispatch(setArtistTableOption(payload)); }, [dispatch] ); const onSelectAllChange = useCallback( - ({ value }: CheckInputChanged) => { + ({ value }) => { selectDispatch({ type: value ? 'selectAll' : 'unselectAll', }); @@ -95,8 +94,6 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) { { + ({ name, value }) => { onTableOptionChange({ tableOptions: { ...tableOptions, diff --git a/frontend/src/Artist/Index/createArtistIndexItemSelector.ts b/frontend/src/Artist/Index/createArtistIndexItemSelector.ts index 4388a3aeb..86ee8a560 100644 --- a/frontend/src/Artist/Index/createArtistIndexItemSelector.ts +++ b/frontend/src/Artist/Index/createArtistIndexItemSelector.ts @@ -1,6 +1,5 @@ import { createSelector } from 'reselect'; import Artist from 'Artist/Artist'; -import Command from 'Commands/Command'; import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames'; import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector'; import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector'; @@ -13,21 +12,25 @@ function createArtistIndexItemSelector(artistId: number) { createArtistQualityProfileSelector(artistId), createArtistMetadataProfileSelector(artistId), createExecutingCommandsSelector(), - ( - artist: Artist, - qualityProfile, - metadataProfile, - executingCommands: Command[] - ) => { + (artist: Artist, qualityProfile, metadataProfile, executingCommands) => { + // If an artist is deleted this selector may fire before the parent + // selectors, which will result in an undefined artist, if that happens + // we want to return early here and again in the render function to avoid + // trying to show an artist that has no information available. + + if (!artist) { + return {}; + } + const isRefreshingArtist = executingCommands.some((command) => { return ( - command.name === REFRESH_ARTIST && command.body.artistId === artistId + command.name === REFRESH_ARTIST && command.body.artistId === artist.id ); }); const isSearchingArtist = executingCommands.some((command) => { return ( - command.name === ARTIST_SEARCH && command.body.artistId === artistId + command.name === ARTIST_SEARCH && command.body.artistId === artist.id ); }); diff --git a/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js b/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js index 9ce7e8f9a..d7feee98d 100644 --- a/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js +++ b/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js @@ -14,7 +14,7 @@ function ArtistInteractiveSearchModal(props) { return ( diff --git a/frontend/src/Calendar/CalendarFilterModal.tsx b/frontend/src/Calendar/CalendarFilterModal.tsx deleted file mode 100644 index e26b2928b..000000000 --- a/frontend/src/Calendar/CalendarFilterModal.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import FilterModal from 'Components/Filter/FilterModal'; -import { setCalendarFilter } from 'Store/Actions/calendarActions'; - -function createCalendarSelector() { - return createSelector( - (state: AppState) => state.calendar.items, - (calendar) => { - return calendar; - } - ); -} - -function createFilterBuilderPropsSelector() { - return createSelector( - (state: AppState) => state.calendar.filterBuilderProps, - (filterBuilderProps) => { - return filterBuilderProps; - } - ); -} - -interface CalendarFilterModalProps { - isOpen: boolean; -} - -export default function CalendarFilterModal(props: CalendarFilterModalProps) { - const sectionItems = useSelector(createCalendarSelector()); - const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); - const customFilterType = 'calendar'; - - const dispatch = useDispatch(); - - const dispatchSetFilter = useCallback( - (payload: unknown) => { - dispatch(setCalendarFilter(payload)); - }, - [dispatch] - ); - - return ( - - ); -} diff --git a/frontend/src/Calendar/CalendarPage.js b/frontend/src/Calendar/CalendarPage.js index bf7f46c10..3a4603e82 100644 --- a/frontend/src/Calendar/CalendarPage.js +++ b/frontend/src/Calendar/CalendarPage.js @@ -14,7 +14,6 @@ import { align, icons } from 'Helpers/Props'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; import CalendarConnector from './CalendarConnector'; -import CalendarFilterModal from './CalendarFilterModal'; import CalendarLinkModal from './iCal/CalendarLinkModal'; import LegendConnector from './Legend/LegendConnector'; import CalendarOptionsModal from './Options/CalendarOptionsModal'; @@ -79,7 +78,6 @@ class CalendarPage extends Component { const { selectedFilterKey, filters, - customFilters, hasArtist, artistError, artistIsFetching, @@ -139,8 +137,7 @@ class CalendarPage extends Component { isDisabled={!hasArtist} selectedFilterKey={selectedFilterKey} filters={filters} - customFilters={customFilters} - filterModalConnectorComponent={CalendarFilterModal} + customFilters={[]} onFilterSelect={onFilterSelect} /> @@ -207,7 +204,6 @@ class CalendarPage extends Component { CalendarPage.propTypes = { selectedFilterKey: PropTypes.string.isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, hasArtist: PropTypes.bool.isRequired, artistError: PropTypes.object, artistIsFetching: PropTypes.bool.isRequired, diff --git a/frontend/src/Calendar/CalendarPageConnector.js b/frontend/src/Calendar/CalendarPageConnector.js index 4221c0339..d0e7e87af 100644 --- a/frontend/src/Calendar/CalendarPageConnector.js +++ b/frontend/src/Calendar/CalendarPageConnector.js @@ -6,7 +6,6 @@ import withCurrentPage from 'Components/withCurrentPage'; import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions'; import { executeCommand } from 'Store/Actions/commandActions'; import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector'; -import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; @@ -60,7 +59,6 @@ function createMapStateToProps() { return createSelector( (state) => state.calendar.selectedFilterKey, (state) => state.calendar.filters, - createCustomFiltersSelector('calendar'), createArtistCountSelector(), createUISettingsSelector(), createMissingAlbumIdsSelector(), @@ -69,7 +67,6 @@ function createMapStateToProps() { ( selectedFilterKey, filters, - customFilters, artistCount, uiSettings, missingAlbumIds, @@ -79,7 +76,6 @@ function createMapStateToProps() { return { selectedFilterKey, filters, - customFilters, colorImpairedMode: uiSettings.enableColorImpairedMode, hasArtist: !!artistCount.count, artistError: artistCount.error, diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js index 3473f4c31..844ffec5f 100644 --- a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js +++ b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js @@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component { type={inputTypes.TAG} name="tags" value={tags} - helpText={translate('ICalTagsArtistHelpText')} + helpText={translate('TagsHelpText')} onChange={this.onInputChange} /> diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts deleted file mode 100644 index 09a03865d..000000000 --- a/frontend/src/Commands/Command.ts +++ /dev/null @@ -1,38 +0,0 @@ -import ModelBase from 'App/ModelBase'; - -export interface CommandBody { - sendUpdatesToClient: boolean; - updateScheduledTask: boolean; - completionMessage: string; - requiresDiskAccess: boolean; - isExclusive: boolean; - isLongRunning: boolean; - name: string; - lastExecutionTime: string; - lastStartTime: string; - trigger: string; - suppressMessages: boolean; - artistId?: number; - artistIds?: number[]; -} - -interface Command extends ModelBase { - name: string; - commandName: string; - message: string; - body: CommandBody; - priority: string; - status: string; - result: string; - queued: string; - started: string; - ended: string; - duration: string; - trigger: string; - stateChangeTime: string; - sendUpdatesToClient: boolean; - updateScheduledTask: boolean; - lastExecutionTime: string; -} - -export default Command; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js index dff1fbf6e..0e4d6a015 100644 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import Alert from 'Components/Alert'; import PathInput from 'Components/Form/PathInput'; import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; @@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component { className={styles.mappedDrivesWarning} kind={kinds.WARNING} > - + Mapped network drives are not available when running as a Windows Service, see the FAQ for more information. } diff --git a/frontend/src/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx index 486027f35..003206961 100644 --- a/frontend/src/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import Artist from 'Artist/Artist'; import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import FilterBuilderRowValue from './FilterBuilderRowValue'; import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; @@ -11,7 +11,7 @@ function ArtistFilterBuilderRowValue(props: FilterBuilderRowValueProps) { const tagList = allArtists .map((artist) => ({ id: artist.id, name: artist.artistName })) - .sort(sortByProp('name')); + .sort(sortByName); return ; } diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js index 0c4a31657..d33f4d4fb 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js @@ -1,4 +1,3 @@ -import { maxBy } from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -51,7 +50,7 @@ class FilterBuilderModalContent extends Component { if (id) { dispatchSetFilter({ selectedFilterKey: id }); } else { - const last = maxBy(customFilters, 'id'); + const last = customFilters[customFilters.length -1]; dispatchSetFilter({ selectedFilterKey: last.id }); } @@ -109,7 +108,7 @@ class FilterBuilderModalContent extends Component { this.setState({ labelErrors: [ { - message: translate('LabelIsRequired') + message: 'Label is required' } ] }); @@ -147,7 +146,7 @@ class FilterBuilderModalContent extends Component { return ( - {translate('CustomFilter')} + Custom Filter @@ -167,9 +166,7 @@ class FilterBuilderModalContent extends Component { -
- {translate('Filters')} -
+
{translate('Filters')}
{ diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 77dad7173..f613d20bc 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,10 @@ import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; -import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue'; -import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue'; +import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector'; 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,10 +66,7 @@ function getRowValueConnector(selectedFilterBuilderProp) { return IndexerFilterBuilderRowValueConnector; case filterBuilderValueTypes.METADATA_PROFILE: - return MetadataProfileFilterBuilderRowValue; - - case filterBuilderValueTypes.MONITOR_NEW_ITEMS: - return MonitorNewItemsFilterBuilderRowValue; + return MetadataProfileFilterBuilderRowValueConnector; case filterBuilderValueTypes.PROTOCOL: return ProtocolFilterBuilderRowValue; @@ -80,7 +75,7 @@ function getRowValueConnector(selectedFilterBuilderProp) { return QualityFilterBuilderRowValueConnector; case filterBuilderValueTypes.QUALITY_PROFILE: - return QualityProfileFilterBuilderRowValue; + return QualityProfileFilterBuilderRowValueConnector; case filterBuilderValueTypes.ARTIST: return ArtistFilterBuilderRowValue; @@ -225,7 +220,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/MonitorNewItemsFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/MonitorNewItemsFilterBuilderRowValue.tsx deleted file mode 100644 index 812d8c5b1..000000000 --- a/frontend/src/Components/Filter/Builder/MonitorNewItemsFilterBuilderRowValue.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps'; -import translate from 'Utilities/String/translate'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; - -const options = [ - { - id: 'all', - get name() { - return translate('AllAlbums'); - }, - }, - { - id: 'new', - get name() { - return translate('New'); - }, - }, - { - id: 'none', - get name() { - return translate('None'); - }, - }, -]; - -function MonitorNewItemsFilterBuilderRowValue( - props: FilterBuilderRowValueProps -) { - return ; -} - -export default MonitorNewItemsFilterBuilderRowValue; 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/PlaylistInput.js b/frontend/src/Components/Form/PlaylistInput.js index 0b3966f60..77718f4f1 100644 --- a/frontend/src/Components/Form/PlaylistInput.js +++ b/frontend/src/Components/Form/PlaylistInput.js @@ -9,6 +9,7 @@ import TableBody from 'Components/Table/TableBody'; import TableRow from 'Components/Table/TableRow'; import tagShape from 'Helpers/Props/Shapes/tagShape'; import translate from 'Utilities/String/translate'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; import styles from './PlaylistInput.css'; @@ -45,17 +46,7 @@ class PlaylistInput extends Component { onChange } = this.props; - const oldSelected = _.reduce( - prevState.selectedState, - (result, value, id) => { - if (value) { - result.push(id); - } - - return result; - }, - [] - ).sort(); + const oldSelected = getSelectedIds(prevState.selectedState, { parseIds: false }).sort(); const newSelected = this.getSelectedIds().sort(); if (!_.isEqual(oldSelected, newSelected)) { @@ -70,17 +61,7 @@ class PlaylistInput extends Component { // Control getSelectedIds = () => { - return _.reduce( - this.state.selectedState, - (result, value, id) => { - if (value) { - result.push(id); - } - - return result; - }, - [] - ); + return getSelectedIds(this.state.selectedState, { parseIds: false }); }; // 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/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx index ce9b0e7e4..972a9bade 100644 --- a/frontend/src/Components/Page/PageContentBody.tsx +++ b/frontend/src/Components/Page/PageContentBody.tsx @@ -1,5 +1,5 @@ -import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react'; -import Scroller, { OnScroll } from 'Components/Scroller/Scroller'; +import React, { forwardRef, ReactNode, useCallback } from 'react'; +import Scroller from 'Components/Scroller/Scroller'; import ScrollDirection from 'Helpers/Props/ScrollDirection'; import { isLocked } from 'Utilities/scrollLock'; import styles from './PageContentBody.css'; @@ -9,11 +9,14 @@ interface PageContentBodyProps { innerClassName?: string; children: ReactNode; initialScrollTop?: number; - onScroll?: (payload: OnScroll) => void; + onScroll?: (payload) => void; } const PageContentBody = forwardRef( - (props: PageContentBodyProps, ref: ForwardedRef) => { + ( + props: PageContentBodyProps, + ref: React.MutableRefObject + ) => { const { className = styles.contentBody, innerClassName = styles.innerContentBody, @@ -23,7 +26,7 @@ const PageContentBody = forwardRef( } = props; const onScrollWrapper = useCallback( - (payload: OnScroll) => { + (payload) => { if (onScroll && !isLocked()) { onScroll(payload); } diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index d6db8d612..09a69b459 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -129,7 +129,7 @@ const links = [ to: '/settings/general' }, { - title: () => translate('Ui'), + title: () => translate('UI'), to: '/settings/ui' } ] diff --git a/frontend/src/Components/Scroller/Scroller.tsx b/frontend/src/Components/Scroller/Scroller.tsx index 37b16eebd..2bcb899aa 100644 --- a/frontend/src/Components/Scroller/Scroller.tsx +++ b/frontend/src/Components/Scroller/Scroller.tsx @@ -1,21 +1,9 @@ import classNames from 'classnames'; import { throttle } from 'lodash'; -import React, { - ForwardedRef, - forwardRef, - MutableRefObject, - ReactNode, - useEffect, - useRef, -} from 'react'; +import React, { forwardRef, ReactNode, useEffect, useRef } from 'react'; import ScrollDirection from 'Helpers/Props/ScrollDirection'; import styles from './Scroller.css'; -export interface OnScroll { - scrollLeft: number; - scrollTop: number; -} - interface ScrollerProps { className?: string; scrollDirection?: ScrollDirection; @@ -24,11 +12,11 @@ interface ScrollerProps { scrollTop?: number; initialScrollTop?: number; children?: ReactNode; - onScroll?: (payload: OnScroll) => void; + onScroll?: (payload) => void; } const Scroller = forwardRef( - (props: ScrollerProps, ref: ForwardedRef) => { + (props: ScrollerProps, ref: React.MutableRefObject) => { const { className, autoFocus = false, @@ -42,7 +30,7 @@ const Scroller = forwardRef( } = props; const internalRef = useRef(); - const currentRef = (ref as MutableRefObject) ?? internalRef; + const currentRef = ref ?? internalRef; useEffect( () => { 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/VirtualTable.js b/frontend/src/Components/Table/VirtualTable.js index 5473413cb..4a597e795 100644 --- a/frontend/src/Components/Table/VirtualTable.js +++ b/frontend/src/Components/Table/VirtualTable.js @@ -7,8 +7,6 @@ import { scrollDirections } from 'Helpers/Props'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import styles from './VirtualTable.css'; -const ROW_HEIGHT = 38; - function overscanIndicesGetter(options) { const { cellCount, @@ -50,7 +48,8 @@ class VirtualTable extends Component { const { items, scrollIndex, - scrollTop + scrollTop, + onRecompute } = this.props; const { @@ -58,7 +57,10 @@ class VirtualTable extends Component { scrollRestored } = this.state; - if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) { + if (this._grid && + (prevState.width !== width || + hasDifferentItemsOrOrder(prevProps.items, items))) { + onRecompute(width); // recomputeGridSize also forces Grid to discard its cache of rendered cells this._grid.recomputeGridSize(); } @@ -101,6 +103,7 @@ class VirtualTable extends Component { className, items, scroller, + scrollTop: ignored, header, headerHeight, rowHeight, @@ -146,7 +149,6 @@ class VirtualTable extends Component { {header}
@@ -189,14 +192,16 @@ VirtualTable.propTypes = { scroller: PropTypes.instanceOf(Element).isRequired, header: PropTypes.node.isRequired, headerHeight: PropTypes.number.isRequired, + rowHeight: PropTypes.oneOfType([PropTypes.func, PropTypes.number]).isRequired, rowRenderer: PropTypes.func.isRequired, - rowHeight: PropTypes.number.isRequired + onRecompute: PropTypes.func.isRequired }; VirtualTable.defaultProps = { className: styles.tableContainer, headerHeight: 38, - rowHeight: ROW_HEIGHT + rowHeight: 38, + onRecompute: () => {} }; export default VirtualTable; 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/Components/withScrollPosition.tsx b/frontend/src/Components/withScrollPosition.tsx index f688a6253..ec13c6ab8 100644 --- a/frontend/src/Components/withScrollPosition.tsx +++ b/frontend/src/Components/withScrollPosition.tsx @@ -1,30 +1,24 @@ +import PropTypes from 'prop-types'; import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; import scrollPositions from 'Store/scrollPositions'; -interface WrappedComponentProps { - initialScrollTop: number; -} - -interface ScrollPositionProps { - history: RouteComponentProps['history']; - location: RouteComponentProps['location']; - match: RouteComponentProps['match']; -} - -function withScrollPosition( - WrappedComponent: React.FC, - scrollPositionKey: string -) { - function ScrollPosition(props: ScrollPositionProps) { +function withScrollPosition(WrappedComponent, scrollPositionKey) { + function ScrollPosition(props) { const { history } = props; const initialScrollTop = - history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0; + history.action === 'POP' || + (history.location.state && history.location.state.restoreScrollPosition) + ? scrollPositions[scrollPositionKey] + : 0; return ; } + ScrollPosition.propTypes = { + history: PropTypes.object.isRequired, + }; + return ScrollPosition; } 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/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js index 005ea0b7a..b19d16c8a 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js @@ -5,7 +5,6 @@ export const DEFAULT = 'default'; export const HISTORY_EVENT_TYPE = 'historyEventType'; export const INDEXER = 'indexer'; export const METADATA_PROFILE = 'metadataProfile'; -export const MONITOR_NEW_ITEMS = 'monitorNewItems'; export const PROTOCOL = 'protocol'; export const QUALITY = 'quality'; export const QUALITY_PROFILE = 'qualityProfile'; 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..91c284636 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 }, { @@ -58,7 +55,7 @@ class SelectAlbumModalContent extends Component { // Listeners onFilterChange = ({ value }) => { - this.setState({ filter: value }); + this.setState({ filter: value.toLowerCase() }); }; // @@ -66,21 +63,14 @@ 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,34 +82,32 @@ class SelectAlbumModalContent extends Component { className={styles.modalBody} scrollDirection={scrollDirections.NONE} > + { + isFetching && + + } + + - {isFetching ? : null} - - {error ?
{errorMessage}
: null} - - - - {isPopulated && !!items.length ? ( + { { items.map((item) => { - return item.title.toLowerCase().includes(filterLower) ? + return item.title.toLowerCase().includes(filter) ? (
- ) : null} + }
@@ -148,13 +136,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/Artist/SelectArtistModalContent.js b/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.js index 14c42a80b..2dceb979e 100644 --- a/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.js +++ b/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.js @@ -29,7 +29,7 @@ class SelectArtistModalContent extends Component { // Listeners onFilterChange = ({ value }) => { - this.setState({ filter: value }); + this.setState({ filter: value.toLowerCase() }); }; // @@ -43,7 +43,6 @@ class SelectArtistModalContent extends Component { } = this.props; const filter = this.state.filter; - const filterLower = filter.toLowerCase(); return ( @@ -70,7 +69,7 @@ class SelectArtistModalContent extends Component { > { items.map((item) => { - return item.artistName.toLowerCase().includes(filterLower) ? + return item.artistName.toLowerCase().includes(filter) ? ( - - - ); - } -} - -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/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index dec15893f..35244e64e 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -15,51 +15,16 @@ import NamingOption from './NamingOption'; import styles from './NamingModal.css'; const separatorOptions = [ - { - key: ' ', - get value() { - return `${translate('Space')} ( )`; - } - }, - { - key: '.', - get value() { - return `${translate('Period')} (.)`; - } - }, - { - key: '_', - get value() { - return `${translate('Underscore')} (_)`; - } - }, - { - key: '-', - get value() { - return `${translate('Dash')} (-)`; - } - } + { key: ' ', value: 'Space ( )' }, + { key: '.', value: 'Period (.)' }, + { key: '_', value: 'Underscore (_)' }, + { key: '-', value: 'Dash (-)' } ]; const caseOptions = [ - { - key: 'title', - get value() { - return translate('DefaultCase'); - } - }, - { - key: 'lower', - get value() { - return translate('Lowercase'); - } - }, - { - key: 'upper', - get value() { - return translate('Uppercase'); - } - } + { key: 'title', value: 'Default Case' }, + { key: 'lower', value: 'Lowercase' }, + { key: 'upper', value: 'Uppercase' } ]; const fileNameTokens = [ @@ -75,23 +40,33 @@ const fileNameTokens = [ const artistTokens = [ { token: '{Artist Name}', example: 'Artist Name' }, - { token: '{Artist CleanName}', example: 'Artist Name' }, + { token: '{Artist NameThe}', example: 'Artist Name, The' }, - { token: '{Artist CleanNameThe}', example: 'Artist Name, The' }, + { token: '{Artist NameFirstCharacter}', example: 'A' }, + + { token: '{Artist CleanName}', example: 'Artist Name' }, + { token: '{Artist Disambiguation}', example: 'Disambiguation' }, + { token: '{Artist Genre}', example: 'Pop' }, + { token: '{Artist MbId}', example: 'db92a151-1ac2-438b-bc43-b82e149ddd50' } ]; const albumTokens = [ { token: '{Album Title}', example: 'Album Title' }, - { token: '{Album CleanTitle}', example: 'Album Title' }, + { token: '{Album TitleThe}', example: 'Album Title, The' }, - { token: '{Album CleanTitleThe}', example: 'Album Title, The' }, + + { token: '{Album CleanTitle}', example: 'Album Title' }, + { token: '{Album Type}', example: 'Album Type' }, + { token: '{Album Disambiguation}', example: 'Disambiguation' }, + { token: '{Album Genre}', example: 'Rock' }, + { token: '{Album MbId}', example: '082c6aff-a7cc-36e0-a960-35a578ecd937' } ]; @@ -121,9 +96,8 @@ const trackTitleTokens = [ const trackArtistTokens = [ { token: '{Track ArtistName}', example: 'Artist Name' }, - { token: '{Track ArtistCleanName}', example: 'Artist Name' }, { token: '{Track ArtistNameThe}', example: 'Artist Name, The' }, - { token: '{Track ArtistCleanNameThe}', example: 'Artist Name, The' }, + { token: '{Track ArtistCleanName}', example: 'Artist Name' }, { token: '{Track ArtistMbId}', example: 'db92a151-1ac2-438b-bc43-b82e149ddd50' } ]; @@ -239,7 +213,7 @@ class NamingModal extends Component { > - {translate('FileNameTokens')} + File Name Tokens @@ -578,7 +552,7 @@ class NamingModal extends Component { onSelectionChange={this.onInputSelectionChange} /> 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} - - { - settings.writeAudioTags.value !== 'no' && - - - {translate('EmbedCoverArtInAudioFiles')} - - - - - } - {translate('ScrubExistingTags')} diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js index ee51799f2..cf42e4c2a 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js @@ -14,7 +14,6 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds } from 'Helpers/Props'; -import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import NotificationEventItems from './NotificationEventItems'; import styles from './EditNotificationModalContent.css'; @@ -33,7 +32,6 @@ function EditNotificationModalContent(props) { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteNotificationPress, ...otherProps } = props; @@ -105,7 +103,7 @@ function EditNotificationModalContent(props) { @@ -142,12 +140,6 @@ function EditNotificationModalContent(props) { } - - { - this.props.toggleAdvancedSettings(); - }; - // // Render @@ -76,7 +65,6 @@ class EditNotificationModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -94,7 +82,6 @@ EditNotificationModalContentConnector.propTypes = { setNotificationFieldValue: PropTypes.func.isRequired, saveNotification: PropTypes.func.isRequired, testNotification: PropTypes.func.isRequired, - toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js index 6351c6f8a..b306f742a 100644 --- a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import Notifications from './Notifications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.notifications', sortByProp('name')), + createSortedSectionSelector('settings.notifications', sortByName), createTagsSelector(), (notifications, tagList) => { return { diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js index 0d1225a93..da1bf7f44 100644 --- a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js @@ -87,9 +87,9 @@ function EditDelayProfileModalContent(props) { { !isFetching && !!error ? - - {translate('AddDelayProfileError')} - : +
+ {translate('UnableToAddANewQualityProfilePleaseTryAgain')} +
: null } @@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) { { id === 1 ? - {translate('DefaultDelayProfileArtist')} + {translate('DefaultDelayProfileHelpText')} : @@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) { type={inputTypes.TAG} name="tags" {...tags} - helpText={translate('DelayProfileArtistTagsHelpText')} + helpText={translate('TagsHelpText')} onChange={onInputChange} /> diff --git a/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js b/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js index 5e719517f..d39303ecc 100644 --- a/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js +++ b/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js @@ -5,7 +5,7 @@ import FieldSet from 'Components/FieldSet'; import Icon from 'Components/Icon'; import PageSectionContent from 'Components/Page/PageSectionContent'; import { icons, metadataProfileNames } from 'Helpers/Props'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import translate from 'Utilities/String/translate'; import EditMetadataProfileModalConnector from './EditMetadataProfileModalConnector'; import MetadataProfile from './MetadataProfile'; @@ -59,20 +59,17 @@ class MetadataProfiles extends Component { >
{ - items - .filter((item) => item.name !== metadataProfileNames.NONE) - .sort(sortByProp('name')) - .map((item) => { - return ( - - ); - }) + items.filter((item) => item.name !== metadataProfileNames.NONE).sort(sortByName).map((item) => { + return ( + + ); + }) } b.name ? 1 : -1; }).map((x) => items[x.format]); } diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js index 4cb318463..581882ffd 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { cloneQualityProfile, deleteQualityProfile, fetchQualityProfiles } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import QualityProfiles from './QualityProfiles'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')), + createSortedSectionSelector('settings.qualityProfiles', sortByName), (qualityProfiles) => qualityProfiles ); } diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js index e1c695c42..c6c297c81 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js @@ -119,7 +119,7 @@ function EditReleaseProfileModalContent(props) { diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.css b/frontend/src/Settings/Quality/Definition/QualityDefinition.css index 860333725..e090428a1 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinition.css +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.css @@ -24,19 +24,19 @@ height: 20px; } -.track { +.bar { top: 9px; margin: 0 5px; height: 3px; background-color: var(--sliderAccentColor); box-shadow: 0 0 0 #000; - &:nth-child(3n + 1) { + &:nth-child(3n+1) { background-color: #ddd; } } -.thumb { +.handle { top: 1px; z-index: 0 !important; width: 18px; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts b/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts index 9c9e8393a..2b92fb212 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts @@ -1,6 +1,8 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'bar': string; + 'handle': string; 'kilobitsPerSecond': string; 'quality': string; 'qualityDefinition': string; @@ -8,9 +10,7 @@ interface CssExports { 'sizeLimit': string; 'sizes': string; 'slider': string; - 'thumb': string; 'title': string; - 'track': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.js b/frontend/src/Settings/Quality/Definition/QualityDefinition.js index 48251abfb..7d8a78737 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinition.js +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.js @@ -55,27 +55,6 @@ class QualityDefinition extends Component { }; } - // - // Control - - trackRenderer(props, state) { - return ( -
- ); - } - - thumbRenderer(props, state) { - return ( -
- ); - } - // // Listeners @@ -195,7 +174,6 @@ class QualityDefinition extends Component {
diff --git a/frontend/src/Settings/Settings.js b/frontend/src/Settings/Settings.js index d2a86adc6..36f47d82e 100644 --- a/frontend/src/Settings/Settings.js +++ b/frontend/src/Settings/Settings.js @@ -18,132 +18,132 @@ function Settings() { className={styles.link} to="/settings/mediamanagement" > - {translate('MediaManagement')} + Media Management
- {translate('MediaManagementSettingsSummary')} + Naming, file management settings and root folders
- {translate('Profiles')} + Profiles
- {translate('ProfilesSettingsArtistSummary')} + Quality, Metadata, Delay, and Release profiles
- {translate('Quality')} + Quality
- {translate('QualitySettingsSummary')} + Quality sizes and naming
- {translate('CustomFormats')} + Custom Formats
- {translate('CustomFormatsSettingsSummary')} + Custom Formats and Settings
- {translate('Indexers')} + Indexers
- {translate('IndexersSettingsSummary')} + Indexers and indexer options
- {translate('DownloadClients')} + Download Clients
- {translate('DownloadClientsSettingsSummary')} + Download clients, download handling and remote path mappings
- {translate('ImportLists')} + Import Lists
- {translate('ImportListsSettingsSummary')} + Import Lists
- {translate('Connect')} + Connect
- {translate('ConnectSettingsSummary')} + Notifications, connections to media servers/players and custom scripts
- {translate('Metadata')} + Metadata
- {translate('MetadataSettingsArtistSummary')} + Create metadata files when tracks are imported or artist are refreshed
- {translate('Tags')} + Tags
- {translate('TagsSettingsSummary')} + Manage artist, profile, restriction, and notification tags
- {translate('General')} + General
- {translate('GeneralSettingsSummary')} + Port, SSL, username/password, proxy, analytics and updates
- {translate('Ui')} + UI
- {translate('UiSettingsSummary')} + Calendar, date and color impaired options
diff --git a/frontend/src/Settings/SettingsToolbarConnector.js b/frontend/src/Settings/SettingsToolbarConnector.js index 65d937ab8..1e6f7a589 100644 --- a/frontend/src/Settings/SettingsToolbarConnector.js +++ b/frontend/src/Settings/SettingsToolbarConnector.js @@ -134,7 +134,6 @@ const historyShape = { }; SettingsToolbarConnector.propTypes = { - showSave: PropTypes.bool, hasPendingChanges: PropTypes.bool.isRequired, history: PropTypes.shape(historyShape).isRequired, onSavePress: PropTypes.func, diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js index 005547bb7..0d86721af 100644 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js +++ b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js @@ -13,7 +13,7 @@ import { } 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 translate from 'Utilities/String/translate'; import AutoTagging from './AutoTagging'; import EditAutoTaggingModal from './EditAutoTaggingModal'; @@ -27,7 +27,7 @@ export default function AutoTaggings() { isFetching, isPopulated } = useSelector( - createSortedSectionSelector('settings.autoTaggings', sortByProp('name')) + createSortedSectionSelector('settings.autoTaggings', sortByName) ); const tagList = useSelector(createTagsSelector()); diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css index d503b0af3..a197dbcd4 100644 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css +++ b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css @@ -25,8 +25,3 @@ border-radius: 4px; background-color: var(--cardCenterBackgroundColor); } - -.autoTaggings { - display: flex; - flex-wrap: wrap; -} diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts index 2a7f6b41e..1339caf02 100644 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts +++ b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts @@ -2,7 +2,6 @@ // Please do not change this file! interface CssExports { 'addSpecification': string; - 'autoTaggings': string; 'center': string; 'deleteButton': string; 'rightButtons': string; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js index 04302729b..2ab1e4a1c 100644 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js @@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
- +
- +
} diff --git a/frontend/src/Settings/Tags/TagInUse.js b/frontend/src/Settings/Tags/TagInUse.js index 27228fa2e..9fb57d230 100644 --- a/frontend/src/Settings/Tags/TagInUse.js +++ b/frontend/src/Settings/Tags/TagInUse.js @@ -12,7 +12,7 @@ export default function TagInUse(props) { return null; } - if (count > 1 && labelPlural) { + if (count > 1 && labelPlural ) { return (
{count} {labelPlural.toLowerCase()} diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js index 15f31d3c5..770dc4720 100644 --- a/frontend/src/Settings/Tags/TagsConnector.js +++ b/frontend/src/Settings/Tags/TagsConnector.js @@ -3,14 +3,12 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; -import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import { fetchTagDetails } from 'Store/Actions/tagActions'; import Tags from './Tags'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('tags', sortByProp('label')), + (state) => state.tags, (tags) => { const isFetching = tags.isFetching || tags.details.isFetching; const error = tags.error || tags.details.error; @@ -27,7 +25,6 @@ function createMapStateToProps() { } const mapDispatchToProps = { - dispatchFetchTags: fetchTags, dispatchFetchTagDetails: fetchTagDetails, dispatchFetchDelayProfiles: fetchDelayProfiles, dispatchFetchImportLists: fetchImportLists, @@ -44,7 +41,6 @@ class MetadatasConnector extends Component { componentDidMount() { const { - dispatchFetchTags, dispatchFetchTagDetails, dispatchFetchDelayProfiles, dispatchFetchImportLists, @@ -54,7 +50,6 @@ class MetadatasConnector extends Component { dispatchFetchDownloadClients } = this.props; - dispatchFetchTags(); dispatchFetchTagDetails(); dispatchFetchDelayProfiles(); dispatchFetchImportLists(); @@ -77,7 +72,6 @@ class MetadatasConnector extends Component { } MetadatasConnector.propTypes = { - dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTagDetails: PropTypes.func.isRequired, dispatchFetchDelayProfiles: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired, diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js index cc27829df..902b922f9 100644 --- a/frontend/src/Settings/UI/UISettings.js +++ b/frontend/src/Settings/UI/UISettings.js @@ -22,19 +22,19 @@ export const firstDayOfWeekOptions = [ ]; export const weekColumnOptions = [ - { key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' }, - { key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' }, - { key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' }, - { key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' } + { key: 'ddd M/D', value: 'Tue 3/25' }, + { key: 'ddd MM/DD', value: 'Tue 03/25' }, + { key: 'ddd D/M', value: 'Tue 25/3' }, + { key: 'ddd DD/MM', value: 'Tue 25/03' } ]; const shortDateFormatOptions = [ - { key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' }, - { key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' }, - { key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' }, - { key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' }, - { key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' }, - { key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' } + { key: 'MMM D YYYY', value: 'Mar 25 2014' }, + { key: 'DD MMM YYYY', value: '25 Mar 2014' }, + { key: 'MM/D/YYYY', value: '03/25/2014' }, + { key: 'MM/DD/YYYY', value: '03/25/2014' }, + { key: 'DD/MM/YYYY', value: '25/03/2014' }, + { key: 'YYYY-MM-DD', value: '2014-03-25' } ]; const longDateFormatOptions = [ @@ -69,7 +69,7 @@ class UISettings extends Component { .map((theme) => ({ key: theme, value: titleCase(theme) })); return ( - +
- {translate('UiLanguage')} + {translate('UILanguage')} { - lastTestData = null; - dispatch(set({ section, isTesting: false, diff --git a/frontend/src/Store/Actions/Settings/customFormats.js b/frontend/src/Store/Actions/Settings/customFormats.js index 3b8a209f9..4a175abea 100644 --- a/frontend/src/Store/Actions/Settings/customFormats.js +++ b/frontend/src/Store/Actions/Settings/customFormats.js @@ -1,12 +1,7 @@ import { createAction } from 'redux-actions'; -import { sortDirections } from 'Helpers/Props'; -import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; -import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; -import createSetClientSideCollectionSortReducer - from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; import getSectionState from 'Utilities/State/getSectionState'; @@ -26,9 +21,6 @@ export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat'; export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat'; export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue'; export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat'; -export const BULK_EDIT_CUSTOM_FORMATS = 'settings/downloadClients/bulkEditCustomFormats'; -export const BULK_DELETE_CUSTOM_FORMATS = 'settings/downloadClients/bulkDeleteCustomFormats'; -export const SET_MANAGE_CUSTOM_FORMATS_SORT = 'settings/downloadClients/setManageCustomFormatsSort'; // // Action Creators @@ -36,9 +28,6 @@ export const SET_MANAGE_CUSTOM_FORMATS_SORT = 'settings/downloadClients/setManag export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS); export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT); export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT); -export const bulkEditCustomFormats = createThunk(BULK_EDIT_CUSTOM_FORMATS); -export const bulkDeleteCustomFormats = createThunk(BULK_DELETE_CUSTOM_FORMATS); -export const setManageCustomFormatsSort = createAction(SET_MANAGE_CUSTOM_FORMATS_SORT); export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => { return { @@ -58,30 +47,20 @@ export default { // State defaultState: { - isFetching: false, - isPopulated: false, - error: null, - isSaving: false, - saveError: null, - isDeleting: false, - deleteError: null, - items: [], - pendingChanges: {}, - isSchemaFetching: false, isSchemaPopulated: false, - schemaError: null, + isFetching: false, + isPopulated: false, schema: { includeCustomFormatWhenRenaming: false }, - - sortKey: 'name', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - name: ({ name }) => { - return name.toLocaleLowerCase(); - } - } + error: null, + isDeleting: false, + deleteError: null, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} }, // @@ -103,10 +82,7 @@ export default { })); createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch); - }, - - [BULK_EDIT_CUSTOM_FORMATS]: createBulkEditItemHandler(section, '/customformat/bulk'), - [BULK_DELETE_CUSTOM_FORMATS]: createBulkRemoveItemHandler(section, '/customformat/bulk') + } }, // @@ -126,9 +102,7 @@ export default { newState.pendingChanges = pendingChanges; return updateSectionState(state, section, newState); - }, - - [SET_MANAGE_CUSTOM_FORMATS_SORT]: createSetClientSideCollectionSortReducer(section) + } } }; diff --git a/frontend/src/Store/Actions/Settings/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js index 1113e7daf..aee945ef5 100644 --- a/frontend/src/Store/Actions/Settings/downloadClients.js +++ b/frontend/src/Store/Actions/Settings/downloadClients.js @@ -96,8 +96,8 @@ export default { sortKey: 'name', sortDirection: sortDirections.ASCENDING, sortPredicates: { - name: ({ name }) => { - return name.toLocaleLowerCase(); + name: function(item) { + return item.name.toLowerCase(); } } }, diff --git a/frontend/src/Store/Actions/Settings/indexerFlags.js b/frontend/src/Store/Actions/Settings/indexerFlags.js deleted file mode 100644 index a53fe1c61..000000000 --- a/frontend/src/Store/Actions/Settings/indexerFlags.js +++ /dev/null @@ -1,48 +0,0 @@ -import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; -import { createThunk } from 'Store/thunks'; - -// -// Variables - -const section = 'settings.indexerFlags'; - -// -// Actions Types - -export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags'; - -// -// Action Creators - -export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS); - -// -// Details - -export default { - - // - // State - - defaultState: { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }, - - // - // Action Handlers - - actionHandlers: { - [FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag') - }, - - // - // Reducers - - reducers: { - - } - -}; diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js index 511a2e475..1e9aded2f 100644 --- a/frontend/src/Store/Actions/Settings/indexers.js +++ b/frontend/src/Store/Actions/Settings/indexers.js @@ -100,8 +100,8 @@ export default { sortKey: 'name', sortDirection: sortDirections.ASCENDING, sortPredicates: { - name: ({ name }) => { - return name.toLocaleLowerCase(); + name: function(item) { + return item.name.toLowerCase(); } } }, diff --git a/frontend/src/Store/Actions/albumActions.js b/frontend/src/Store/Actions/albumActions.js index 4ac39a0aa..d1ac2c348 100644 --- a/frontend/src/Store/Actions/albumActions.js +++ b/frontend/src/Store/Actions/albumActions.js @@ -36,11 +36,6 @@ export const defaultState = { sortPredicates: { rating: function(item) { return item.ratings.value; - }, - size: function(item) { - const { statistics = {} } = item; - - return statistics.sizeOnDisk || 0; } }, @@ -86,12 +81,6 @@ export const defaultState = { isSortable: true, isVisible: false }, - { - name: 'size', - label: () => translate('Size'), - isSortable: true, - isVisible: false - }, { name: 'rating', label: () => translate('Rating'), diff --git a/frontend/src/Store/Actions/albumSelectionActions.js b/frontend/src/Store/Actions/albumSelectionActions.js deleted file mode 100644 index f19f5b691..000000000 --- a/frontend/src/Store/Actions/albumSelectionActions.js +++ /dev/null @@ -1,86 +0,0 @@ -import moment from 'moment'; -import { createAction } from 'redux-actions'; -import { sortDirections } from 'Helpers/Props'; -import { createThunk, handleThunks } from 'Store/thunks'; -import updateSectionState from 'Utilities/State/updateSectionState'; -import createFetchHandler from './Creators/createFetchHandler'; -import createHandleActions from './Creators/createHandleActions'; -import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; - -// -// Variables - -export const section = 'albumSelection'; - -// -// State - -export const defaultState = { - isFetching: false, - isReprocessing: false, - isPopulated: false, - error: null, - sortKey: 'title', - sortDirection: sortDirections.ASCENDING, - items: [], - sortPredicates: { - title: ({ title }) => { - return title.toLocaleLowerCase(); - }, - - releaseDate: function({ releaseDate }, direction) { - if (releaseDate) { - return moment(releaseDate).unix(); - } - - if (direction === sortDirections.DESCENDING) { - return 0; - } - - return Number.MAX_VALUE; - } - } -}; - -export const persistState = [ - 'albumSelection.sortKey', - 'albumSelection.sortDirection' -]; - -// -// Actions Types - -export const FETCH_ALBUMS = 'albumSelection/fetchAlbums'; -export const SET_ALBUMS_SORT = 'albumSelection/setAlbumsSort'; -export const CLEAR_ALBUMS = 'albumSelection/clearAlbums'; - -// -// Action Creators - -export const fetchAlbums = createThunk(FETCH_ALBUMS); -export const setAlbumsSort = createAction(SET_ALBUMS_SORT); -export const clearAlbums = createAction(CLEAR_ALBUMS); - -// -// Action Handlers - -export const actionHandlers = handleThunks({ - [FETCH_ALBUMS]: createFetchHandler(section, '/album') -}); - -// -// Reducers - -export const reducers = createHandleActions({ - - [SET_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(section), - - [CLEAR_ALBUMS]: (state) => { - return updateSectionState(state, section, { - ...defaultState, - sortKey: state.sortKey, - sortDirection: state.sortDirection - }); - } - -}, defaultState, section); diff --git a/frontend/src/Store/Actions/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js index 736502460..756a6ee25 100644 --- a/frontend/src/Store/Actions/artistIndexActions.js +++ b/frontend/src/Store/Actions/artistIndexActions.js @@ -1,6 +1,6 @@ import { createAction } from 'redux-actions'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, sortDirections } from 'Helpers/Props'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import translate from 'Utilities/String/translate'; import { filterPredicates, filters, sortPredicates } from './artistActions'; import createHandleActions from './Creators/createHandleActions'; @@ -94,12 +94,6 @@ export const defaultState = { isSortable: true, isVisible: false }, - { - name: 'monitorNewItems', - label: () => translate('MonitorNewItems'), - isSortable: true, - isVisible: false - }, { name: 'nextAlbum', label: () => translate('NextAlbum'), @@ -151,7 +145,7 @@ export const defaultState = { { name: 'genres', label: () => translate('Genres'), - isSortable: true, + isSortable: false, isVisible: false }, { @@ -273,12 +267,6 @@ export const defaultState = { type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.METADATA_PROFILE }, - { - name: 'monitorNewItems', - label: () => translate('MonitorNewItems'), - type: filterBuilderTypes.EXACT, - valueType: filterBuilderValueTypes.MONITOR_NEW_ITEMS - }, { name: 'nextAlbum', label: () => translate('NextAlbum'), @@ -334,7 +322,7 @@ export const defaultState = { return acc; }, []); - return tagList.sort(sortByProp('name')); + return tagList.sort(sortByName); } }, { diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js index e13ff4672..d473f1368 100644 --- a/frontend/src/Store/Actions/calendarActions.js +++ b/frontend/src/Store/Actions/calendarActions.js @@ -4,10 +4,9 @@ import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import * as calendarViews from 'Calendar/calendarViews'; import * as commandNames from 'Commands/commandNames'; -import { filterBuilderTypes, filterBuilderValueTypes, filterTypes } from 'Helpers/Props'; +import { filterTypes } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; -import findSelectedFilters from 'Utilities/Filter/findSelectedFilters'; import translate from 'Utilities/String/translate'; import { set, update } from './baseActions'; import { executeCommandHelper } from './commandActions'; @@ -55,8 +54,8 @@ export const defaultState = { label: () => translate('All'), filters: [ { - key: 'unmonitored', - value: [true], + key: 'monitored', + value: false, type: filterTypes.EQUAL } ] @@ -66,35 +65,19 @@ export const defaultState = { label: () => translate('MonitoredOnly'), filters: [ { - key: 'unmonitored', - value: [false], + key: 'monitored', + value: true, type: filterTypes.EQUAL } ] } - ], - - filterBuilderProps: [ - { - name: 'unmonitored', - label: () => translate('IncludeUnmonitored'), - type: filterBuilderTypes.EQUAL, - valueType: filterBuilderValueTypes.BOOL - }, - { - name: 'tags', - label: () => translate('Tags'), - type: filterBuilderTypes.CONTAINS, - valueType: filterBuilderValueTypes.TAG - } ] }; export const persistState = [ 'calendar.view', 'calendar.selectedFilterKey', - 'calendar.options', - 'calendar.customFilters' + 'calendar.options' ]; // @@ -206,10 +189,6 @@ function isRangePopulated(start, end, state) { return false; } -function getCustomFilters(state, type) { - return state.customFilters.items.filter((customFilter) => customFilter.type === type); -} - // // Action Creators @@ -231,8 +210,7 @@ export const actionHandlers = handleThunks({ [FETCH_CALENDAR]: function(getState, payload, dispatch) { const state = getState(); const calendar = state.calendar; - const customFilters = getCustomFilters(state, section); - const selectedFilters = findSelectedFilters(calendar.selectedFilterKey, calendar.filters, customFilters); + const unmonitored = calendar.selectedFilterKey === 'all'; const { time = calendar.time, @@ -259,26 +237,13 @@ export const actionHandlers = handleThunks({ dispatch(set(attrs)); - const requestParams = { - start, - end - }; - - selectedFilters.forEach((selectedFilter) => { - if (selectedFilter.key === 'unmonitored') { - requestParams.unmonitored = selectedFilter.value.includes(true); - } - - if (selectedFilter.key === 'tags') { - requestParams.tags = selectedFilter.value.join(','); - } - }); - - requestParams.unmonitored = requestParams.unmonitored ?? false; - const promise = createAjaxRequest({ url: '/calendar', - data: requestParams + data: { + unmonitored, + start, + end + } }).request; promise.done((data) => { diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 9d16d29c4..225698229 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -150,7 +150,7 @@ export const defaultState = { }, { key: 'importFailed', - label: () => translate('ImportCompleteFailed'), + label: () => translate('ImportFailed'), filters: [ { key: 'eventType', diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index 85fda482b..95b02d089 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -1,6 +1,5 @@ import * as albums from './albumActions'; import * as albumHistory from './albumHistoryActions'; -import * as albumSelection from './albumSelectionActions'; import * as app from './appActions'; import * as artist from './artistActions'; import * as artistHistory from './artistHistoryActions'; @@ -14,7 +13,6 @@ import * as history from './historyActions'; import * as interactiveImportActions from './interactiveImportActions'; import * as oAuth from './oAuthActions'; import * as organizePreview from './organizePreviewActions'; -import * as parse from './parseActions'; import * as paths from './pathActions'; import * as providerOptions from './providerOptionActions'; import * as queue from './queueActions'; @@ -30,28 +28,26 @@ import * as wanted from './wantedActions'; export default [ app, - albums, - albumHistory, - albumSelection, - artist, - artistHistory, - artistIndex, blocklist, captcha, calendar, commands, customFilters, + albums, trackFiles, + albumHistory, history, interactiveImportActions, oAuth, organizePreview, retagPreview, - parse, paths, providerOptions, queue, releases, + artist, + artistHistory, + artistIndex, search, settings, system, diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index a250292c5..e0e295568 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -16,6 +16,7 @@ import createSetClientSideCollectionSortReducer from './Creators/Reducers/create export const section = 'interactiveImport'; +const albumsSection = `${section}.albums`; const trackFilesSection = `${section}.trackFiles`; let abortCurrentFetchRequest = null; let abortCurrentRequest = null; @@ -57,6 +58,15 @@ export const defaultState = { } }, + albums: { + isFetching: false, + isPopulated: false, + error: null, + sortKey: 'albumTitle', + sortDirection: sortDirections.ASCENDING, + items: [] + }, + trackFiles: { isFetching: false, isPopulated: false, @@ -87,6 +97,10 @@ export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder'; export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder'; export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode'; +export const FETCH_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/fetchInteractiveImportAlbums'; +export const SET_INTERACTIVE_IMPORT_ALBUMS_SORT = 'interactiveImport/clearInteractiveImportAlbumsSort'; +export const CLEAR_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/clearInteractiveImportAlbums'; + export const FETCH_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/fetchInteractiveImportTrackFiles'; export const CLEAR_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/clearInteractiveImportTrackFiles'; @@ -103,6 +117,10 @@ export const addRecentFolder = createAction(ADD_RECENT_FOLDER); export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER); export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE); +export const fetchInteractiveImportAlbums = createThunk(FETCH_INTERACTIVE_IMPORT_ALBUMS); +export const setInteractiveImportAlbumsSort = createAction(SET_INTERACTIVE_IMPORT_ALBUMS_SORT); +export const clearInteractiveImportAlbums = createAction(CLEAR_INTERACTIVE_IMPORT_ALBUMS); + export const fetchInteractiveImportTrackFiles = createThunk(FETCH_INTERACTIVE_IMPORT_TRACKFILES); export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_IMPORT_TRACKFILES); @@ -190,7 +208,6 @@ export const actionHandlers = handleThunks({ trackIds: (item.tracks || []).map((e) => e.id), quality: item.quality, releaseGroup: item.releaseGroup, - indexerFlags: item.indexerFlags, downloadId: item.downloadId, additionalFile: item.additionalFile, replaceExistingFiles: item.replaceExistingFiles, @@ -235,6 +252,8 @@ export const actionHandlers = handleThunks({ }); }, + [FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler(albumsSection, '/album'), + [FETCH_INTERACTIVE_IMPORT_TRACKFILES]: createFetchHandler(trackFilesSection, '/trackFile') }); @@ -316,6 +335,14 @@ export const reducers = createHandleActions({ return Object.assign({}, state, { importMode: payload.importMode }); }, + [SET_INTERACTIVE_IMPORT_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(albumsSection), + + [CLEAR_INTERACTIVE_IMPORT_ALBUMS]: (state) => { + return updateSectionState(state, albumsSection, { + ...defaultState.albums + }); + }, + [CLEAR_INTERACTIVE_IMPORT_TRACKFILES]: (state) => { return updateSectionState(state, trackFilesSection, { ...defaultState.trackFiles diff --git a/frontend/src/Store/Actions/parseActions.ts b/frontend/src/Store/Actions/parseActions.ts deleted file mode 100644 index d4b6e9bcb..000000000 --- a/frontend/src/Store/Actions/parseActions.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Dispatch } from 'redux'; -import { createAction } from 'redux-actions'; -import { batchActions } from 'redux-batched-actions'; -import AppState from 'App/State/AppState'; -import { createThunk, handleThunks } from 'Store/thunks'; -import createAjaxRequest from 'Utilities/createAjaxRequest'; -import { set, update } from './baseActions'; -import createHandleActions from './Creators/createHandleActions'; -import createClearReducer from './Creators/Reducers/createClearReducer'; - -interface FetchPayload { - title: string; -} - -// -// Variables - -export const section = 'parse'; -let parseTimeout: number | null = null; -let abortCurrentRequest: (() => void) | null = null; - -// -// State - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - item: {}, -}; - -// -// Actions Types - -export const FETCH = 'parse/fetch'; -export const CLEAR = 'parse/clear'; - -// -// Action Creators - -export const fetch = createThunk(FETCH); -export const clear = createAction(CLEAR); - -// -// Action Handlers - -export const actionHandlers = handleThunks({ - [FETCH]: function ( - _getState: () => AppState, - payload: FetchPayload, - dispatch: Dispatch - ) { - if (parseTimeout) { - clearTimeout(parseTimeout); - } - - parseTimeout = window.setTimeout(async () => { - dispatch(set({ section, isFetching: true })); - - if (abortCurrentRequest) { - abortCurrentRequest(); - } - - const { request, abortRequest } = createAjaxRequest({ - url: '/parse', - data: { - title: payload.title, - }, - }); - - try { - const data = await request; - - dispatch( - batchActions([ - update({ section, data }), - - set({ - section, - isFetching: false, - isPopulated: true, - error: null, - }), - ]) - ); - } catch (error) { - dispatch( - set({ - section, - isAdding: false, - isAdded: false, - addError: error, - }) - ); - } - - abortCurrentRequest = abortRequest; - }, 300); - }, -}); - -// -// Reducers - -export const reducers = createHandleActions( - { - [CLEAR]: createClearReducer(section, defaultState), - }, - defaultState, - section -); diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index c4955c915..1c9b6f5ef 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -219,9 +219,8 @@ export const defaultState = { }; export const persistState = [ - 'releases.album.selectedFilterKey', + 'releases.selectedFilterKey', 'releases.album.customFilters', - 'releases.artist.selectedFilterKey', 'releases.artist.customFilters' ]; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index 54b059083..b787110c1 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -11,7 +11,6 @@ import downloadClients from './Settings/downloadClients'; import general from './Settings/general'; import importListExclusions from './Settings/importListExclusions'; import importLists from './Settings/importLists'; -import indexerFlags from './Settings/indexerFlags'; import indexerOptions from './Settings/indexerOptions'; import indexers from './Settings/indexers'; import languages from './Settings/languages'; @@ -39,7 +38,6 @@ export * from './Settings/downloadClientOptions'; export * from './Settings/general'; export * from './Settings/importLists'; export * from './Settings/importListExclusions'; -export * from './Settings/indexerFlags'; export * from './Settings/indexerOptions'; export * from './Settings/indexers'; export * from './Settings/languages'; @@ -75,7 +73,6 @@ export const defaultState = { downloadClients: downloadClients.defaultState, downloadClientOptions: downloadClientOptions.defaultState, general: general.defaultState, - indexerFlags: indexerFlags.defaultState, indexerOptions: indexerOptions.defaultState, indexers: indexers.defaultState, importLists: importLists.defaultState, @@ -122,7 +119,6 @@ export const actionHandlers = handleThunks({ ...downloadClients.actionHandlers, ...downloadClientOptions.actionHandlers, ...general.actionHandlers, - ...indexerFlags.actionHandlers, ...indexerOptions.actionHandlers, ...indexers.actionHandlers, ...importLists.actionHandlers, @@ -160,7 +156,6 @@ export const reducers = createHandleActions({ ...downloadClients.reducers, ...downloadClientOptions.reducers, ...general.reducers, - ...indexerFlags.reducers, ...indexerOptions.reducers, ...indexers.reducers, ...importLists.reducers, diff --git a/frontend/src/Store/Actions/trackActions.js b/frontend/src/Store/Actions/trackActions.js index a71388c88..bd1f472c3 100644 --- a/frontend/src/Store/Actions/trackActions.js +++ b/frontend/src/Store/Actions/trackActions.js @@ -77,15 +77,6 @@ export const defaultState = { }), isVisible: false }, - { - name: 'indexerFlags', - columnLabel: () => translate('IndexerFlags'), - label: React.createElement(Icon, { - name: icons.FLAG, - title: () => translate('IndexerFlags') - }), - isVisible: false - }, { name: 'status', label: () => translate('Status'), diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js index 61d6f7752..35aa162d4 100644 --- a/frontend/src/Store/Actions/wantedActions.js +++ b/frontend/src/Store/Actions/wantedActions.js @@ -52,12 +52,6 @@ export const defaultState = { isSortable: true, isVisible: true }, - { - name: 'albums.lastSearchTime', - label: () => translate('LastSearched'), - isSortable: true, - isVisible: false - }, // { // name: 'status', // label: 'Status', @@ -137,12 +131,6 @@ export const defaultState = { // label: 'Status', // isVisible: true // }, - { - name: 'albums.lastSearchTime', - label: () => translate('LastSearched'), - isSortable: true, - isVisible: false - }, { name: 'actions', columnLabel: () => translate('Actions'), diff --git a/frontend/src/Store/Selectors/createAllArtistSelector.ts b/frontend/src/Store/Selectors/createAllArtistSelector.js similarity index 71% rename from frontend/src/Store/Selectors/createAllArtistSelector.ts rename to frontend/src/Store/Selectors/createAllArtistSelector.js index 6b6010429..38b1bcef1 100644 --- a/frontend/src/Store/Selectors/createAllArtistSelector.ts +++ b/frontend/src/Store/Selectors/createAllArtistSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createAllArtistSelector() { return createSelector( - (state: AppState) => state.artist, + (state) => state.artist, (artist) => { return artist.items; } diff --git a/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts b/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts index 414a451f5..2ae54a10c 100644 --- a/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts +++ b/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts @@ -1,5 +1,4 @@ import { createSelector } from 'reselect'; -import AlbumAppState from 'App/State/AlbumAppState'; import AppState from 'App/State/AppState'; import Artist from 'Artist/Artist'; import { createArtistSelectorForHook } from './createArtistSelector'; @@ -8,11 +7,11 @@ function createArtistAlbumsSelector(artistId: number) { return createSelector( (state: AppState) => state.albums, createArtistSelectorForHook(artistId), - (albums: AlbumAppState, artist = {} as Artist) => { + (albums, artist = {} as Artist) => { const { isFetching, isPopulated, error, items } = albums; const filteredAlbums = items.filter( - (album) => album.artistId === artist.id + (album) => album.artist.artistMetadataId === artist.artistMetadataId ); return { diff --git a/frontend/src/Store/Selectors/createArtistCountSelector.ts b/frontend/src/Store/Selectors/createArtistCountSelector.js similarity index 65% rename from frontend/src/Store/Selectors/createArtistCountSelector.ts rename to frontend/src/Store/Selectors/createArtistCountSelector.js index b432d64a7..31e0a39fc 100644 --- a/frontend/src/Store/Selectors/createArtistCountSelector.ts +++ b/frontend/src/Store/Selectors/createArtistCountSelector.js @@ -1,19 +1,18 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import createAllArtistSelector from './createAllArtistSelector'; function createArtistCountSelector() { return createSelector( createAllArtistSelector(), - (state: AppState) => state.artist.error, - (state: AppState) => state.artist.isFetching, - (state: AppState) => state.artist.isPopulated, + (state) => state.artist.error, + (state) => state.artist.isFetching, + (state) => state.artist.isPopulated, (artists, error, isFetching, isPopulated) => { return { count: artists.length, error, isFetching, - isPopulated, + isPopulated }; } ); diff --git a/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts b/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts index fa60d936d..0acbd3997 100644 --- a/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts +++ b/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts @@ -1,14 +1,13 @@ import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; import Artist from 'Artist/Artist'; -import MetadataProfile from 'typings/MetadataProfile'; import { createArtistSelectorForHook } from './createArtistSelector'; function createArtistMetadataProfileSelector(artistId: number) { return createSelector( (state: AppState) => state.settings.metadataProfiles.items, createArtistSelectorForHook(artistId), - (metadataProfiles: MetadataProfile[], artist = {} as Artist) => { + (metadataProfiles, artist = {} as Artist) => { return metadataProfiles.find((profile) => { return profile.id === artist.metadataProfileId; }); diff --git a/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts b/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts index 67639919b..99325276f 100644 --- a/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts +++ b/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts @@ -1,14 +1,13 @@ import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; import Artist from 'Artist/Artist'; -import QualityProfile from 'typings/QualityProfile'; import { createArtistSelectorForHook } from './createArtistSelector'; function createArtistQualityProfileSelector(artistId: number) { return createSelector( (state: AppState) => state.settings.qualityProfiles.items, createArtistSelectorForHook(artistId), - (qualityProfiles: QualityProfile[], artist = {} as Artist) => { + (qualityProfiles, artist = {} as Artist) => { return qualityProfiles.find( (profile) => profile.id === artist.qualityProfileId ); diff --git a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts b/frontend/src/Store/Selectors/createCommandExecutingSelector.js similarity index 50% rename from frontend/src/Store/Selectors/createCommandExecutingSelector.ts rename to frontend/src/Store/Selectors/createCommandExecutingSelector.js index 6a80e172b..6037d5820 100644 --- a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts +++ b/frontend/src/Store/Selectors/createCommandExecutingSelector.js @@ -2,10 +2,13 @@ import { createSelector } from 'reselect'; import { isCommandExecuting } from 'Utilities/Command'; import createCommandSelector from './createCommandSelector'; -function createCommandExecutingSelector(name: string, contraints = {}) { - return createSelector(createCommandSelector(name, contraints), (command) => { - return isCommandExecuting(command); - }); +function createCommandExecutingSelector(name, contraints = {}) { + return createSelector( + createCommandSelector(name, contraints), + (command) => { + return isCommandExecuting(command); + } + ); } export default createCommandExecutingSelector; diff --git a/frontend/src/Store/Selectors/createCommandSelector.js b/frontend/src/Store/Selectors/createCommandSelector.js new file mode 100644 index 000000000..709dfebaf --- /dev/null +++ b/frontend/src/Store/Selectors/createCommandSelector.js @@ -0,0 +1,14 @@ +import { createSelector } from 'reselect'; +import { findCommand } from 'Utilities/Command'; +import createCommandsSelector from './createCommandsSelector'; + +function createCommandSelector(name, contraints = {}) { + return createSelector( + createCommandsSelector(), + (commands) => { + return findCommand(commands, { name, ...contraints }); + } + ); +} + +export default createCommandSelector; diff --git a/frontend/src/Store/Selectors/createCommandSelector.ts b/frontend/src/Store/Selectors/createCommandSelector.ts deleted file mode 100644 index cced7b186..000000000 --- a/frontend/src/Store/Selectors/createCommandSelector.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createSelector } from 'reselect'; -import { findCommand } from 'Utilities/Command'; -import createCommandsSelector from './createCommandsSelector'; - -function createCommandSelector(name: string, contraints = {}) { - return createSelector(createCommandsSelector(), (commands) => { - return findCommand(commands, { name, ...contraints }); - }); -} - -export default createCommandSelector; diff --git a/frontend/src/Store/Selectors/createCommandsSelector.ts b/frontend/src/Store/Selectors/createCommandsSelector.js similarity index 71% rename from frontend/src/Store/Selectors/createCommandsSelector.ts rename to frontend/src/Store/Selectors/createCommandsSelector.js index 2dd5d24a2..7b9edffd9 100644 --- a/frontend/src/Store/Selectors/createCommandsSelector.ts +++ b/frontend/src/Store/Selectors/createCommandsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createCommandsSelector() { return createSelector( - (state: AppState) => state.commands, + (state) => state.commands, (commands) => { return commands.items; } diff --git a/frontend/src/Store/Selectors/createDeepEqualSelector.js b/frontend/src/Store/Selectors/createDeepEqualSelector.js new file mode 100644 index 000000000..85562f28b --- /dev/null +++ b/frontend/src/Store/Selectors/createDeepEqualSelector.js @@ -0,0 +1,9 @@ +import _ from 'lodash'; +import { createSelectorCreator, defaultMemoize } from 'reselect'; + +const createDeepEqualSelector = createSelectorCreator( + defaultMemoize, + _.isEqual +); + +export default createDeepEqualSelector; diff --git a/frontend/src/Store/Selectors/createDeepEqualSelector.ts b/frontend/src/Store/Selectors/createDeepEqualSelector.ts deleted file mode 100644 index 9d4a63d2e..000000000 --- a/frontend/src/Store/Selectors/createDeepEqualSelector.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { isEqual } from 'lodash'; -import { createSelectorCreator, defaultMemoize } from 'reselect'; - -const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual); - -export default createDeepEqualSelector; diff --git a/frontend/src/Store/Selectors/createExecutingCommandsSelector.ts b/frontend/src/Store/Selectors/createExecutingCommandsSelector.js similarity index 78% rename from frontend/src/Store/Selectors/createExecutingCommandsSelector.ts rename to frontend/src/Store/Selectors/createExecutingCommandsSelector.js index dd16571fc..266865a8a 100644 --- a/frontend/src/Store/Selectors/createExecutingCommandsSelector.ts +++ b/frontend/src/Store/Selectors/createExecutingCommandsSelector.js @@ -1,10 +1,9 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import { isCommandExecuting } from 'Utilities/Command'; function createExecutingCommandsSelector() { return createSelector( - (state: AppState) => state.commands.items, + (state) => state.commands.items, (commands) => { return commands.filter((command) => isCommandExecuting(command)); } diff --git a/frontend/src/Store/Selectors/createExistingArtistSelector.ts b/frontend/src/Store/Selectors/createExistingArtistSelector.js similarity index 58% rename from frontend/src/Store/Selectors/createExistingArtistSelector.ts rename to frontend/src/Store/Selectors/createExistingArtistSelector.js index 91b5bc4d6..4811f2034 100644 --- a/frontend/src/Store/Selectors/createExistingArtistSelector.ts +++ b/frontend/src/Store/Selectors/createExistingArtistSelector.js @@ -1,15 +1,13 @@ -import { some } from 'lodash'; +import _ from 'lodash'; import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import createAllArtistSelector from './createAllArtistSelector'; function createExistingArtistSelector() { return createSelector( - (_: AppState, { foreignArtistId }: { foreignArtistId: string }) => - foreignArtistId, + (state, { foreignArtistId }) => foreignArtistId, createAllArtistSelector(), (foreignArtistId, artist) => { - return some(artist, { foreignArtistId }); + return _.some(artist, { foreignArtistId }); } ); } diff --git a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts b/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts deleted file mode 100644 index 90587639c..000000000 --- a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -const createIndexerFlagsSelector = createSelector( - (state: AppState) => state.settings.indexerFlags, - (indexerFlags) => indexerFlags -); - -export default createIndexerFlagsSelector; diff --git a/frontend/src/Store/Selectors/createMetadataProfileSelector.js b/frontend/src/Store/Selectors/createMetadataProfileSelector.js new file mode 100644 index 000000000..bdd0d0636 --- /dev/null +++ b/frontend/src/Store/Selectors/createMetadataProfileSelector.js @@ -0,0 +1,15 @@ +import { createSelector } from 'reselect'; + +function createMetadataProfileSelector() { + return createSelector( + (state, { metadataProfileId }) => metadataProfileId, + (state) => state.settings.metadataProfiles.items, + (metadataProfileId, metadataProfiles) => { + return metadataProfiles.find((profile) => { + return profile.id === metadataProfileId; + }); + } + ); +} + +export default createMetadataProfileSelector; diff --git a/frontend/src/Store/Selectors/createMetadataProfileSelector.ts b/frontend/src/Store/Selectors/createMetadataProfileSelector.ts deleted file mode 100644 index ae4c061db..000000000 --- a/frontend/src/Store/Selectors/createMetadataProfileSelector.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -function createMetadataProfileSelector() { - return createSelector( - (_: AppState, { metadataProfileId }: { metadataProfileId: number }) => - metadataProfileId, - (state: AppState) => state.settings.metadataProfiles.items, - (metadataProfileId, metadataProfiles) => { - return metadataProfiles.find( - (profile) => profile.id === metadataProfileId - ); - } - ); -} - -export default createMetadataProfileSelector; diff --git a/frontend/src/Store/Selectors/createMultiArtistsSelector.ts b/frontend/src/Store/Selectors/createMultiArtistsSelector.ts deleted file mode 100644 index d8f7ea92b..000000000 --- a/frontend/src/Store/Selectors/createMultiArtistsSelector.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; - -function createMultiArtistsSelector(artistIds: number[]) { - return createSelector( - (state: AppState) => state.artist.itemMap, - (state: AppState) => state.artist.items, - (itemMap, allArtists) => { - return artistIds.reduce((acc: Artist[], artistId) => { - const artist = allArtists[itemMap[artistId]]; - - if (artist) { - acc.push(artist); - } - - return acc; - }, []); - } - ); -} - -export default createMultiArtistsSelector; diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.js b/frontend/src/Store/Selectors/createProfileInUseSelector.js new file mode 100644 index 000000000..84fefb83e --- /dev/null +++ b/frontend/src/Store/Selectors/createProfileInUseSelector.js @@ -0,0 +1,24 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from './createAllArtistSelector'; + +function createProfileInUseSelector(profileProp) { + return createSelector( + (state, { id }) => id, + createAllArtistSelector(), + (state) => state.settings.importLists.items, + (id, artist, lists) => { + if (!id) { + return false; + } + + if (_.some(artist, { [profileProp]: id }) || _.some(lists, { [profileProp]: id })) { + return true; + } + + return false; + } + ); +} + +export default createProfileInUseSelector; diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.ts b/frontend/src/Store/Selectors/createProfileInUseSelector.ts deleted file mode 100644 index 85f0c3211..000000000 --- a/frontend/src/Store/Selectors/createProfileInUseSelector.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; -import ImportList from 'typings/ImportList'; -import createAllArtistSelector from './createAllArtistSelector'; - -function createProfileInUseSelector(profileProp: string) { - return createSelector( - (_: AppState, { id }: { id: number }) => id, - createAllArtistSelector(), - (state: AppState) => state.settings.importLists.items, - (id, artists, lists) => { - if (!id) { - return false; - } - - return ( - artists.some((a) => a[profileProp as keyof Artist] === id) || - lists.some((list) => list[profileProp as keyof ImportList] === id) - ); - } - ); -} - -export default createProfileInUseSelector; diff --git a/frontend/src/Store/Selectors/createQualityProfileSelector.js b/frontend/src/Store/Selectors/createQualityProfileSelector.js new file mode 100644 index 000000000..611dfc903 --- /dev/null +++ b/frontend/src/Store/Selectors/createQualityProfileSelector.js @@ -0,0 +1,26 @@ +import { createSelector } from 'reselect'; + +export function createQualityProfileSelectorForHook(qualityProfileId) { + return createSelector( + (state) => state.settings.qualityProfiles.items, + (qualityProfiles) => { + return qualityProfiles.find((profile) => { + return profile.id === qualityProfileId; + }); + } + ); +} + +function createQualityProfileSelector() { + return createSelector( + (state, { qualityProfileId }) => qualityProfileId, + (state) => state.settings.qualityProfiles.items, + (qualityProfileId, qualityProfiles) => { + return qualityProfiles.find((profile) => { + return profile.id === qualityProfileId; + }); + } + ); +} + +export default createQualityProfileSelector; diff --git a/frontend/src/Store/Selectors/createQualityProfileSelector.ts b/frontend/src/Store/Selectors/createQualityProfileSelector.ts deleted file mode 100644 index b913e0c46..000000000 --- a/frontend/src/Store/Selectors/createQualityProfileSelector.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -export function createQualityProfileSelectorForHook(qualityProfileId: number) { - return createSelector( - (state: AppState) => state.settings.qualityProfiles.items, - (qualityProfiles) => { - return qualityProfiles.find((profile) => profile.id === qualityProfileId); - } - ); -} - -function createQualityProfileSelector() { - return createSelector( - (_: AppState, { qualityProfileId }: { qualityProfileId: number }) => - qualityProfileId, - (state: AppState) => state.settings.qualityProfiles.items, - (qualityProfileId, qualityProfiles) => { - return qualityProfiles.find((profile) => profile.id === qualityProfileId); - } - ); -} - -export default createQualityProfileSelector; diff --git a/frontend/src/Store/Selectors/createQueueItemSelector.ts b/frontend/src/Store/Selectors/createQueueItemSelector.js similarity index 52% rename from frontend/src/Store/Selectors/createQueueItemSelector.ts rename to frontend/src/Store/Selectors/createQueueItemSelector.js index 54951a724..c85d7ed82 100644 --- a/frontend/src/Store/Selectors/createQueueItemSelector.ts +++ b/frontend/src/Store/Selectors/createQueueItemSelector.js @@ -1,16 +1,21 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createQueueItemSelector() { return createSelector( - (_: AppState, { albumId }: { albumId: number }) => albumId, - (state: AppState) => state.queue.details.items, + (state, { albumId }) => albumId, + (state) => state.queue.details.items, (albumId, details) => { if (!albumId || !details) { return null; } - return details.find((item) => item.albumId === albumId); + return details.find((item) => { + if (item.album) { + return item.album.id === albumId; + } + + return false; + }); } ); } diff --git a/frontend/src/Store/Selectors/createRootFoldersSelector.ts b/frontend/src/Store/Selectors/createRootFoldersSelector.ts index 432f9056d..a016d7665 100644 --- a/frontend/src/Store/Selectors/createRootFoldersSelector.ts +++ b/frontend/src/Store/Selectors/createRootFoldersSelector.ts @@ -1,15 +1,11 @@ import { createSelector } from 'reselect'; import { RootFolderAppState } from 'App/State/SettingsAppState'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import RootFolder from 'typings/RootFolder'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; export default function createRootFoldersSelector() { return createSelector( - createSortedSectionSelector( - 'settings.rootFolders', - sortByProp('name') - ), + createSortedSectionSelector('settings.rootFolders', sortByName), (rootFolders: RootFolderAppState) => rootFolders ); } diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.ts b/frontend/src/Store/Selectors/createSortedSectionSelector.js similarity index 68% rename from frontend/src/Store/Selectors/createSortedSectionSelector.ts rename to frontend/src/Store/Selectors/createSortedSectionSelector.js index abee01f75..331d890c9 100644 --- a/frontend/src/Store/Selectors/createSortedSectionSelector.ts +++ b/frontend/src/Store/Selectors/createSortedSectionSelector.js @@ -1,18 +1,14 @@ import { createSelector } from 'reselect'; import getSectionState from 'Utilities/State/getSectionState'; -function createSortedSectionSelector( - section: string, - comparer: (a: T, b: T) => number -) { +function createSortedSectionSelector(section, comparer) { return createSelector( (state) => state, (state) => { const sectionState = getSectionState(state, section, true); - return { ...sectionState, - items: [...sectionState.items].sort(comparer), + items: [...sectionState.items].sort(comparer) }; } ); diff --git a/frontend/src/Store/Selectors/createSystemStatusSelector.ts b/frontend/src/Store/Selectors/createSystemStatusSelector.js similarity index 70% rename from frontend/src/Store/Selectors/createSystemStatusSelector.ts rename to frontend/src/Store/Selectors/createSystemStatusSelector.js index f5e276069..df586bbb9 100644 --- a/frontend/src/Store/Selectors/createSystemStatusSelector.ts +++ b/frontend/src/Store/Selectors/createSystemStatusSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createSystemStatusSelector() { return createSelector( - (state: AppState) => state.system.status, + (state) => state.system.status, (status) => { return status.item; } diff --git a/frontend/src/Store/Selectors/createTagDetailsSelector.ts b/frontend/src/Store/Selectors/createTagDetailsSelector.js similarity index 62% rename from frontend/src/Store/Selectors/createTagDetailsSelector.ts rename to frontend/src/Store/Selectors/createTagDetailsSelector.js index 2a271cafe..dd178944c 100644 --- a/frontend/src/Store/Selectors/createTagDetailsSelector.ts +++ b/frontend/src/Store/Selectors/createTagDetailsSelector.js @@ -1,10 +1,9 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createTagDetailsSelector() { return createSelector( - (_: AppState, { id }: { id: number }) => id, - (state: AppState) => state.tags.details.items, + (state, { id }) => id, + (state) => state.tags.details.items, (id, tagDetails) => { return tagDetails.find((t) => t.id === id); } diff --git a/frontend/src/Store/Selectors/createTagsSelector.ts b/frontend/src/Store/Selectors/createTagsSelector.js similarity index 68% rename from frontend/src/Store/Selectors/createTagsSelector.ts rename to frontend/src/Store/Selectors/createTagsSelector.js index f653ff6e3..fbfd91cdb 100644 --- a/frontend/src/Store/Selectors/createTagsSelector.ts +++ b/frontend/src/Store/Selectors/createTagsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createTagsSelector() { return createSelector( - (state: AppState) => state.tags.items, + (state) => state.tags.items, (tags) => { return tags; } diff --git a/frontend/src/Store/Selectors/createTrackFileSelector.ts b/frontend/src/Store/Selectors/createTrackFileSelector.js similarity index 66% rename from frontend/src/Store/Selectors/createTrackFileSelector.ts rename to frontend/src/Store/Selectors/createTrackFileSelector.js index a162df1fa..bcfc5cb0b 100644 --- a/frontend/src/Store/Selectors/createTrackFileSelector.ts +++ b/frontend/src/Store/Selectors/createTrackFileSelector.js @@ -1,10 +1,9 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createTrackFileSelector() { return createSelector( - (_: AppState, { trackFileId }: { trackFileId: number }) => trackFileId, - (state: AppState) => state.trackFiles, + (state, { trackFileId }) => trackFileId, + (state) => state.trackFiles, (trackFileId, trackFiles) => { if (!trackFileId) { return; diff --git a/frontend/src/Store/Selectors/createUISettingsSelector.ts b/frontend/src/Store/Selectors/createUISettingsSelector.js similarity index 69% rename from frontend/src/Store/Selectors/createUISettingsSelector.ts rename to frontend/src/Store/Selectors/createUISettingsSelector.js index ff539679b..b256d0e98 100644 --- a/frontend/src/Store/Selectors/createUISettingsSelector.ts +++ b/frontend/src/Store/Selectors/createUISettingsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createUISettingsSelector() { return createSelector( - (state: AppState) => state.settings.ui, + (state) => state.settings.ui, (ui) => { return ui.item; } diff --git a/frontend/src/Store/scrollPositions.js b/frontend/src/Store/scrollPositions.js new file mode 100644 index 000000000..287a58593 --- /dev/null +++ b/frontend/src/Store/scrollPositions.js @@ -0,0 +1,5 @@ +const scrollPositions = { + artistIndex: 0 +}; + +export default scrollPositions; diff --git a/frontend/src/Store/scrollPositions.ts b/frontend/src/Store/scrollPositions.ts deleted file mode 100644 index 199bfa84c..000000000 --- a/frontend/src/Store/scrollPositions.ts +++ /dev/null @@ -1,5 +0,0 @@ -const scrollPositions: Record = { - artistIndex: 0, -}; - -export default scrollPositions; diff --git a/frontend/src/Store/thunks.js b/frontend/src/Store/thunks.js new file mode 100644 index 000000000..6daa843f4 --- /dev/null +++ b/frontend/src/Store/thunks.js @@ -0,0 +1,27 @@ +const thunks = {}; + +function identity(payload) { + return payload; +} + +export function createThunk(type, identityFunction = identity) { + return function(payload = {}) { + return function(dispatch, getState) { + const thunk = thunks[type]; + + if (thunk) { + return thunk(getState, identityFunction(payload), dispatch); + } + + throw Error(`Thunk handler has not been registered for ${type}`); + }; + }; +} + +export function handleThunks(handlers) { + const types = Object.keys(handlers); + + types.forEach((type) => { + thunks[type] = handlers[type]; + }); +} diff --git a/frontend/src/Store/thunks.ts b/frontend/src/Store/thunks.ts deleted file mode 100644 index fd277211e..000000000 --- a/frontend/src/Store/thunks.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Dispatch } from 'redux'; -import AppState from 'App/State/AppState'; - -type GetState = () => AppState; -type Thunk = ( - getState: GetState, - identityFn: never, - dispatch: Dispatch -) => unknown; - -const thunks: Record = {}; - -function identity(payload: T): TResult { - return payload as unknown as TResult; -} - -export function createThunk(type: string, identityFunction = identity) { - return function (payload?: T) { - return function (dispatch: Dispatch, getState: GetState) { - const thunk = thunks[type]; - - if (thunk) { - const finalPayload = payload ?? {}; - - return thunk(getState, identityFunction(finalPayload), dispatch); - } - - throw Error(`Thunk handler has not been registered for ${type}`); - }; - }; -} - -export function handleThunks(handlers: Record) { - const types = Object.keys(handlers); - - types.forEach((type) => { - thunks[type] = handlers[type]; - }); -} diff --git a/frontend/src/Styles/Themes/index.js b/frontend/src/Styles/Themes/index.js index 4dec39164..d93c5dd8c 100644 --- a/frontend/src/Styles/Themes/index.js +++ b/frontend/src/Styles/Themes/index.js @@ -2,7 +2,7 @@ import * as dark from './dark'; import * as light from './light'; const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -const auto = defaultDark ? dark : light; +const auto = defaultDark ? { ...dark } : { ...light }; export default { auto, diff --git a/frontend/src/Styles/Variables/fonts.js b/frontend/src/Styles/Variables/fonts.js index def48f28e..3b0077c5a 100644 --- a/frontend/src/Styles/Variables/fonts.js +++ b/frontend/src/Styles/Variables/fonts.js @@ -2,6 +2,7 @@ module.exports = { // Families defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif', monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;', + passwordFamily: 'text-security-disc', // Sizes extraSmallFontSize: '11px', diff --git a/frontend/src/System/Logs/Files/LogFiles.js b/frontend/src/System/Logs/Files/LogFiles.js index 5339a8590..83736c617 100644 --- a/frontend/src/System/Logs/Files/LogFiles.js +++ b/frontend/src/System/Logs/Files/LogFiles.js @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Alert from 'Components/Alert'; +import Link from 'Components/Link/Link'; 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 PageToolbar from 'Components/Page/Toolbar/PageToolbar'; @@ -77,16 +77,15 @@ class LogFiles extends Component {
- {translate('LogFilesLocation', { - location - })} + Log files are located in: {location}
- {currentLogView === 'Log Files' ? ( -
- -
- ) : null} + { + currentLogView === 'Log Files' && +
+ The log level defaults to 'Info' and can be changed in General Settings +
+ }
{ diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css index 6e38929c9..034804711 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css @@ -10,6 +10,15 @@ width: 100%; } +.commandName { + display: inline-block; + min-width: 220px; +} + +.userAgent { + color: #b0b0b0; +} + .queued, .started, .ended { diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts index 2c6010533..3bc00b738 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts @@ -2,12 +2,14 @@ // Please do not change this file! interface CssExports { 'actions': string; + 'commandName': string; 'duration': string; 'ended': string; 'queued': string; 'started': string; 'trigger': string; 'triggerContent': string; + 'userAgent': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js new file mode 100644 index 000000000..6f4da3828 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js @@ -0,0 +1,279 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import { icons, kinds } from 'Helpers/Props'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import styles from './QueuedTaskRow.css'; + +function getStatusIconProps(status, message) { + const title = titleCase(status); + + switch (status) { + case 'queued': + return { + name: icons.PENDING, + title + }; + + case 'started': + return { + name: icons.REFRESH, + isSpinning: true, + title + }; + + case 'completed': + return { + name: icons.CHECK, + kind: kinds.SUCCESS, + title: message === 'Completed' ? title : `${title}: ${message}` + }; + + case 'failed': + return { + name: icons.FATAL, + kind: kinds.DANGER, + title: `${title}: ${message}` + }; + + default: + return { + name: icons.UNKNOWN, + title + }; + } +} + +function getFormattedDates(props) { + const { + queued, + started, + ended, + showRelativeDates, + shortDateFormat + } = props; + + if (showRelativeDates) { + return { + queuedAt: moment(queued).fromNow(), + startedAt: started ? moment(started).fromNow() : '-', + endedAt: ended ? moment(ended).fromNow() : '-' + }; + } + + return { + queuedAt: formatDate(queued, shortDateFormat), + startedAt: started ? formatDate(started, shortDateFormat) : '-', + endedAt: ended ? formatDate(ended, shortDateFormat) : '-' + }; +} + +class QueuedTaskRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + ...getFormattedDates(props), + isCancelConfirmModalOpen: false + }; + + this._updateTimeoutId = null; + } + + componentDidMount() { + this.setUpdateTimer(); + } + + componentDidUpdate(prevProps) { + const { + queued, + started, + ended + } = this.props; + + if ( + queued !== prevProps.queued || + started !== prevProps.started || + ended !== prevProps.ended + ) { + this.setState(getFormattedDates(this.props)); + } + } + + componentWillUnmount() { + if (this._updateTimeoutId) { + this._updateTimeoutId = clearTimeout(this._updateTimeoutId); + } + } + + // + // Control + + setUpdateTimer() { + this._updateTimeoutId = setTimeout(() => { + this.setState(getFormattedDates(this.props)); + this.setUpdateTimer(); + }, 30000); + } + + // + // Listeners + + onCancelPress = () => { + this.setState({ + isCancelConfirmModalOpen: true + }); + }; + + onAbortCancel = () => { + this.setState({ + isCancelConfirmModalOpen: false + }); + }; + + // + // Render + + render() { + const { + trigger, + commandName, + queued, + started, + ended, + status, + duration, + message, + clientUserAgent, + longDateFormat, + timeFormat, + onCancelPress + } = this.props; + + const { + queuedAt, + startedAt, + endedAt, + isCancelConfirmModalOpen + } = this.state; + + let triggerIcon = icons.QUICK; + + if (trigger === 'manual') { + triggerIcon = icons.INTERACTIVE; + } else if (trigger === 'scheduled') { + triggerIcon = icons.SCHEDULED; + } + + return ( + + + + + + + + + + + + {commandName} + + { + clientUserAgent ? + + from: {clientUserAgent} + : + null + } + + + + {queuedAt} + + + + {startedAt} + + + + {endedAt} + + + + {formatTimeSpan(duration)} + + + + { + status === 'queued' && + + } + + + + + ); + } +} + +QueuedTaskRow.propTypes = { + trigger: PropTypes.string.isRequired, + commandName: PropTypes.string.isRequired, + queued: PropTypes.string.isRequired, + started: PropTypes.string, + ended: PropTypes.string, + status: PropTypes.string.isRequired, + duration: PropTypes.string, + message: PropTypes.string, + clientUserAgent: PropTypes.string, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onCancelPress: PropTypes.func.isRequired +}; + +export default QueuedTaskRow; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx deleted file mode 100644 index 4511bcbf4..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import moment from 'moment'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { CommandBody } from 'Commands/Command'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; -import { icons, kinds } from 'Helpers/Props'; -import { cancelCommand } from 'Store/Actions/commandActions'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import QueuedTaskRowNameCell from './QueuedTaskRowNameCell'; -import styles from './QueuedTaskRow.css'; - -function getStatusIconProps(status: string, message: string | undefined) { - const title = titleCase(status); - - switch (status) { - case 'queued': - return { - name: icons.PENDING, - title, - }; - - case 'started': - return { - name: icons.REFRESH, - isSpinning: true, - title, - }; - - case 'completed': - return { - name: icons.CHECK, - kind: kinds.SUCCESS, - title: message === 'Completed' ? title : `${title}: ${message}`, - }; - - case 'failed': - return { - name: icons.FATAL, - kind: kinds.DANGER, - title: `${title}: ${message}`, - }; - - default: - return { - name: icons.UNKNOWN, - title, - }; - } -} - -function getFormattedDates( - queued: string, - started: string | undefined, - ended: string | undefined, - showRelativeDates: boolean, - shortDateFormat: string -) { - if (showRelativeDates) { - return { - queuedAt: moment(queued).fromNow(), - startedAt: started ? moment(started).fromNow() : '-', - endedAt: ended ? moment(ended).fromNow() : '-', - }; - } - - return { - queuedAt: formatDate(queued, shortDateFormat), - startedAt: started ? formatDate(started, shortDateFormat) : '-', - endedAt: ended ? formatDate(ended, shortDateFormat) : '-', - }; -} - -interface QueuedTimes { - queuedAt: string; - startedAt: string; - endedAt: string; -} - -export interface QueuedTaskRowProps { - id: number; - trigger: string; - commandName: string; - queued: string; - started?: string; - ended?: string; - status: string; - duration?: string; - message?: string; - body: CommandBody; - clientUserAgent?: string; -} - -export default function QueuedTaskRow(props: QueuedTaskRowProps) { - const { - id, - trigger, - commandName, - queued, - started, - ended, - status, - duration, - message, - body, - clientUserAgent, - } = props; - - const dispatch = useDispatch(); - const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } = - useSelector(createUISettingsSelector()); - - const updateTimeTimeoutId = useRef | null>( - null - ); - const [times, setTimes] = useState( - getFormattedDates( - queued, - started, - ended, - showRelativeDates, - shortDateFormat - ) - ); - - const [ - isCancelConfirmModalOpen, - openCancelConfirmModal, - closeCancelConfirmModal, - ] = useModalOpenState(false); - - const handleCancelPress = useCallback(() => { - dispatch(cancelCommand({ id })); - }, [id, dispatch]); - - useEffect(() => { - updateTimeTimeoutId.current = setTimeout(() => { - setTimes( - getFormattedDates( - queued, - started, - ended, - showRelativeDates, - shortDateFormat - ) - ); - }, 30000); - - return () => { - if (updateTimeTimeoutId.current) { - clearTimeout(updateTimeTimeoutId.current); - } - }; - }, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]); - - const { queuedAt, startedAt, endedAt } = times; - - let triggerIcon = icons.QUICK; - - if (trigger === 'manual') { - triggerIcon = icons.INTERACTIVE; - } else if (trigger === 'scheduled') { - triggerIcon = icons.SCHEDULED; - } - - return ( - - - - - - - - - - - - - {queuedAt} - - - - {startedAt} - - - - {endedAt} - - - - {formatTimeSpan(duration)} - - - - {status === 'queued' && ( - - )} - - - - - ); -} diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js new file mode 100644 index 000000000..f55ab985a --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { cancelCommand } from 'Store/Actions/commandActions'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import QueuedTaskRow from './QueuedTaskRow'; + +function createMapStateToProps() { + return createSelector( + createUISettingsSelector(), + (uiSettings) => { + return { + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onCancelPress() { + dispatch(cancelCommand({ + id: props.id + })); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow); diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css deleted file mode 100644 index 41acb33f8..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css +++ /dev/null @@ -1,8 +0,0 @@ -.commandName { - display: inline-block; - min-width: 220px; -} - -.userAgent { - color: #b0b0b0; -} diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts deleted file mode 100644 index fc9081492..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'commandName': string; - 'userAgent': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx deleted file mode 100644 index 41a307d5f..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { CommandBody } from 'Commands/Command'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import createMultiArtistsSelector from 'Store/Selectors/createMultiArtistsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; -import styles from './QueuedTaskRowNameCell.css'; - -function formatTitles(titles: string[]) { - if (!titles) { - return null; - } - - if (titles.length > 11) { - return ( - - {titles.slice(0, 10).join(', ')}, {titles.length - 10} more - - ); - } - - return {titles.join(', ')}; -} - -export interface QueuedTaskRowNameCellProps { - commandName: string; - body: CommandBody; - clientUserAgent?: string; -} - -export default function QueuedTaskRowNameCell( - props: QueuedTaskRowNameCellProps -) { - const { commandName, body, clientUserAgent } = props; - const movieIds = [...(body.artistIds ?? [])]; - - if (body.artistId) { - movieIds.push(body.artistId); - } - - const artists = useSelector(createMultiArtistsSelector(movieIds)); - const sortedArtists = artists.sort(sortByProp('sortName')); - - return ( - - - {commandName} - {sortedArtists.length ? ( - - {formatTitles(sortedArtists.map((a) => a.artistName))} - ) : null} - - - {clientUserAgent ? ( - - {translate('From')}: {clientUserAgent} - - ) : null} - - ); -} diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.js b/frontend/src/System/Tasks/Queued/QueuedTasks.js new file mode 100644 index 000000000..dac38f1d4 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTasks.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import translate from 'Utilities/String/translate'; +import QueuedTaskRowConnector from './QueuedTaskRowConnector'; + +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 + } +]; + +function QueuedTasks(props) { + const { + isFetching, + isPopulated, + items + } = props; + + return ( +
+ { + isFetching && !isPopulated && + + } + + { + isPopulated && +
+ + { + 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/firstCharToUpper.js b/frontend/src/Utilities/String/firstCharToUpper.js deleted file mode 100644 index 1ce64831c..000000000 --- a/frontend/src/Utilities/String/firstCharToUpper.js +++ /dev/null @@ -1,9 +0,0 @@ -function firstCharToUpper(input) { - if (!input) { - return ''; - } - - return [].map.call(input, (char, i) => (i ? char : char.toUpperCase())).join(''); -} - -export default firstCharToUpper; 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/Utilities/Table/getSelectedIds.js b/frontend/src/Utilities/Table/getSelectedIds.js new file mode 100644 index 000000000..705f13a5d --- /dev/null +++ b/frontend/src/Utilities/Table/getSelectedIds.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; + +function getSelectedIds(selectedState, { parseIds = true } = {}) { + return _.reduce(selectedState, (result, value, id) => { + if (value) { + const parsedId = parseIds ? parseInt(id) : id; + + result.push(parsedId); + } + + return result; + }, []); +} + +export default getSelectedIds; diff --git a/frontend/src/Utilities/Table/getSelectedIds.ts b/frontend/src/Utilities/Table/getSelectedIds.ts deleted file mode 100644 index b84db6245..000000000 --- a/frontend/src/Utilities/Table/getSelectedIds.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { reduce } from 'lodash'; -import { SelectedState } from 'Helpers/Hooks/useSelectState'; - -function getSelectedIds(selectedState: SelectedState): number[] { - return reduce( - selectedState, - (result: number[], value, id) => { - if (value) { - result.push(parseInt(id)); - } - - return result; - }, - [] - ); -} - -export default getSelectedIds; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js index 6710118b1..7e3b971ad 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 { - - - -
- {translate('SearchForAllCutoffUnmetAlbumsConfirmationCount', { totalRecords })} + {translate('MassAlbumsCutoffUnmetWarning', [totalRecords])}
- {translate('MassSearchCancelWarning')} + {translate('ThisCannotBeCancelled')}
} diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js index 1dd9870d1..dbb4f2235 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js @@ -131,15 +131,13 @@ class CutoffUnmetConnector extends Component { onSearchSelectedPress = (selected) => { 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 ( - - - -
- {translate('SearchForAllMissingAlbumsConfirmationCount', { totalRecords })} + {translate('MassAlbumsSearchWarning', [totalRecords])}
- {translate('MassSearchCancelWarning')} + {translate('ThisCannotBeCancelled')}
} diff --git a/frontend/src/Wanted/Missing/MissingConnector.js b/frontend/src/Wanted/Missing/MissingConnector.js index 008f1a149..3d0abfa03 100644 --- a/frontend/src/Wanted/Missing/MissingConnector.js +++ b/frontend/src/Wanted/Missing/MissingConnector.js @@ -121,15 +121,13 @@ class MissingConnector extends Component { onSearchSelectedPress = (selected) => { 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 @@