mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-10 15:23:40 -07:00
Compare commits
No commits in common. "develop" and "v2.5.0.4277" have entirely different histories.
develop
...
v2.5.0.427
444 changed files with 5804 additions and 11056 deletions
|
@ -6,7 +6,7 @@
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"nodeGypDependencies": true,
|
"nodeGypDependencies": true,
|
||||||
"version": "20",
|
"version": "16",
|
||||||
"nvmVersion": "latest"
|
"nvmVersion": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -60,7 +60,6 @@ body:
|
||||||
- Master
|
- Master
|
||||||
- Develop
|
- Develop
|
||||||
- Nightly
|
- Nightly
|
||||||
- Plugins (experimental)
|
|
||||||
- Other (This issue will be closed)
|
- Other (This issue will be closed)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
2
.github/workflows/label-actions.yml
vendored
2
.github/workflows/label-actions.yml
vendored
|
@ -12,6 +12,6 @@ jobs:
|
||||||
action:
|
action:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/label-actions@v4
|
- uses: dessant/label-actions@v3
|
||||||
with:
|
with:
|
||||||
process-only: 'issues'
|
process-only: 'issues'
|
||||||
|
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v5
|
- uses: dessant/lock-threads@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: '90'
|
issue-inactive-days: '90'
|
||||||
|
|
35
.gitignore
vendored
35
.gitignore
vendored
|
@ -121,7 +121,6 @@ _artifacts
|
||||||
_rawPackage/
|
_rawPackage/
|
||||||
_dotTrace*
|
_dotTrace*
|
||||||
_tests/
|
_tests/
|
||||||
_temp*
|
|
||||||
*.Result.xml
|
*.Result.xml
|
||||||
coverage*.xml
|
coverage*.xml
|
||||||
coverage*.json
|
coverage*.json
|
||||||
|
@ -140,6 +139,12 @@ project.fragment.lock.json
|
||||||
artifacts/
|
artifacts/
|
||||||
**/Properties/launchSettings.json
|
**/Properties/launchSettings.json
|
||||||
|
|
||||||
|
#VS outout folders
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
output/*
|
||||||
|
|
||||||
|
|
||||||
# macOS metadata files
|
# macOS metadata files
|
||||||
._*
|
._*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -158,12 +163,34 @@ Thumbs.db
|
||||||
/tools/Addins/*
|
/tools/Addins/*
|
||||||
packages.config.md5sum
|
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
|
# ignore node_modules symlink
|
||||||
node_modules
|
node_modules
|
||||||
node_modules.nosync
|
node_modules.nosync
|
||||||
|
|
||||||
# API doc generation
|
# API doc generation
|
||||||
.config/
|
.config/
|
||||||
|
|
||||||
# Ignore Jetbrains IntelliJ Workspace Directories
|
|
||||||
.idea/
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# Lidarr
|
# Lidarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
|
||||||
[](https://wiki.servarr.com/lidarr/installation#docker)
|
[](https://wiki.servarr.com/lidarr/installation#docker)
|
||||||

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
|
|
|
@ -9,18 +9,18 @@ variables:
|
||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '2.13.1'
|
majorVersion: '2.5.0'
|
||||||
minorVersion: $[counter('minorVersion', 1076)]
|
minorVersion: $[counter('minorVersion', 1076)]
|
||||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.427'
|
dotnetVersion: '6.0.424'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-22.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-13'
|
macImage: 'macOS-12'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
|
@ -1120,19 +1120,19 @@ stages:
|
||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@3
|
- task: SonarCloudPrepare@2
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'lidarr'
|
organization: 'lidarr'
|
||||||
scannerMode: 'cli'
|
scannerMode: 'CLI'
|
||||||
configMode: 'manual'
|
configMode: 'manual'
|
||||||
cliProjectKey: 'lidarr_Lidarr.UI'
|
cliProjectKey: 'lidarr_Lidarr.UI'
|
||||||
cliProjectName: 'LidarrUI'
|
cliProjectName: 'LidarrUI'
|
||||||
cliProjectVersion: '$(lidarrVersion)'
|
cliProjectVersion: '$(lidarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@3
|
- task: SonarCloudAnalyze@2
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
|
@ -1208,12 +1208,12 @@ stages:
|
||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@3
|
- task: SonarCloudPrepare@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'lidarr'
|
organization: 'lidarr'
|
||||||
scannerMode: 'dotnet'
|
scannerMode: 'MSBuild'
|
||||||
projectKey: 'lidarr_Lidarr'
|
projectKey: 'lidarr_Lidarr'
|
||||||
projectName: 'Lidarr'
|
projectName: 'Lidarr'
|
||||||
projectVersion: '$(lidarrVersion)'
|
projectVersion: '$(lidarrVersion)'
|
||||||
|
@ -1226,10 +1226,10 @@ stages:
|
||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@3
|
- task: SonarCloudAnalyze@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5.3.11
|
- task: reportgenerator@5
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
|
|
|
@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ')
|
||||||
app_guid=${app_guid:-media}
|
app_guid=${app_guid:-media}
|
||||||
|
|
||||||
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
||||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||||
|
|
||||||
# Create User / Group as needed
|
# Create User / Group as needed
|
||||||
|
@ -114,7 +114,7 @@ case "$ARCH" in
|
||||||
esac
|
esac
|
||||||
echo ""
|
echo ""
|
||||||
echo "Removing previous tarballs"
|
echo "Removing previous tarballs"
|
||||||
# -f to Force so we fail if it doesn't exist
|
# -f to Force so we fail if it doesnt exist
|
||||||
rm -f "${app^}".*.tar.gz
|
rm -f "${app^}".*.tar.gz
|
||||||
echo ""
|
echo ""
|
||||||
echo "Downloading..."
|
echo "Downloading..."
|
||||||
|
|
15
docs.sh
15
docs.sh
|
@ -1,18 +1,13 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
FRAMEWORK="net6.0"
|
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
ARCHITECTURE="${2:-x64}"
|
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
RUNTIME="win-$ARCHITECTURE"
|
RUNTIME="win-x64"
|
||||||
elif [ "$PLATFORM" = "Linux" ]; then
|
elif [ "$PLATFORM" = "Linux" ]; then
|
||||||
RUNTIME="linux-$ARCHITECTURE"
|
RUNTIME="linux-x64"
|
||||||
elif [ "$PLATFORM" = "Mac" ]; then
|
elif [ "$PLATFORM" = "Mac" ]; then
|
||||||
RUNTIME="osx-$ARCHITECTURE"
|
RUNTIME="osx-x64"
|
||||||
else
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -40,7 +35,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 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/$application" v1 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "explicit"
|
"source.fixAll": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
|
|
@ -26,7 +26,6 @@ module.exports = (env) => {
|
||||||
const config = {
|
const config = {
|
||||||
mode: isProduction ? 'production' : 'development',
|
mode: isProduction ? 'production' : 'development',
|
||||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||||
target: 'web',
|
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
children: false
|
children: false
|
||||||
|
@ -68,7 +67,7 @@ module.exports = (env) => {
|
||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
filename: '[name]-[contenthash].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -93,7 +92,7 @@ module.exports = (env) => {
|
||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'Content/styles.css',
|
filename: 'Content/styles.css',
|
||||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
|
@ -135,12 +134,6 @@ module.exports = (env) => {
|
||||||
{
|
{
|
||||||
source: 'frontend/src/Content/robots.txt',
|
source: 'frontend/src/Content/robots.txt',
|
||||||
destination: path.join(distFolder, '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,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: '3.41'
|
corejs: 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -209,7 +202,7 @@ module.exports = (env) => {
|
||||||
options: {
|
options: {
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
modules: {
|
modules: {
|
||||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,6 @@ const mixinsFiles = [
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
'autoprefixer',
|
|
||||||
['postcss-mixins', {
|
['postcss-mixins', {
|
||||||
mixinsFiles
|
mixinsFiles
|
||||||
}],
|
}],
|
||||||
|
|
|
@ -172,8 +172,7 @@ function HistoryDetails(props) {
|
||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
if (eventType === 'downloadFailed') {
|
||||||
const {
|
const {
|
||||||
message,
|
message
|
||||||
indexer
|
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -193,14 +192,6 @@ function HistoryDetails(props) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
indexer ? (
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
message ?
|
message ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
|
|
@ -26,5 +26,4 @@
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 90px;
|
width: 90px;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,40 +57,30 @@ function QueueStatusCell(props) {
|
||||||
|
|
||||||
if (status === 'paused') {
|
if (status === 'paused') {
|
||||||
iconName = icons.PAUSED;
|
iconName = icons.PAUSED;
|
||||||
title = translate('Paused');
|
title = 'Paused';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'queued') {
|
if (status === 'queued') {
|
||||||
iconName = icons.QUEUED;
|
iconName = icons.QUEUED;
|
||||||
title = translate('Queued');
|
title = 'Queued';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOADED;
|
iconName = icons.DOWNLOADED;
|
||||||
title = translate('Downloaded');
|
title = 'Downloaded';
|
||||||
|
|
||||||
if (trackedDownloadState === 'importBlocked') {
|
|
||||||
title += ` - ${translate('UnableToImportAutomatically')}`;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importFailed') {
|
|
||||||
title += ` - ${translate('ImportFailed', { sourceTitle })}`;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importPending') {
|
if (trackedDownloadState === 'importPending') {
|
||||||
title += ` - ${translate('WaitingToImport')}`;
|
title += ' - Waiting to Import';
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'importing') {
|
if (trackedDownloadState === 'importing') {
|
||||||
title += ` - ${translate('Importing')}`;
|
title += ' - Importing';
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'failedPending') {
|
if (trackedDownloadState === 'failedPending') {
|
||||||
title += ` - ${translate('WaitingToProcess')}`;
|
title += ' - Waiting to Process';
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,38 +91,36 @@ function QueueStatusCell(props) {
|
||||||
|
|
||||||
if (status === 'delay') {
|
if (status === 'delay') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
title = translate('Pending');
|
title = 'Pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'downloadClientUnavailable') {
|
if (status === 'downloadClientUnavailable') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
title = translate('PendingDownloadClientUnavailable');
|
title = 'Pending - Download client is unavailable';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'failed') {
|
if (status === 'failed') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('DownloadFailed');
|
title = 'Download failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
const warningMessage =
|
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||||
errorMessage || translate('CheckDownloadClientForDetails');
|
|
||||||
title = translate('DownloadWarning', { warningMessage });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOAD;
|
iconName = icons.DOWNLOAD;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('ImportFailed', { sourceTitle });
|
title = `Import failed: ${sourceTitle}`;
|
||||||
} else {
|
} else {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('DownloadFailed');
|
title = 'Download failed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ interface Album extends ModelBase {
|
||||||
monitored: boolean;
|
monitored: boolean;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
lastSearchTime?: string;
|
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,8 +121,6 @@
|
||||||
|
|
||||||
.releaseDate,
|
.releaseDate,
|
||||||
.sizeOnDisk,
|
.sizeOnDisk,
|
||||||
.albumType,
|
|
||||||
.secondaryTypes,
|
|
||||||
.qualityProfileName,
|
.qualityProfileName,
|
||||||
.links,
|
.links,
|
||||||
.tags {
|
.tags {
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'albumNavigationButton': string;
|
'albumNavigationButton': string;
|
||||||
'albumNavigationButtons': string;
|
'albumNavigationButtons': string;
|
||||||
'albumType': string;
|
|
||||||
'alternateTitlesIconContainer': string;
|
'alternateTitlesIconContainer': string;
|
||||||
'backdrop': string;
|
'backdrop': string;
|
||||||
'backdropOverlay': string;
|
'backdropOverlay': string;
|
||||||
|
@ -21,7 +20,6 @@ interface CssExports {
|
||||||
'overview': string;
|
'overview': string;
|
||||||
'qualityProfileName': string;
|
'qualityProfileName': string;
|
||||||
'releaseDate': string;
|
'releaseDate': string;
|
||||||
'secondaryTypes': string;
|
|
||||||
'sizeOnDisk': string;
|
'sizeOnDisk': string;
|
||||||
'tags': string;
|
'tags': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
|
|
|
@ -192,7 +192,6 @@ class AlbumDetails extends Component {
|
||||||
duration,
|
duration,
|
||||||
overview,
|
overview,
|
||||||
albumType,
|
albumType,
|
||||||
secondaryTypes,
|
|
||||||
statistics = {},
|
statistics = {},
|
||||||
monitored,
|
monitored,
|
||||||
releaseDate,
|
releaseDate,
|
||||||
|
@ -205,7 +204,6 @@ class AlbumDetails extends Component {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
tracksError,
|
|
||||||
trackFilesError,
|
trackFilesError,
|
||||||
hasTrackFiles,
|
hasTrackFiles,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
|
@ -398,11 +396,10 @@ class AlbumDetails extends Component {
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
duration ?
|
!!duration &&
|
||||||
<span className={styles.duration}>
|
<span className={styles.duration}>
|
||||||
{formatDuration(duration)}
|
{formatDuration(duration)}
|
||||||
</span> :
|
</span>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<HeartRating
|
<HeartRating
|
||||||
|
@ -421,15 +418,14 @@ class AlbumDetails extends Component {
|
||||||
title={translate('ReleaseDate')}
|
title={translate('ReleaseDate')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.CALENDAR}
|
name={icons.CALENDAR}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.releaseDate}>
|
<span className={styles.releaseDate}>
|
||||||
{moment(releaseDate).format(shortDateFormat)}
|
{moment(releaseDate).format(shortDateFormat)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -438,15 +434,16 @@ class AlbumDetails extends Component {
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.DRIVE}
|
name={icons.DRIVE}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.sizeOnDisk}>
|
<span className={styles.sizeOnDisk}>
|
||||||
{formatBytes(sizeOnDisk)}
|
{
|
||||||
|
formatBytes(sizeOnDisk || 0)
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
|
@ -462,55 +459,32 @@ class AlbumDetails extends Component {
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{
|
{
|
||||||
albumType ?
|
!!albumType &&
|
||||||
<Label
|
<Label
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
title={translate('Type')}
|
title={translate('Type')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.INFO}
|
name={icons.INFO}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
<span className={styles.albumType}>
|
|
||||||
|
<span className={styles.qualityProfileName}>
|
||||||
{albumType}
|
{albumType}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Label>
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
secondaryTypes.length ?
|
|
||||||
<Label
|
|
||||||
className={styles.detailsLabel}
|
|
||||||
title={translate('SecondaryTypes')}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Icon
|
|
||||||
name={icons.INFO}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
<span className={styles.secondaryTypes}>
|
|
||||||
{secondaryTypes.join(', ')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -519,15 +493,14 @@ class AlbumDetails extends Component {
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.EXTERNAL_LINK}
|
name={icons.EXTERNAL_LINK}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.links}>
|
<span className={styles.links}>
|
||||||
{translate('Links')}
|
{translate('Links')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
|
@ -553,9 +526,8 @@ class AlbumDetails extends Component {
|
||||||
|
|
||||||
<div className={styles.contentContainer}>
|
<div className={styles.contentContainer}>
|
||||||
{
|
{
|
||||||
!isPopulated && !albumsError && !tracksError && !trackFilesError ?
|
!isPopulated && !albumsError && !trackFilesError &&
|
||||||
<LoadingIndicator /> :
|
<LoadingIndicator />
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -566,14 +538,6 @@ class AlbumDetails extends Component {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && tracksError ?
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('TracksLoadError')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && trackFilesError ?
|
!isFetching && trackFilesError ?
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
|
@ -602,14 +566,6 @@ class AlbumDetails extends Component {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !media.length ?
|
|
||||||
<Alert kind={kinds.WARNING}>
|
|
||||||
{translate('NoMediumInformation')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrganizePreviewModalConnector
|
<OrganizePreviewModalConnector
|
||||||
|
@ -676,7 +632,6 @@ AlbumDetails.propTypes = {
|
||||||
duration: PropTypes.number,
|
duration: PropTypes.number,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
albumType: PropTypes.string.isRequired,
|
albumType: PropTypes.string.isRequired,
|
||||||
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
statistics: PropTypes.object.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
releaseDate: PropTypes.string.isRequired,
|
releaseDate: PropTypes.string.isRequired,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
|
@ -703,8 +658,6 @@ AlbumDetails.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
AlbumDetails.defaultProps = {
|
AlbumDetails.defaultProps = {
|
||||||
secondaryTypes: [],
|
|
||||||
statistics: {},
|
|
||||||
isSaving: false
|
isSaving: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
import Logs from 'System/Logs/Logs';
|
import Logs from 'System/Logs/Logs';
|
||||||
import Status from 'System/Status/Status';
|
import Status from 'System/Status/Status';
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import Updates from 'System/Updates/Updates';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
|
@ -248,7 +248,7 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/system/updates"
|
path="/system/updates"
|
||||||
component={Updates}
|
component={UpdatesConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -44,7 +44,6 @@ export interface CustomFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
version: string;
|
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
|
|
|
@ -4,7 +4,6 @@ import AppSectionState, {
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
|
@ -13,16 +12,13 @@ import MetadataProfile from 'typings/MetadataProfile';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import RootFolder from 'typings/RootFolder';
|
import RootFolder from 'typings/RootFolder';
|
||||||
import General from 'typings/Settings/General';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export type GeneralAppState = AppSectionItemState<General>;
|
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
@ -45,11 +41,6 @@ export interface MetadataProfilesAppState
|
||||||
extends AppSectionState<MetadataProfile>,
|
extends AppSectionState<MetadataProfile>,
|
||||||
AppSectionSchemaState<MetadataProfile> {}
|
AppSectionSchemaState<MetadataProfile> {}
|
||||||
|
|
||||||
export interface CustomFormatAppState
|
|
||||||
extends AppSectionState<CustomFormat>,
|
|
||||||
AppSectionDeleteState,
|
|
||||||
AppSectionSaveState {}
|
|
||||||
|
|
||||||
export interface RootFolderAppState
|
export interface RootFolderAppState
|
||||||
extends AppSectionState<RootFolder>,
|
extends AppSectionState<RootFolder>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
@ -59,10 +50,7 @@ export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
advancedSettings: boolean;
|
|
||||||
customFormats: CustomFormatAppState;
|
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
general: GeneralAppState;
|
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import Update from 'typings/Update';
|
import { AppSectionItemState } from './AppSectionState';
|
||||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
|
||||||
|
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
export type UpdateAppState = AppSectionState<Update>;
|
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
updates: UpdateAppState;
|
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,7 +149,9 @@ class AlbumRow extends Component {
|
||||||
if (name === 'secondaryTypes') {
|
if (name === 'secondaryTypes') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
{secondaryTypes.join(', ')}
|
{
|
||||||
|
secondaryTypes
|
||||||
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
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 translate from 'Utilities/String/translate';
|
||||||
import styles from './EditArtistModalContent.css';
|
import styles from './EditArtistModalContent.css';
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ class EditArtistModalContent extends Component {
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form {...otherProps}>
|
<Form {...otherProps}>
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Monitored')}
|
{translate('Monitored')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -107,10 +107,9 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('MonitorNewItems')}
|
{translate('MonitorNewItems')}
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -133,7 +132,7 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('QualityProfile')}
|
{translate('QualityProfile')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -147,10 +146,10 @@ class EditArtistModalContent extends Component {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
showMetadataProfile ?
|
showMetadataProfile &&
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('MetadataProfile')}
|
Metadata Profile
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
|
@ -174,11 +173,10 @@ class EditArtistModalContent extends Component {
|
||||||
{...metadataProfileId}
|
{...metadataProfileId}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup> :
|
</FormGroup>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Path')}
|
{translate('Path')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -191,7 +189,7 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Tags')}
|
{translate('Tags')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -211,7 +209,7 @@ class EditArtistModalContent extends Component {
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={onDeleteArtistPress}
|
onPress={onDeleteArtistPress}
|
||||||
>
|
>
|
||||||
{translate('Delete')}
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -79,7 +79,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('LastAlbum')}
|
{translate('Last Album')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
|
|
@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
value={tags}
|
value={tags}
|
||||||
helpText={translate('ICalTagsArtistHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -3,8 +3,8 @@ import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import PathInput from 'Components/Form/PathInput';
|
import PathInput from 'Components/Form/PathInput';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component {
|
||||||
className={styles.mappedDrivesWarning}
|
className={styles.mappedDrivesWarning}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/lidarr/faq#why-cant-lidarr-see-my-files-on-a-remote-server' })} />
|
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/lidarr/faq">FAQ</Link> for more information.
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,11 @@ import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||||
import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue';
|
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
|
||||||
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
|
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
|
||||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||||
import styles from './FilterBuilderRow.css';
|
import styles from './FilterBuilderRow.css';
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
return IndexerFilterBuilderRowValueConnector;
|
return IndexerFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.METADATA_PROFILE:
|
case filterBuilderValueTypes.METADATA_PROFILE:
|
||||||
return MetadataProfileFilterBuilderRowValue;
|
return MetadataProfileFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.MONITOR_NEW_ITEMS:
|
case filterBuilderValueTypes.MONITOR_NEW_ITEMS:
|
||||||
return MonitorNewItemsFilterBuilderRowValue;
|
return MonitorNewItemsFilterBuilderRowValue;
|
||||||
|
@ -80,7 +80,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
return QualityFilterBuilderRowValueConnector;
|
return QualityFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||||
return QualityProfileFilterBuilderRowValue;
|
return QualityProfileFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.ARTIST:
|
case filterBuilderValueTypes.ARTIST:
|
||||||
return ArtistFilterBuilderRowValue;
|
return ArtistFilterBuilderRowValue;
|
||||||
|
|
|
@ -25,7 +25,7 @@ const EVENT_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
get name() {
|
get name() {
|
||||||
return translate('ImportCompleteFailed');
|
return translate('ImportFailed');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 <FilterBuilderRowValue {...props} tagList={tagList} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MetadataProfileFilterBuilderRowValue;
|
|
|
@ -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);
|
|
@ -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 <FilterBuilderRowValue {...props} tagList={tagList} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QualityProfileFilterBuilderRowValue;
|
|
|
@ -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);
|
|
@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './EnhancedSelectInput.css';
|
import styles from './EnhancedSelectInput.css';
|
||||||
|
|
||||||
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
function isArrowKey(keyCode) {
|
||||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||||
}
|
}
|
||||||
|
@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component {
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
onComputeMaxHeight = (data) => {
|
||||||
|
const {
|
||||||
|
top,
|
||||||
|
bottom
|
||||||
|
} = data.offsets.reference;
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
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;
|
return data;
|
||||||
};
|
};
|
||||||
|
@ -450,10 +457,6 @@ class EnhancedSelectInput extends Component {
|
||||||
order: 851,
|
order: 851,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fn: this.onComputeMaxHeight
|
fn: this.onComputeMaxHeight
|
||||||
},
|
|
||||||
preventOverflow: {
|
|
||||||
enabled: true,
|
|
||||||
boundariesElement: 'viewport'
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -49,12 +49,12 @@ function getComponent(type) {
|
||||||
case inputTypes.DEVICE:
|
case inputTypes.DEVICE:
|
||||||
return DeviceInputConnector;
|
return DeviceInputConnector;
|
||||||
|
|
||||||
case inputTypes.KEY_VALUE_LIST:
|
|
||||||
return KeyValueListInput;
|
|
||||||
|
|
||||||
case inputTypes.PLAYLIST:
|
case inputTypes.PLAYLIST:
|
||||||
return PlaylistInputConnector;
|
return PlaylistInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.KEY_VALUE_LIST:
|
||||||
|
return KeyValueListInput;
|
||||||
|
|
||||||
case inputTypes.MONITOR_ALBUMS_SELECT:
|
case inputTypes.MONITOR_ALBUMS_SELECT:
|
||||||
return MonitorAlbumsSelectInput;
|
return MonitorAlbumsSelectInput;
|
||||||
|
|
||||||
|
|
156
frontend/src/Components/Form/KeyValueListInput.js
Normal file
156
frontend/src/Components/Form/KeyValueListInput.js
Normal file
|
@ -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 (
|
||||||
|
<div className={classNames(
|
||||||
|
className,
|
||||||
|
isFocused && styles.isFocused,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
[...value, { key: '', value: '' }].map((v, index) => {
|
||||||
|
return (
|
||||||
|
<KeyValueListInputItem
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
keyValue={v.key}
|
||||||
|
value={v.value}
|
||||||
|
keyPlaceholder={keyPlaceholder}
|
||||||
|
valuePlaceholder={valuePlaceholder}
|
||||||
|
isNew={index === value.length}
|
||||||
|
onChange={this.onItemChange}
|
||||||
|
onRemove={this.onRemoveItem}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
|
@ -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<KeyValue[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
isFocused && styles.isFocused,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{[...value, { key: '', value: '' }].map((v, index) => (
|
|
||||||
<KeyValueListInputItem
|
|
||||||
key={index}
|
|
||||||
index={index}
|
|
||||||
keyValue={v.key}
|
|
||||||
value={v.value}
|
|
||||||
keyPlaceholder={keyPlaceholder}
|
|
||||||
valuePlaceholder={valuePlaceholder}
|
|
||||||
isNew={index === value.length}
|
|
||||||
onChange={handleItemChange}
|
|
||||||
onRemove={handleRemoveItem}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KeyValueListInput;
|
|
|
@ -5,19 +5,13 @@
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
border-bottom: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyInputWrapper {
|
.inputWrapper {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.valueInputWrapper {
|
|
||||||
flex: 1 0 0;
|
|
||||||
min-width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonWrapper {
|
.buttonWrapper {
|
||||||
flex: 0 0 22px;
|
flex: 0 0 22px;
|
||||||
}
|
}
|
||||||
|
@ -26,10 +20,6 @@
|
||||||
.valueInput {
|
.valueInput {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: var(--inputBackgroundColor);
|
||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--helpTextColor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'buttonWrapper': string;
|
'buttonWrapper': string;
|
||||||
|
'inputWrapper': string;
|
||||||
'itemContainer': string;
|
'itemContainer': string;
|
||||||
'keyInput': string;
|
'keyInput': string;
|
||||||
'keyInputWrapper': string;
|
|
||||||
'valueInput': string;
|
'valueInput': string;
|
||||||
'valueInputWrapper': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
124
frontend/src/Components/Form/KeyValueListInputItem.js
Normal file
124
frontend/src/Components/Form/KeyValueListInputItem.js
Normal file
|
@ -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 (
|
||||||
|
<div className={styles.itemContainer}>
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={styles.keyInput}
|
||||||
|
name="key"
|
||||||
|
value={keyValue}
|
||||||
|
placeholder={keyPlaceholder}
|
||||||
|
onChange={this.onKeyChange}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={styles.valueInput}
|
||||||
|
name="value"
|
||||||
|
value={value}
|
||||||
|
placeholder={valuePlaceholder}
|
||||||
|
onChange={this.onValueChange}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonWrapper}>
|
||||||
|
{
|
||||||
|
isNew ?
|
||||||
|
null :
|
||||||
|
<IconButton
|
||||||
|
name={icons.REMOVE}
|
||||||
|
tabIndex={-1}
|
||||||
|
onPress={this.onRemovePress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
|
@ -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 (
|
|
||||||
<div className={styles.itemContainer}>
|
|
||||||
<div className={styles.keyInputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={styles.keyInput}
|
|
||||||
name="key"
|
|
||||||
value={keyValue}
|
|
||||||
placeholder={keyPlaceholder}
|
|
||||||
onChange={handleKeyChange}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.valueInputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={styles.valueInput}
|
|
||||||
name="value"
|
|
||||||
value={value}
|
|
||||||
placeholder={valuePlaceholder}
|
|
||||||
onChange={handleValueChange}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.buttonWrapper}>
|
|
||||||
{isNew ? null : (
|
|
||||||
<IconButton
|
|
||||||
name={icons.REMOVE}
|
|
||||||
tabIndex={-1}
|
|
||||||
onPress={handleRemovePress}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KeyValueListInputItem;
|
|
|
@ -14,8 +14,6 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||||
return inputTypes.CHECK;
|
return inputTypes.CHECK;
|
||||||
case 'device':
|
case 'device':
|
||||||
return inputTypes.DEVICE;
|
return inputTypes.DEVICE;
|
||||||
case 'keyValueList':
|
|
||||||
return inputTypes.KEY_VALUE_LIST;
|
|
||||||
case 'playlist':
|
case 'playlist':
|
||||||
return inputTypes.PLAYLIST;
|
return inputTypes.PLAYLIST;
|
||||||
case 'password':
|
case 'password':
|
||||||
|
|
|
@ -83,6 +83,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
|
.modal.small,
|
||||||
|
.modal.medium {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.modalContainer {
|
.modalContainer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
&.isDisabled {
|
.isDisabled {
|
||||||
color: var(--disabledColor);
|
color: var(--disabledColor);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import ArtistSearchInputConnector from './ArtistSearchInputConnector';
|
import ArtistSearchInputConnector from './ArtistSearchInputConnector';
|
||||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
||||||
import styles from './PageHeader.css';
|
import styles from './PageHeader.css';
|
||||||
|
|
||||||
class PageHeader extends Component {
|
class PageHeader extends Component {
|
||||||
|
@ -83,7 +83,6 @@ class PageHeader extends Component {
|
||||||
size={14}
|
size={14}
|
||||||
title={translate('Donate')}
|
title={translate('Donate')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.translation}
|
className={styles.translation}
|
||||||
title={translate('SuggestTranslationChange')}
|
title={translate('SuggestTranslationChange')}
|
||||||
|
@ -91,8 +90,7 @@ class PageHeader extends Component {
|
||||||
to="https://translate.servarr.com/projects/servarr/lidarr/"
|
to="https://translate.servarr.com/projects/servarr/lidarr/"
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
|
<PageHeaderActionsMenuConnector
|
||||||
<PageHeaderActionsMenu
|
|
||||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
90
frontend/src/Components/Page/Header/PageHeaderActionsMenu.js
Normal file
90
frontend/src/Components/Page/Header/PageHeaderActionsMenu.js
Normal file
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<Menu alignMenu={align.RIGHT}>
|
||||||
|
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||||
|
<Icon
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
title={translate('Menu')}
|
||||||
|
/>
|
||||||
|
</MenuButton>
|
||||||
|
|
||||||
|
<MenuContent>
|
||||||
|
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||||
|
<Icon
|
||||||
|
className={styles.itemIcon}
|
||||||
|
name={icons.KEYBOARD}
|
||||||
|
/>
|
||||||
|
{translate('KeyboardShortcuts')}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItemSeparator />
|
||||||
|
|
||||||
|
<MenuItem onPress={onRestartPress}>
|
||||||
|
<Icon
|
||||||
|
className={styles.itemIcon}
|
||||||
|
name={icons.RESTART}
|
||||||
|
/>
|
||||||
|
{translate('Restart')}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem onPress={onShutdownPress}>
|
||||||
|
<Icon
|
||||||
|
className={styles.itemIcon}
|
||||||
|
name={icons.SHUTDOWN}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
/>
|
||||||
|
Shutdown
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
{
|
||||||
|
formsAuth &&
|
||||||
|
<div className={styles.separator} />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
formsAuth &&
|
||||||
|
<MenuItem
|
||||||
|
to={`${window.Lidarr.urlBase}/logout`}
|
||||||
|
noRouter={true}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={styles.itemIcon}
|
||||||
|
name={icons.LOGOUT}
|
||||||
|
/>
|
||||||
|
{translate('Logout')}
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PageHeaderActionsMenu.propTypes = {
|
||||||
|
formsAuth: PropTypes.bool.isRequired,
|
||||||
|
onKeyboardShortcutsPress: PropTypes.func.isRequired,
|
||||||
|
onRestartPress: PropTypes.func.isRequired,
|
||||||
|
onShutdownPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageHeaderActionsMenu;
|
|
@ -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 (
|
|
||||||
<div>
|
|
||||||
<Menu alignMenu={align.RIGHT}>
|
|
||||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
|
||||||
<Icon name={icons.INTERACTIVE} title={translate('Menu')} />
|
|
||||||
</MenuButton>
|
|
||||||
|
|
||||||
<MenuContent>
|
|
||||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
|
||||||
<Icon className={styles.itemIcon} name={icons.KEYBOARD} />
|
|
||||||
{translate('KeyboardShortcuts')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
{isDocker ? null : (
|
|
||||||
<>
|
|
||||||
<MenuItemSeparator />
|
|
||||||
|
|
||||||
<MenuItem onPress={handleRestartPress}>
|
|
||||||
<Icon className={styles.itemIcon} name={icons.RESTART} />
|
|
||||||
{translate('Restart')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem onPress={handleShutdownPress}>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.SHUTDOWN}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
/>
|
|
||||||
{translate('Shutdown')}
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{formsAuth ? (
|
|
||||||
<>
|
|
||||||
<MenuItemSeparator />
|
|
||||||
|
|
||||||
<MenuItem to={`${window.Lidarr.urlBase}/logout`} noRouter={true}>
|
|
||||||
<Icon className={styles.itemIcon} name={icons.LOGOUT} />
|
|
||||||
{translate('Logout')}
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</MenuContent>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PageHeaderActionsMenu;
|
|
|
@ -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 (
|
||||||
|
<PageHeaderActionsMenu
|
||||||
|
{...this.props}
|
||||||
|
onRestartPress={this.onRestartPress}
|
||||||
|
onShutdownPress={this.onShutdownPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PageHeaderActionsMenuConnector.propTypes = {
|
||||||
|
restart: PropTypes.func.isRequired,
|
||||||
|
shutdown: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);
|
|
@ -172,7 +172,7 @@ class SignalRConnector extends Component {
|
||||||
const status = resource.status;
|
const status = resource.status;
|
||||||
|
|
||||||
// Both successful and failed commands need to be
|
// 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') {
|
if (status === 'completed' || status === 'failed') {
|
||||||
this.props.dispatchFinishCommand(resource);
|
this.props.dispatchFinishCommand(resource);
|
||||||
|
@ -224,58 +224,10 @@ class SignalRConnector extends Component {
|
||||||
repopulatePage('trackFileUpdated');
|
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 = () => {
|
handleHealth = () => {
|
||||||
this.props.dispatchFetchHealth();
|
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) => {
|
handleArtist = (body) => {
|
||||||
const action = body.action;
|
const action = body.action;
|
||||||
const section = 'artist';
|
const section = 'artist';
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
line-height: 1.52857143;
|
line-height: 1.52857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.tableContainer {
|
.tableContainer {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.headerCell {
|
.headerCell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
height: 25px;
|
height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.pager {
|
.pager {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.headerCell {
|
.headerCell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
9
frontend/src/Content/Images/Icons/browserconfig.xml
Normal file
9
frontend/src/Content/Images/Icons/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/Content/Images/Icons/mstile-150x150.png"/>
|
||||||
|
<TileColor>#00ccff</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
19
frontend/src/Content/Images/Icons/manifest.json
Normal file
19
frontend/src/Content/Images/Icons/manifest.json
Normal file
|
@ -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": "standalone"
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="__URL_BASE__/Content/Images/Icons/mstile-150x150.png" />
|
|
||||||
<TileColor>
|
|
||||||
#00ccff
|
|
||||||
</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -2,8 +2,8 @@ export const AUTO_COMPLETE = 'autoComplete';
|
||||||
export const CAPTCHA = 'captcha';
|
export const CAPTCHA = 'captcha';
|
||||||
export const CHECK = 'check';
|
export const CHECK = 'check';
|
||||||
export const DEVICE = 'device';
|
export const DEVICE = 'device';
|
||||||
export const KEY_VALUE_LIST = 'keyValueList';
|
|
||||||
export const PLAYLIST = 'playlist';
|
export const PLAYLIST = 'playlist';
|
||||||
|
export const KEY_VALUE_LIST = 'keyValueList';
|
||||||
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
|
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
|
||||||
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
|
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
|
||||||
export const FLOAT = 'float';
|
export const FLOAT = 'float';
|
||||||
|
@ -34,8 +34,8 @@ export const all = [
|
||||||
CAPTCHA,
|
CAPTCHA,
|
||||||
CHECK,
|
CHECK,
|
||||||
DEVICE,
|
DEVICE,
|
||||||
KEY_VALUE_LIST,
|
|
||||||
PLAYLIST,
|
PLAYLIST,
|
||||||
|
KEY_VALUE_LIST,
|
||||||
MONITOR_ALBUMS_SELECT,
|
MONITOR_ALBUMS_SELECT,
|
||||||
MONITOR_NEW_ITEMS_SELECT,
|
MONITOR_NEW_ITEMS_SELECT,
|
||||||
FLOAT,
|
FLOAT,
|
||||||
|
|
|
@ -11,7 +11,6 @@ import Scroller from 'Components/Scroller/Scroller';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectAlbumRow from './SelectAlbumRow';
|
import SelectAlbumRow from './SelectAlbumRow';
|
||||||
import styles from './SelectAlbumModalContent.css';
|
import styles from './SelectAlbumModalContent.css';
|
||||||
|
@ -20,7 +19,6 @@ const columns = [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
label: () => translate('AlbumTitle'),
|
label: () => translate('AlbumTitle'),
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -31,7 +29,6 @@ const columns = [
|
||||||
{
|
{
|
||||||
name: 'releaseDate',
|
name: 'releaseDate',
|
||||||
label: () => translate('ReleaseDate'),
|
label: () => translate('ReleaseDate'),
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -66,22 +63,16 @@ class SelectAlbumModalContent extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
items,
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
onSortPress,
|
|
||||||
onAlbumSelect,
|
onAlbumSelect,
|
||||||
onModalClose
|
onModalClose,
|
||||||
|
isFetching,
|
||||||
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const filter = this.state.filter;
|
const filter = this.state.filter;
|
||||||
const filterLower = filter.toLowerCase();
|
const filterLower = filter.toLowerCase();
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error, 'Unable to load albums');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
|
@ -92,14 +83,10 @@ class SelectAlbumModalContent extends Component {
|
||||||
className={styles.modalBody}
|
className={styles.modalBody}
|
||||||
scrollDirection={scrollDirections.NONE}
|
scrollDirection={scrollDirections.NONE}
|
||||||
>
|
>
|
||||||
<Scroller
|
{
|
||||||
className={styles.scroller}
|
isFetching &&
|
||||||
autoFocus={false}
|
<LoadingIndicator />
|
||||||
>
|
}
|
||||||
{isFetching ? <LoadingIndicator /> : null}
|
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
className={styles.filterInput}
|
className={styles.filterInput}
|
||||||
placeholder={translate('FilterAlbumPlaceholder')}
|
placeholder={translate('FilterAlbumPlaceholder')}
|
||||||
|
@ -109,12 +96,14 @@ class SelectAlbumModalContent extends Component {
|
||||||
onChange={this.onFilterChange}
|
onChange={this.onFilterChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isPopulated && !!items.length ? (
|
<Scroller
|
||||||
|
className={styles.scroller}
|
||||||
|
autoFocus={false}
|
||||||
|
>
|
||||||
|
{
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
sortKey={sortKey}
|
{...otherProps}
|
||||||
sortDirection={sortDirection}
|
|
||||||
onSortPress={onSortPress}
|
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{
|
{
|
||||||
|
@ -133,7 +122,7 @@ class SelectAlbumModalContent extends Component {
|
||||||
}
|
}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
) : null}
|
}
|
||||||
</Scroller>
|
</Scroller>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
|
@ -148,13 +137,8 @@ class SelectAlbumModalContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectAlbumModalContent.propTypes = {
|
SelectAlbumModalContent.propTypes = {
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
sortDirection: PropTypes.string,
|
|
||||||
onSortPress: PropTypes.func.isRequired,
|
|
||||||
onAlbumSelect: PropTypes.func.isRequired,
|
onAlbumSelect: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,14 +3,18 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { clearAlbums, fetchAlbums, setAlbumsSort } from 'Store/Actions/albumSelectionActions';
|
import {
|
||||||
import { saveInteractiveImportItem, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
clearInteractiveImportAlbums,
|
||||||
|
fetchInteractiveImportAlbums,
|
||||||
|
saveInteractiveImportItem,
|
||||||
|
setInteractiveImportAlbumsSort,
|
||||||
|
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import SelectAlbumModalContent from './SelectAlbumModalContent';
|
import SelectAlbumModalContent from './SelectAlbumModalContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createClientSideCollectionSelector('albumSelection'),
|
createClientSideCollectionSelector('interactiveImport.albums'),
|
||||||
(albums) => {
|
(albums) => {
|
||||||
return albums;
|
return albums;
|
||||||
}
|
}
|
||||||
|
@ -18,9 +22,9 @@ function createMapStateToProps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchAlbums,
|
fetchInteractiveImportAlbums,
|
||||||
setAlbumsSort,
|
setInteractiveImportAlbumsSort,
|
||||||
clearAlbums,
|
clearInteractiveImportAlbums,
|
||||||
updateInteractiveImportItem,
|
updateInteractiveImportItem,
|
||||||
saveInteractiveImportItem
|
saveInteractiveImportItem
|
||||||
};
|
};
|
||||||
|
@ -35,20 +39,20 @@ class SelectAlbumModalContentConnector extends Component {
|
||||||
artistId
|
artistId
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
this.props.fetchAlbums({ artistId });
|
this.props.fetchInteractiveImportAlbums({ artistId });
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
// This clears the albums for the queue and hides the queue
|
// This clears the albums for the queue and hides the queue
|
||||||
// We'll need another place to store albums for manual import
|
// We'll need another place to store albums for manual import
|
||||||
this.props.clearAlbums();
|
this.props.clearInteractiveImportAlbums();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onSortPress = (sortKey, sortDirection) => {
|
onSortPress = (sortKey, sortDirection) => {
|
||||||
this.props.setAlbumsSort({ sortKey, sortDirection });
|
this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection });
|
||||||
};
|
};
|
||||||
|
|
||||||
onAlbumSelect = (albumId) => {
|
onAlbumSelect = (albumId) => {
|
||||||
|
@ -78,7 +82,6 @@ class SelectAlbumModalContentConnector extends Component {
|
||||||
return (
|
return (
|
||||||
<SelectAlbumModalContent
|
<SelectAlbumModalContent
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onAlbumSelect={this.onAlbumSelect}
|
onAlbumSelect={this.onAlbumSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -89,9 +92,9 @@ SelectAlbumModalContentConnector.propTypes = {
|
||||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
artistId: PropTypes.number.isRequired,
|
artistId: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
fetchAlbums: PropTypes.func.isRequired,
|
fetchInteractiveImportAlbums: PropTypes.func.isRequired,
|
||||||
setAlbumsSort: PropTypes.func.isRequired,
|
setInteractiveImportAlbumsSort: PropTypes.func.isRequired,
|
||||||
clearAlbums: PropTypes.func.isRequired,
|
clearInteractiveImportAlbums: PropTypes.func.isRequired,
|
||||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|
|
@ -18,17 +18,12 @@
|
||||||
.leftButtons,
|
.leftButtons,
|
||||||
.rightButtons {
|
.rightButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1 0 50%;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leftButtons {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightButtons {
|
.rightButtons {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex: 1 1 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.importMode,
|
.importMode,
|
||||||
|
@ -36,7 +31,6 @@
|
||||||
composes: select from '~Components/Form/SelectInput.css';
|
composes: select from '~Components/Form/SelectInput.css';
|
||||||
|
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
max-width: 100%;
|
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,12 +43,10 @@
|
||||||
.leftButtons,
|
.leftButtons,
|
||||||
.rightButtons {
|
.rightButtons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.leftButtons {
|
.leftButtons {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
max-width: fit-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightButtons {
|
.rightButtons {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
@ -131,8 +130,7 @@ class AddNewItem extends Component {
|
||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('FailedLoadingSearchResults')}
|
{translate('FailedLoadingSearchResults')}
|
||||||
</div>
|
</div>
|
||||||
|
<div>{getErrorMessage(error)}</div>
|
||||||
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
@ -8,7 +8,6 @@ import ParseToolbarButton from 'Parse/ParseToolbarButton';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||||
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
|
|
||||||
|
|
||||||
function CustomFormatSettingsPage() {
|
function CustomFormatSettingsPage() {
|
||||||
return (
|
return (
|
||||||
|
@ -18,13 +17,11 @@ function CustomFormatSettingsPage() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
showSave={false}
|
showSave={false}
|
||||||
additionalButtons={
|
additionalButtons={
|
||||||
<>
|
<Fragment>
|
||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<ParseToolbarButton />
|
<ParseToolbarButton />
|
||||||
|
</Fragment>
|
||||||
<ManageCustomFormatsToolbarButton />
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
import EditCustomFormatModal from './EditCustomFormatModal';
|
import EditCustomFormatModal from './EditCustomFormatModal';
|
||||||
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
|
|
||||||
|
|
||||||
function mapStateToProps() {
|
function mapStateToProps() {
|
||||||
return {};
|
return {};
|
||||||
|
@ -37,7 +36,6 @@ class EditCustomFormatModalConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
EditCustomFormatModalConnector.propTypes = {
|
EditCustomFormatModalConnector.propTypes = {
|
||||||
...EditCustomFormatModalContentConnector.propTypes,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 (
|
|
||||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
|
||||||
<ManageCustomFormatsEditModalContent
|
|
||||||
customFormatIds={customFormatIds}
|
|
||||||
onSavePress={onSavePress}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ManageCustomFormatsEditModal;
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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 (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>{translate('EditSelectedCustomFormats')}</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('IncludeCustomFormatWhenRenaming')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="includeCustomFormatWhenRenaming"
|
|
||||||
value={includeCustomFormatWhenRenaming}
|
|
||||||
values={enableOptions}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
|
||||||
<div className={styles.selected}>
|
|
||||||
{translate('CountCustomFormatsSelected', {
|
|
||||||
count: selectedCount,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
|
||||||
|
|
||||||
<Button onPress={save}>{translate('ApplyChanges')}</Button>
|
|
||||||
</div>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ManageCustomFormatsEditModalContent;
|
|
|
@ -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 (
|
|
||||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
|
||||||
<ManageCustomFormatsModalContent onModalClose={onModalClose} />
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ManageCustomFormatsModal;
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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<OnSelectedChangeCallback>(
|
|
||||||
({ 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 (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>{translate('ManageCustomFormats')}</ModalHeader>
|
|
||||||
<ModalBody>
|
|
||||||
{isFetching ? <LoadingIndicator /> : null}
|
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
|
||||||
|
|
||||||
{isPopulated && !error && !items.length ? (
|
|
||||||
<Alert kind={kinds.INFO}>{translate('NoCustomFormatsFound')}</Alert>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
|
||||||
<Table
|
|
||||||
columns={COLUMNS}
|
|
||||||
horizontalScroll={true}
|
|
||||||
selectAll={true}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
sortKey={sortKey}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
onSelectAllChange={onSelectAllChange}
|
|
||||||
onSortPress={onSortPress}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{items.map((item) => {
|
|
||||||
return (
|
|
||||||
<ManageCustomFormatsModalRow
|
|
||||||
key={item.id}
|
|
||||||
isSelected={selectedState[item.id]}
|
|
||||||
{...item}
|
|
||||||
columns={COLUMNS}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
) : null}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<div className={styles.leftButtons}>
|
|
||||||
<SpinnerButton
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
isSpinning={isDeleting}
|
|
||||||
isDisabled={!anySelected}
|
|
||||||
onPress={onDeletePress}
|
|
||||||
>
|
|
||||||
{translate('Delete')}
|
|
||||||
</SpinnerButton>
|
|
||||||
|
|
||||||
<SpinnerButton
|
|
||||||
isSpinning={isSaving}
|
|
||||||
isDisabled={!anySelected}
|
|
||||||
onPress={onEditPress}
|
|
||||||
>
|
|
||||||
{translate('Edit')}
|
|
||||||
</SpinnerButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
|
|
||||||
<ManageCustomFormatsEditModal
|
|
||||||
isOpen={isEditModalOpen}
|
|
||||||
customFormatIds={selectedIds}
|
|
||||||
onModalClose={onEditModalClose}
|
|
||||||
onSavePress={onSavePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isDeleteModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('DeleteSelectedCustomFormats')}
|
|
||||||
message={translate('DeleteSelectedCustomFormatsMessageText', {
|
|
||||||
count: selectedIds.length,
|
|
||||||
})}
|
|
||||||
confirmLabel={translate('Delete')}
|
|
||||||
onConfirm={onConfirmDelete}
|
|
||||||
onCancel={onDeleteModalClose}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ManageCustomFormatsModalContent;
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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 (
|
|
||||||
<TableRow>
|
|
||||||
<TableSelectCell
|
|
||||||
id={id}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onSelectedChange={handlelectedChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.name}>{name}</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.includeCustomFormatWhenRenaming}>
|
|
||||||
{includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
|
||||||
<IconButton
|
|
||||||
name={icons.EDIT}
|
|
||||||
onPress={handleEditCustomFormatModalOpen}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<EditCustomFormatModalConnector
|
|
||||||
id={id}
|
|
||||||
isOpen={isEditCustomFormatModalOpen}
|
|
||||||
onModalClose={handleEditCustomFormatModalClose}
|
|
||||||
onDeleteCustomFormatPress={handleDeleteCustomFormatPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isDeleteCustomFormatModalOpen}
|
|
||||||
kind="danger"
|
|
||||||
title={translate('DeleteCustomFormat')}
|
|
||||||
message={translate('DeleteCustomFormatMessageText', { name })}
|
|
||||||
confirmLabel={translate('Delete')}
|
|
||||||
isSpinning={isDeleting}
|
|
||||||
onConfirm={handleConfirmDeleteCustomFormat}
|
|
||||||
onCancel={handleDeleteCustomFormatModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ManageCustomFormatsModalRow;
|
|
|
@ -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 (
|
|
||||||
<>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('ManageFormats')}
|
|
||||||
iconName={icons.MANAGE}
|
|
||||||
onPress={openManageModal}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ManageCustomFormatsModal
|
|
||||||
isOpen={isManageModalOpen}
|
|
||||||
onModalClose={closeManageModal}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ManageCustomFormatsToolbarButton;
|
|
|
@ -7,8 +7,8 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
@ -52,13 +52,12 @@ function EditSpecificationModalContent(props) {
|
||||||
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
<div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} />
|
||||||
|
{'More details'} <Link to="https://www.regular-expressions.info/tutorial.html">{'Here'}</Link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
{'Regular expressions can be tested '}
|
||||||
</div>
|
<Link to="http://regexstorm.net/tester">Here</Link>
|
||||||
<div>
|
|
||||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import Column from 'Components/Table/Column';
|
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteDownloadClients,
|
bulkDeleteDownloadClients,
|
||||||
bulkEditDownloadClients,
|
bulkEditDownloadClients,
|
||||||
|
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
typeof ManageDownloadClientsModalRow
|
typeof ManageDownloadClientsModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
|
@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
|
||||||
|
|
||||||
interface ManageDownloadClientsModalContentProps {
|
interface ManageDownloadClientsModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageDownloadClientsModalContent(
|
function ManageDownloadClientsModalContent(
|
||||||
|
@ -218,9 +220,9 @@ function ManageDownloadClientsModalContent(
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
{isPopulated && !error && !items.length ? (
|
{isPopulated && !error && !items.length && (
|
||||||
<Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert>
|
<Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
<Table
|
<Table
|
||||||
|
|
|
@ -156,7 +156,6 @@ class GeneralSettings extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoggingSettings
|
<LoggingSettings
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onInputChange={onInputChange}
|
onInputChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -15,14 +15,12 @@ const logLevelOptions = [
|
||||||
|
|
||||||
function LoggingSettings(props) {
|
function LoggingSettings(props) {
|
||||||
const {
|
const {
|
||||||
advancedSettings,
|
|
||||||
settings,
|
settings,
|
||||||
onInputChange
|
onInputChange
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
logLevel,
|
logLevel
|
||||||
logSizeLimit
|
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -41,30 +39,11 @@ function LoggingSettings(props) {
|
||||||
{...logLevel}
|
{...logLevel}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
isAdvanced={true}
|
|
||||||
>
|
|
||||||
<FormLabel>{translate('LogSizeLimit')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.NUMBER}
|
|
||||||
name="logSizeLimit"
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
unit="MB"
|
|
||||||
helpText={translate('LogSizeLimitHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...logSizeLimit}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LoggingSettings.propTypes = {
|
LoggingSettings.propTypes = {
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
|
||||||
settings: PropTypes.object.isRequired,
|
settings: PropTypes.object.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired
|
onInputChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -292,7 +292,7 @@ function EditImportListModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('ImportListTagsHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -198,9 +198,9 @@ function ManageImportListsModalContent(
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
{isPopulated && !error && !items.length ? (
|
{isPopulated && !error && !items.length && (
|
||||||
<Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert>
|
<Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
<Table
|
<Table
|
||||||
|
|
|
@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import Column from 'Components/Table/Column';
|
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteIndexers,
|
bulkDeleteIndexers,
|
||||||
bulkEditIndexers,
|
bulkEditIndexers,
|
||||||
|
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
typeof ManageIndexersModalRow
|
typeof ManageIndexersModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
|
@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
|
||||||
|
|
||||||
interface ManageIndexersModalContentProps {
|
interface ManageIndexersModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
|
@ -213,9 +215,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
{isPopulated && !error && !items.length ? (
|
{isPopulated && !error && !items.length && (
|
||||||
<Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert>
|
<Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
<Table
|
<Table
|
||||||
|
|
|
@ -191,21 +191,26 @@ class MediaManagement extends Component {
|
||||||
<FieldSet
|
<FieldSet
|
||||||
legend={translate('Importing')}
|
legend={translate('Importing')}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
!isWindows &&
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
size={sizes.MEDIUM}
|
size={sizes.MEDIUM}
|
||||||
>
|
>
|
||||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
<FormLabel>
|
||||||
|
{translate('SkipFreeSpaceCheck')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="skipFreeSpaceCheckWhenImporting"
|
name="skipFreeSpaceCheckWhenImporting"
|
||||||
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
}
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -14,11 +15,11 @@ function createMapStateToProps() {
|
||||||
(state) => state.settings.advancedSettings,
|
(state) => state.settings.advancedSettings,
|
||||||
(state) => state.settings.namingExamples,
|
(state) => state.settings.namingExamples,
|
||||||
createSettingsSectionSelector(SECTION),
|
createSettingsSectionSelector(SECTION),
|
||||||
(advancedSettings, namingExamples, sectionSettings) => {
|
(advancedSettings, examples, sectionSettings) => {
|
||||||
return {
|
return {
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
examples: namingExamples.item,
|
examples: examples.item,
|
||||||
examplesPopulated: namingExamples.isPopulated,
|
examplesPopulated: !_.isEmpty(examples.item),
|
||||||
...sectionSettings
|
...sectionSettings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,9 +94,9 @@ class RootFolder extends Component {
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={this.state.isDeleteRootFolderModalOpen}
|
isOpen={this.state.isDeleteRootFolderModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('RemoveRootFolder')}
|
title={translate('DeleteRootFolder')}
|
||||||
message={translate('RemoveRootFolderArtistsMessageText', { name })}
|
message={translate('DeleteRootFolderMessageText', { name })}
|
||||||
confirmLabel={translate('Remove')}
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={this.onConfirmDeleteRootFolder}
|
onConfirm={this.onConfirmDeleteRootFolder}
|
||||||
onCancel={this.onDeleteRootFolderModalClose}
|
onCancel={this.onDeleteRootFolderModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -105,7 +105,7 @@ function EditNotificationModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('NotificationsTagsArtistHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -87,9 +87,9 @@ function EditDelayProfileModalContent(props) {
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error ?
|
!isFetching && !!error ?
|
||||||
<Alert kind={kinds.DANGER}>
|
<div>
|
||||||
{translate('AddDelayProfileError')}
|
{translate('UnableToAddANewQualityProfilePleaseTryAgain')}
|
||||||
</Alert> :
|
</div> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) {
|
||||||
{
|
{
|
||||||
id === 1 ?
|
id === 1 ?
|
||||||
<Alert>
|
<Alert>
|
||||||
{translate('DefaultDelayProfileArtist')}
|
{translate('DefaultDelayProfileHelpText')}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) {
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
{...tags}
|
{...tags}
|
||||||
helpText={translate('DelayProfileArtistTagsHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -119,7 +119,7 @@ function EditReleaseProfileModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('ReleaseProfileTagArtistHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -24,19 +24,19 @@
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track {
|
.bar {
|
||||||
top: 9px;
|
top: 9px;
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background-color: var(--sliderAccentColor);
|
background-color: var(--sliderAccentColor);
|
||||||
box-shadow: 0 0 0 #000;
|
box-shadow: 0 0 0 #000;
|
||||||
|
|
||||||
&:nth-child(3n + 1) {
|
&:nth-child(3n+1) {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.handle {
|
||||||
top: 1px;
|
top: 1px;
|
||||||
z-index: 0 !important;
|
z-index: 0 !important;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'bar': string;
|
||||||
|
'handle': string;
|
||||||
'kilobitsPerSecond': string;
|
'kilobitsPerSecond': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
'qualityDefinition': string;
|
'qualityDefinition': string;
|
||||||
|
@ -8,9 +10,7 @@ interface CssExports {
|
||||||
'sizeLimit': string;
|
'sizeLimit': string;
|
||||||
'sizes': string;
|
'sizes': string;
|
||||||
'slider': string;
|
'slider': string;
|
||||||
'thumb': string;
|
|
||||||
'title': string;
|
'title': string;
|
||||||
'track': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
|
@ -55,27 +55,6 @@ class QualityDefinition extends Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
trackRenderer(props, state) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className={styles.track}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbRenderer(props, state) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className={styles.thumb}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
@ -195,7 +174,6 @@ class QualityDefinition extends Component {
|
||||||
|
|
||||||
<div className={styles.sizeLimit}>
|
<div className={styles.sizeLimit}>
|
||||||
<ReactSlider
|
<ReactSlider
|
||||||
className={styles.slider}
|
|
||||||
min={slider.min}
|
min={slider.min}
|
||||||
max={slider.max}
|
max={slider.max}
|
||||||
step={slider.step}
|
step={slider.step}
|
||||||
|
@ -204,9 +182,9 @@ class QualityDefinition extends Component {
|
||||||
withTracks={true}
|
withTracks={true}
|
||||||
allowCross={false}
|
allowCross={false}
|
||||||
snapDragDisabled={true}
|
snapDragDisabled={true}
|
||||||
pearling={true}
|
className={styles.slider}
|
||||||
renderThumb={this.thumbRenderer}
|
trackClassName={styles.bar}
|
||||||
renderTrack={this.trackRenderer}
|
thumbClassName={styles.handle}
|
||||||
onChange={this.onSliderChange}
|
onChange={this.onSliderChange}
|
||||||
onAfterChange={this.onAfterSliderChange}
|
onAfterChange={this.onAfterSliderChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
|
||||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,11 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||||
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import Tags from './Tags';
|
import Tags from './Tags';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('tags', sortByProp('label')),
|
(state) => state.tags,
|
||||||
(tags) => {
|
(tags) => {
|
||||||
const isFetching = tags.isFetching || tags.details.isFetching;
|
const isFetching = tags.isFetching || tags.details.isFetching;
|
||||||
const error = tags.error || tags.details.error;
|
const error = tags.error || tags.details.error;
|
||||||
|
|
|
@ -7,7 +7,7 @@ function createRemoveItemHandler(section, url) {
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
queryParams
|
...queryParams
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
dispatch(set({ section, isDeleting: true }));
|
dispatch(set({ section, isDeleting: true }));
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { createAction } from 'redux-actions';
|
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 createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
import createSetClientSideCollectionSortReducer
|
|
||||||
from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
|
||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
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 DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
|
||||||
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
|
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
|
||||||
export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
|
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
|
// 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 fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
|
||||||
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
|
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
|
||||||
export const deleteCustomFormat = createThunk(DELETE_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) => {
|
export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
|
@ -58,30 +47,20 @@ export default {
|
||||||
// State
|
// State
|
||||||
|
|
||||||
defaultState: {
|
defaultState: {
|
||||||
isFetching: false,
|
|
||||||
isPopulated: false,
|
|
||||||
error: null,
|
|
||||||
isSaving: false,
|
|
||||||
saveError: null,
|
|
||||||
isDeleting: false,
|
|
||||||
deleteError: null,
|
|
||||||
items: [],
|
|
||||||
pendingChanges: {},
|
|
||||||
|
|
||||||
isSchemaFetching: false,
|
isSchemaFetching: false,
|
||||||
isSchemaPopulated: false,
|
isSchemaPopulated: false,
|
||||||
schemaError: null,
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
schema: {
|
schema: {
|
||||||
includeCustomFormatWhenRenaming: false
|
includeCustomFormatWhenRenaming: false
|
||||||
},
|
},
|
||||||
|
error: null,
|
||||||
sortKey: 'name',
|
isDeleting: false,
|
||||||
sortDirection: sortDirections.ASCENDING,
|
deleteError: null,
|
||||||
sortPredicates: {
|
isSaving: false,
|
||||||
name: ({ name }) => {
|
saveError: null,
|
||||||
return name.toLocaleLowerCase();
|
items: [],
|
||||||
}
|
pendingChanges: {}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -103,10 +82,7 @@ export default {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch);
|
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;
|
newState.pendingChanges = pendingChanges;
|
||||||
|
|
||||||
return updateSectionState(state, section, newState);
|
return updateSectionState(state, section, newState);
|
||||||
},
|
}
|
||||||
|
|
||||||
[SET_MANAGE_CUSTOM_FORMATS_SORT]: createSetClientSideCollectionSortReducer(section)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue