mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-19 21:13:28 -07:00
Compare commits
277 commits
v2.5.2.431
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
d2330a3232 |
||
|
4cb306780f | ||
|
393db165f3 | ||
|
eb861f06d3 | ||
|
6f1b370772 |
||
|
074f06442a |
||
|
fef111d396 | ||
|
76b7713870 | ||
|
d50ed84541 | ||
|
002e8f5b69 | ||
|
c7b8aa8a04 | ||
|
91f06801ca |
||
|
dc61618711 | ||
|
fd00a5627c | ||
|
66ea1b1dfb | ||
|
72fa05cf41 | ||
|
c51b5c6fba | ||
|
efebab9ba2 | ||
|
47c32c9963 | ||
|
9f229bb684 | ||
|
f9b2e57696 | ||
|
4b48edab0a | ||
|
e087574de7 | ||
|
8877cf99f1 | ||
|
a56e5b3f9a | ||
|
5bb1949ea2 | ||
|
979042948d | ||
|
ebe59b18d9 | ||
|
086a451dff | ||
|
1bcb82eed0 | ||
|
ae9b4cec75 | ||
|
ed777de015 | ||
|
96f956a5d6 | ||
|
68a8f40746 | ||
|
c518cf63e7 |
||
|
da55b8578a | ||
|
234c29ef49 |
||
|
de169e8a1f | ||
|
4b300a448a | ||
|
785bcfda0b |
||
|
94ea751ad2 | ||
|
0c172b58f1 |
||
|
ea2ee70208 | ||
|
8b63928a25 |
||
|
7217e891f7 | ||
|
345bbcd992 |
||
|
bd9d7ba085 | ||
|
3937bebfea | ||
|
767b0930a5 | ||
|
c3f0fc640c | ||
|
9dbcc79436 | ||
|
3dd04cecbf | ||
|
d8850af019 | ||
|
fbfd24e226 |
||
|
d9562c701e | ||
|
d21ad2ad68 | ||
|
556f0ea54b | ||
|
e4a36ca388 | ||
|
1045684935 | ||
|
9ba71ae6b1 | ||
|
89b9352fef | ||
|
c83332e58c | ||
|
4677a1115a |
||
|
6150a57596 | ||
|
13f6b1a086 |
||
|
8027ab5d2e |
||
|
5bdc119b98 | ||
|
1b9b57ae9b | ||
|
c28a97cafd | ||
|
099d19a04d | ||
|
d381463b60 | ||
|
a86bd8e862 | ||
|
4bea38ab9c | ||
|
950c51bc59 | ||
|
18f13fe7f8 | ||
|
f8d4b3a59b | ||
|
5cf9624e55 |
||
|
81895f8033 | ||
|
a1c2bfa527 | ||
|
33049910de |
||
|
6dd87fd348 |
||
|
9314eb34ab | ||
|
84b91ba6c1 | ||
|
6c6f92fbed | ||
|
1e42ae94aa | ||
|
29f5810865 | ||
|
342c82aa1f | ||
|
5a3f879442 | ||
|
6e57c14e57 | ||
|
9fc549b43b | ||
|
a2201001c5 | ||
|
8c99280f07 | ||
|
07db508580 | ||
|
031f32a52c | ||
|
2997c16346 | ||
|
a1a53dbb5e | ||
|
e8bb78e5bb | ||
|
6292f223ac | ||
|
f4dc294ab3 | ||
|
23611cb116 |
||
|
f177345d01 | ||
|
ec050a7b3c | ||
|
860bd04c59 | ||
|
261f30d268 | ||
|
36998abba0 | ||
|
ad12617694 | ||
|
be115da157 | ||
|
664b972494 |
||
|
2b2fd5a175 | ||
|
d8222c066c |
||
|
bc6417229e | ||
|
e0e17a2ea7 | ||
|
5bf2ae9e6f |
||
|
8e01ba5f21 |
||
|
45e8ecffa0 | ||
|
3c4b438d27 | ||
|
8fd79d7291 | ||
|
477a799b8a | ||
|
51a38bc648 | ||
|
917f705695 | ||
|
5a1092b511 | ||
|
ef2c6366c4 | ||
|
1ffb82e364 |
||
|
e2f8753a6a | ||
|
739019498f | ||
|
396b2ae7c1 | ||
|
0216616738 | ||
|
82e0b628cc | ||
|
014f8a58b1 | ||
|
5cbb2848c7 |
||
|
554cf8ec55 | ||
|
4ff6c71456 | ||
|
7cfcf01ae3 | ||
|
17c5c66e54 |
||
|
40dab8deb9 | ||
|
39f0e4d989 | ||
|
35a46eca7b |
||
|
79b29f39f9 | ||
|
0e19c03e9a | ||
|
e6388cab94 | ||
|
47e504fbc9 | ||
|
1a40839202 | ||
|
25a80aa29d | ||
|
7255126af5 | ||
|
166f87ae68 | ||
|
babdf10273 |
||
|
19c2994ff3 | ||
|
e420ee0645 | ||
|
78469a96c9 | ||
|
bc6df548fc | ||
|
797e4c773e | ||
|
119141723a | ||
|
fd1719e58c | ||
|
41612708ff | ||
|
535caf1324 | ||
|
eb3c7d6990 | ||
|
4c603e24f6 | ||
|
ec93c33aa9 | ||
|
afb3fd5bd5 | ||
|
198a13755f | ||
|
44a5654918 | ||
|
8aa0754843 | ||
|
c42e96b55d | ||
|
f92935e3d2 | ||
|
13bb8f5089 | ||
|
ad084cdf91 | ||
|
4bcdc49777 | ||
|
502cb20898 | ||
|
0fd6c263b1 | ||
|
11af8a5e05 |
||
|
88196340a8 | ||
|
4048f2bd72 | ||
|
1f76f6cb19 | ||
|
cc409d50f5 | ||
|
14716a1405 | ||
|
f3a697ca68 | ||
|
f87a8fa9f5 | ||
|
b298bfd932 | ||
|
ecb7d9f6a6 | ||
|
eef55f65c6 | ||
|
beabad5e3a |
||
|
2b1684a793 | ||
|
f23d75d031 | ||
|
abe0090f94 | ||
|
8d32a532e4 | ||
|
a3b78aacdc | ||
|
c43a141b65 | ||
|
d2f5feab5d | ||
|
cfb517a90f | ||
|
3f81e0254f | ||
|
29d17c6347 | ||
|
23f7dc3d3c | ||
|
e39e990696 | ||
|
0c2ede48e8 | ||
|
ca23ac3011 | ||
|
e50e79167a | ||
|
fd3f493eb6 | ||
|
f6d3481e38 | ||
|
e04c28fe2d | ||
|
030300c896 | ||
|
50872f3629 | ||
|
3232e9ab94 | ||
|
8623b4410c | ||
|
a843a46fbe | ||
|
54a758a1b8 | ||
|
ca5379f817 | ||
|
96b51a02e2 | ||
|
f7acd57f73 | ||
|
20eb61dbc6 | ||
|
cb6608975e | ||
|
2db04a3452 | ||
|
5cead5f7ff | ||
|
850c08dda3 | ||
|
f005695b48 | ||
|
55626594c5 | ||
|
c645afc389 | ||
|
33d6169882 | ||
|
c750f4764f |
||
|
c6c52c4117 | ||
|
0e9a5fbd04 | ||
|
f3cd49a2da | ||
|
1a74118d6b | ||
|
9850823298 | ||
|
bd7d25f963 | ||
|
3a5012655e | ||
|
ba55a4778a | ||
|
ff91589f73 | ||
|
2caba01123 | ||
|
4588bc4a7e | ||
|
3381ffc311 |
||
|
f705603211 |
||
|
715274bcc7 | ||
|
9b063aa291 | ||
|
af4ff00476 | ||
|
b14c647c86 | ||
|
00cca22dc7 | ||
|
73fddd5201 | ||
|
b7e5a745a1 | ||
|
b67533bccf | ||
|
21d9ecccd6 | ||
|
34c9300cbf | ||
|
3e5af06622 | ||
|
ccce4f5cc0 |
||
|
5947b4642c | ||
|
856ac2ffa5 | ||
|
b2a4c75cce | ||
|
2818f4e073 | ||
|
60fe75877b | ||
|
2170ada8a2 | ||
|
c26c0d5bd6 | ||
|
d17c6a9b3e | ||
|
af6c0cc6f5 | ||
|
21344361e4 | ||
|
8efb602531 | ||
|
bd5f171fa9 | ||
|
1dee2aaf80 | ||
|
f024dad65f | ||
|
ef3b644d52 |
||
|
af4eeeb893 | ||
|
7738ee78c8 | ||
|
8d3dc0a470 | ||
|
2b8b8ed147 | ||
|
828e04bcad | ||
|
bb6528c104 | ||
|
f49388f3c4 | ||
|
8ff8c27e24 | ||
|
281bcb28fe | ||
|
e60a219671 | ||
|
34ac9dbfcb | ||
|
30ceb77615 | ||
|
304646f324 | ||
|
7555141961 | ||
|
df09e903d4 | ||
|
88f4c0c6cd | ||
|
8f9281f914 | ||
|
5fed16c38a | ||
|
963ffbea4e |
422 changed files with 11347 additions and 5770 deletions
|
@ -6,7 +6,7 @@
|
|||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"version": "20",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
|
|
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -60,6 +60,7 @@ body:
|
|||
- Master
|
||||
- Develop
|
||||
- Nightly
|
||||
- Plugins (experimental)
|
||||
- Other (This issue will be closed)
|
||||
validations:
|
||||
required: true
|
||||
|
|
2
.github/workflows/label-actions.yml
vendored
2
.github/workflows/label-actions.yml
vendored
|
@ -12,6 +12,6 @@ jobs:
|
|||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v3
|
||||
- uses: dessant/label-actions@v4
|
||||
with:
|
||||
process-only: 'issues'
|
||||
|
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
|||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '90'
|
||||
|
|
35
.gitignore
vendored
35
.gitignore
vendored
|
@ -121,6 +121,7 @@ _artifacts
|
|||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
_temp*
|
||||
*.Result.xml
|
||||
coverage*.xml
|
||||
coverage*.json
|
||||
|
@ -139,12 +140,6 @@ project.fragment.lock.json
|
|||
artifacts/
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
#VS outout folders
|
||||
bin
|
||||
obj
|
||||
output/*
|
||||
|
||||
|
||||
# macOS metadata files
|
||||
._*
|
||||
.DS_Store
|
||||
|
@ -163,34 +158,12 @@ Thumbs.db
|
|||
/tools/Addins/*
|
||||
packages.config.md5sum
|
||||
|
||||
|
||||
# Common IntelliJ Platform excludes
|
||||
|
||||
# User specific
|
||||
**/.idea/**/workspace.xml
|
||||
**/.idea/**/tasks.xml
|
||||
**/.idea/shelf/*
|
||||
**/.idea/dictionaries
|
||||
**/.idea/.idea.Radarr.Posix
|
||||
**/.idea/.idea.Radarr.Windows
|
||||
|
||||
# Sensitive or high-churn files
|
||||
**/.idea/**/dataSources/
|
||||
**/.idea/**/dataSources.ids
|
||||
**/.idea/**/dataSources.xml
|
||||
**/.idea/**/dataSources.local.xml
|
||||
**/.idea/**/sqlDataSources.xml
|
||||
**/.idea/**/dynamic.xml
|
||||
|
||||
# Rider
|
||||
# Rider auto-generates .iml files, and contentModel.xml
|
||||
**/.idea/**/*.iml
|
||||
**/.idea/**/contentModel.xml
|
||||
**/.idea/**/modules.xml
|
||||
|
||||
# ignore node_modules symlink
|
||||
node_modules
|
||||
node_modules.nosync
|
||||
|
||||
# API doc generation
|
||||
.config/
|
||||
|
||||
# Ignore Jetbrains IntelliJ Workspace Directories
|
||||
.idea/
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Lidarr
|
||||
|
||||
[](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)
|
||||

|
||||
[](#backers)
|
||||
|
@ -8,6 +9,9 @@
|
|||
|
||||
Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||
|
||||
> [!WARNING]
|
||||
> NOTICE - The Lidarr Metadata Server is currently down impacting adding artists, etc. Please follow [GHI 5498](https://github.com/Lidarr/Lidarr/issues/5498) or see Discord for detaila.
|
||||
|
||||
## Major Features Include:
|
||||
|
||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
|
|
|
@ -9,18 +9,18 @@ variables:
|
|||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '2.5.2'
|
||||
majorVersion: '2.13.3'
|
||||
minorVersion: $[counter('minorVersion', 1076)]
|
||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.424'
|
||||
dotnetVersion: '6.0.427'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-12'
|
||||
linuxImage: 'ubuntu-22.04'
|
||||
macImage: 'macOS-13'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
|
@ -1120,19 +1120,19 @@ stages:
|
|||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@2
|
||||
- task: SonarCloudPrepare@3
|
||||
env:
|
||||
SONAR_SCANNER_OPTS: ''
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'lidarr'
|
||||
scannerMode: 'CLI'
|
||||
scannerMode: 'cli'
|
||||
configMode: 'manual'
|
||||
cliProjectKey: 'lidarr_Lidarr.UI'
|
||||
cliProjectName: 'LidarrUI'
|
||||
cliProjectVersion: '$(lidarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@2
|
||||
- task: SonarCloudAnalyze@3
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
|
@ -1208,12 +1208,12 @@ stages:
|
|||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@2
|
||||
- task: SonarCloudPrepare@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'lidarr'
|
||||
scannerMode: 'MSBuild'
|
||||
scannerMode: 'dotnet'
|
||||
projectKey: 'lidarr_Lidarr'
|
||||
projectName: 'Lidarr'
|
||||
projectVersion: '$(lidarrVersion)'
|
||||
|
@ -1226,10 +1226,10 @@ stages:
|
|||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@2
|
||||
- task: SonarCloudAnalyze@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@5
|
||||
- task: reportgenerator@5.3.11
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
|
|
15
docs.sh
15
docs.sh
|
@ -1,13 +1,18 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
PLATFORM=$1
|
||||
ARCHITECTURE="${2:-x64}"
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
RUNTIME="win-x64"
|
||||
RUNTIME="win-$ARCHITECTURE"
|
||||
elif [ "$PLATFORM" = "Linux" ]; then
|
||||
RUNTIME="linux-x64"
|
||||
RUNTIME="linux-$ARCHITECTURE"
|
||||
elif [ "$PLATFORM" = "Mac" ]; then
|
||||
RUNTIME="osx-x64"
|
||||
RUNTIME="osx-$ARCHITECTURE"
|
||||
else
|
||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
|||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v1 &
|
||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||
|
||||
sleep 45
|
||||
|
||||
|
|
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
|
@ -9,7 +9,7 @@
|
|||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
|
|
|
@ -26,6 +26,7 @@ module.exports = (env) => {
|
|||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||
target: 'web',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
|
@ -134,6 +135,12 @@ module.exports = (env) => {
|
|||
{
|
||||
source: 'frontend/src/Content/robots.txt',
|
||||
destination: path.join(distFolder, 'Content/robots.txt')
|
||||
},
|
||||
|
||||
// manifest.json and browserconfig.xml
|
||||
{
|
||||
source: 'frontend/src/Content/*.(json|xml)',
|
||||
destination: path.join(distFolder, 'Content')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -181,7 +188,7 @@ module.exports = (env) => {
|
|||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
corejs: '3.41'
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -16,6 +16,7 @@ const mixinsFiles = [
|
|||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'autoprefixer',
|
||||
['postcss-mixins', {
|
||||
mixinsFiles
|
||||
}],
|
||||
|
|
|
@ -172,7 +172,8 @@ function HistoryDetails(props) {
|
|||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const {
|
||||
message
|
||||
message,
|
||||
indexer
|
||||
} = data;
|
||||
|
||||
return (
|
||||
|
@ -192,6 +193,14 @@ function HistoryDetails(props) {
|
|||
null
|
||||
}
|
||||
|
||||
{
|
||||
indexer ? (
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{
|
||||
message ?
|
||||
<DescriptionListItem
|
||||
|
|
|
@ -57,30 +57,40 @@ function QueueStatusCell(props) {
|
|||
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = 'Paused';
|
||||
title = translate('Paused');
|
||||
}
|
||||
|
||||
if (status === 'queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = 'Queued';
|
||||
title = translate('Queued');
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = 'Downloaded';
|
||||
title = translate('Downloaded');
|
||||
|
||||
if (trackedDownloadState === 'importBlocked') {
|
||||
title += ` - ${translate('UnableToImportAutomatically')}`;
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importFailed') {
|
||||
title += ` - ${translate('ImportFailed', { sourceTitle })}`;
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ' - Waiting to Import';
|
||||
title += ` - ${translate('WaitingToImport')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ' - Importing';
|
||||
title += ` - ${translate('Importing')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ' - Waiting to Process';
|
||||
title += ` - ${translate('WaitingToProcess')}`;
|
||||
iconKind = kinds.DANGER;
|
||||
}
|
||||
}
|
||||
|
@ -91,36 +101,38 @@ function QueueStatusCell(props) {
|
|||
|
||||
if (status === 'delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = 'Pending';
|
||||
title = translate('Pending');
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
iconName = icons.PENDING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = 'Pending - Download client is unavailable';
|
||||
title = translate('PendingDownloadClientUnavailable');
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = 'Download failed';
|
||||
title = translate('DownloadFailed');
|
||||
}
|
||||
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||
const warningMessage =
|
||||
errorMessage || translate('CheckDownloadClientForDetails');
|
||||
title = translate('DownloadWarning', { warningMessage });
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = `Import failed: ${sourceTitle}`;
|
||||
title = translate('ImportFailed', { sourceTitle });
|
||||
} else {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = 'Download failed';
|
||||
title = translate('DownloadFailed');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,17 +8,17 @@ function ArtistMonitorNewItemsOptionsPopoverContent() {
|
|||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllAlbums')}
|
||||
data="Monitor all new albums"
|
||||
data={translate('MonitorAllAlbums')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('NewAlbums')}
|
||||
data="Monitor new albums released after the newest existing album"
|
||||
data={translate('MonitorNewAlbumsData')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data="Don't monitor any new albums"
|
||||
data={translate('MonitorNoAlbumsData')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ interface Album extends ModelBase {
|
|||
monitored: boolean;
|
||||
releaseDate: string;
|
||||
statistics: Statistics;
|
||||
lastSearchTime?: string;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -121,6 +121,8 @@
|
|||
|
||||
.releaseDate,
|
||||
.sizeOnDisk,
|
||||
.albumType,
|
||||
.secondaryTypes,
|
||||
.qualityProfileName,
|
||||
.links,
|
||||
.tags {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
interface CssExports {
|
||||
'albumNavigationButton': string;
|
||||
'albumNavigationButtons': string;
|
||||
'albumType': string;
|
||||
'alternateTitlesIconContainer': string;
|
||||
'backdrop': string;
|
||||
'backdropOverlay': string;
|
||||
|
@ -20,6 +21,7 @@ interface CssExports {
|
|||
'overview': string;
|
||||
'qualityProfileName': string;
|
||||
'releaseDate': string;
|
||||
'secondaryTypes': string;
|
||||
'sizeOnDisk': string;
|
||||
'tags': string;
|
||||
'title': string;
|
||||
|
|
|
@ -192,6 +192,7 @@ class AlbumDetails extends Component {
|
|||
duration,
|
||||
overview,
|
||||
albumType,
|
||||
secondaryTypes,
|
||||
statistics = {},
|
||||
monitored,
|
||||
releaseDate,
|
||||
|
@ -204,6 +205,7 @@ class AlbumDetails extends Component {
|
|||
isFetching,
|
||||
isPopulated,
|
||||
albumsError,
|
||||
tracksError,
|
||||
trackFilesError,
|
||||
hasTrackFiles,
|
||||
shortDateFormat,
|
||||
|
@ -396,10 +398,11 @@ class AlbumDetails extends Component {
|
|||
<div className={styles.details}>
|
||||
<div>
|
||||
{
|
||||
!!duration &&
|
||||
duration ?
|
||||
<span className={styles.duration}>
|
||||
{formatDuration(duration)}
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
<HeartRating
|
||||
|
@ -418,14 +421,15 @@ class AlbumDetails extends Component {
|
|||
title={translate('ReleaseDate')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CALENDAR}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.releaseDate}>
|
||||
{moment(releaseDate).format(shortDateFormat)}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.CALENDAR}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.releaseDate}>
|
||||
{moment(releaseDate).format(shortDateFormat)}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Tooltip
|
||||
|
@ -434,16 +438,15 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
|
@ -459,32 +462,55 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
{
|
||||
!!albumType &&
|
||||
albumType ?
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={translate('Type')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
size={17}
|
||||
/>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.albumType}>
|
||||
{albumType}
|
||||
</span>
|
||||
</div>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{albumType}
|
||||
</span>
|
||||
</Label>
|
||||
{
|
||||
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
|
||||
|
@ -493,14 +519,15 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
{translate('Links')}
|
||||
</span>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.links}>
|
||||
{translate('Links')}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
|
@ -526,8 +553,9 @@ class AlbumDetails extends Component {
|
|||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isPopulated && !albumsError && !trackFilesError &&
|
||||
<LoadingIndicator />
|
||||
!isPopulated && !albumsError && !tracksError && !trackFilesError ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -538,6 +566,14 @@ class AlbumDetails extends Component {
|
|||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && tracksError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('TracksLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && trackFilesError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
|
@ -566,6 +602,14 @@ class AlbumDetails extends Component {
|
|||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !media.length ?
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('NoMediumInformation')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModalConnector
|
||||
|
@ -632,6 +676,7 @@ AlbumDetails.propTypes = {
|
|||
duration: PropTypes.number,
|
||||
overview: PropTypes.string,
|
||||
albumType: PropTypes.string.isRequired,
|
||||
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
|
@ -658,6 +703,8 @@ AlbumDetails.propTypes = {
|
|||
};
|
||||
|
||||
AlbumDetails.defaultProps = {
|
||||
secondaryTypes: [],
|
||||
statistics: {},
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
|||
import Logs from 'System/Logs/Logs';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import Updates from 'System/Updates/Updates';
|
||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
|
@ -248,7 +248,7 @@ function AppRoutes(props) {
|
|||
|
||||
<Route
|
||||
path="/system/updates"
|
||||
component={UpdatesConnector}
|
||||
component={Updates}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
|
|
@ -44,6 +44,7 @@ export interface CustomFilter {
|
|||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
version: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
|
|
|
@ -4,6 +4,7 @@ import AppSectionState, {
|
|||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import Indexer from 'typings/Indexer';
|
||||
|
@ -12,13 +13,16 @@ import MetadataProfile from 'typings/MetadataProfile';
|
|||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import RootFolder from 'typings/RootFolder';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
import General from 'typings/Settings/General';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type GeneralAppState = AppSectionItemState<General>;
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
AppSectionDeleteState,
|
||||
|
@ -41,6 +45,11 @@ export interface MetadataProfilesAppState
|
|||
extends AppSectionState<MetadataProfile>,
|
||||
AppSectionSchemaState<MetadataProfile> {}
|
||||
|
||||
export interface CustomFormatAppState
|
||||
extends AppSectionState<CustomFormat>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface RootFolderAppState
|
||||
extends AppSectionState<RootFolder>,
|
||||
AppSectionDeleteState,
|
||||
|
@ -50,7 +59,10 @@ export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
|||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
customFormats: CustomFormatAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
import Update from 'typings/Update';
|
||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type UpdateAppState = AppSectionState<Update>;
|
||||
|
||||
interface SystemAppState {
|
||||
updates: UpdateAppState;
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
|
|
|
@ -149,9 +149,7 @@ class AlbumRow extends Component {
|
|||
if (name === 'secondaryTypes') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
secondaryTypes
|
||||
}
|
||||
{secondaryTypes.join(', ')}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, inputTypes, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditArtistModalContent.css';
|
||||
|
||||
|
@ -93,7 +93,7 @@ class EditArtistModalContent extends Component {
|
|||
|
||||
<ModalBody>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('Monitored')}
|
||||
</FormLabel>
|
||||
|
@ -107,9 +107,10 @@ class EditArtistModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('MonitorNewItems')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
|
@ -132,7 +133,7 @@ class EditArtistModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('QualityProfile')}
|
||||
</FormLabel>
|
||||
|
@ -146,10 +147,10 @@ class EditArtistModalContent extends Component {
|
|||
</FormGroup>
|
||||
|
||||
{
|
||||
showMetadataProfile &&
|
||||
<FormGroup>
|
||||
showMetadataProfile ?
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
Metadata Profile
|
||||
{translate('MetadataProfile')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
|
@ -173,10 +174,11 @@ class EditArtistModalContent extends Component {
|
|||
{...metadataProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('Path')}
|
||||
</FormLabel>
|
||||
|
@ -189,7 +191,7 @@ class EditArtistModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('Tags')}
|
||||
</FormLabel>
|
||||
|
@ -209,7 +211,7 @@ class EditArtistModalContent extends Component {
|
|||
kind={kinds.DANGER}
|
||||
onPress={onDeleteArtistPress}
|
||||
>
|
||||
Delete
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
|
|
|
@ -79,7 +79,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Last Album')}
|
||||
{translate('LastAlbum')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
|
|
@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
|
|||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
|
|
|
@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
|
|||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
value={tags}
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('ICalTagsArtistHelpText')}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -3,8 +3,8 @@ import React, { Component } from 'react';
|
|||
import Alert from 'Components/Alert';
|
||||
import PathInput from 'Components/Form/PathInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
|
@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component {
|
|||
className={styles.mappedDrivesWarning}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/lidarr/faq">FAQ</Link> for more information.
|
||||
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/lidarr/faq#why-cant-lidarr-see-my-files-on-a-remote-server' })} />
|
||||
</Alert>
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ const EVENT_TYPE_OPTIONS = [
|
|||
{
|
||||
id: 7,
|
||||
get name() {
|
||||
return translate('ImportFailed');
|
||||
return translate('ImportCompleteFailed');
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -20,6 +20,8 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
|||
import TextInput from './TextInput';
|
||||
import styles from './EnhancedSelectInput.css';
|
||||
|
||||
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
||||
|
||||
function isArrowKey(keyCode) {
|
||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||
}
|
||||
|
@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
|
|||
// Listeners
|
||||
|
||||
onComputeMaxHeight = (data) => {
|
||||
const {
|
||||
top,
|
||||
bottom
|
||||
} = data.offsets.reference;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if ((/^botton/).test(data.placement)) {
|
||||
data.styles.maxHeight = windowHeight - bottom;
|
||||
} else {
|
||||
data.styles.maxHeight = top;
|
||||
}
|
||||
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
@ -457,6 +450,10 @@ class EnhancedSelectInput extends Component {
|
|||
order: 851,
|
||||
enabled: true,
|
||||
fn: this.onComputeMaxHeight
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
boundariesElement: 'viewport'
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -49,12 +49,12 @@ function getComponent(type) {
|
|||
case inputTypes.DEVICE:
|
||||
return DeviceInputConnector;
|
||||
|
||||
case inputTypes.PLAYLIST:
|
||||
return PlaylistInputConnector;
|
||||
|
||||
case inputTypes.KEY_VALUE_LIST:
|
||||
return KeyValueListInput;
|
||||
|
||||
case inputTypes.PLAYLIST:
|
||||
return PlaylistInputConnector;
|
||||
|
||||
case inputTypes.MONITOR_ALBUMS_SELECT:
|
||||
return MonitorAlbumsSelectInput;
|
||||
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
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;
|
104
frontend/src/Components/Form/KeyValueListInput.tsx
Normal file
104
frontend/src/Components/Form/KeyValueListInput.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
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,13 +5,19 @@
|
|||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
.keyInputWrapper {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.valueInputWrapper {
|
||||
flex: 1 0 0;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
flex: 0 0 22px;
|
||||
}
|
||||
|
@ -20,6 +26,10 @@
|
|||
.valueInput {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
background-color: transparent;
|
||||
color: var(--textColor);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--helpTextColor);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'buttonWrapper': string;
|
||||
'inputWrapper': string;
|
||||
'itemContainer': string;
|
||||
'keyInput': string;
|
||||
'keyInputWrapper': string;
|
||||
'valueInput': string;
|
||||
'valueInputWrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
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;
|
89
frontend/src/Components/Form/KeyValueListInputItem.tsx
Normal file
89
frontend/src/Components/Form/KeyValueListInputItem.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
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,6 +14,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||
return inputTypes.CHECK;
|
||||
case 'device':
|
||||
return inputTypes.DEVICE;
|
||||
case 'keyValueList':
|
||||
return inputTypes.KEY_VALUE_LIST;
|
||||
case 'playlist':
|
||||
return inputTypes.PLAYLIST;
|
||||
case 'password':
|
||||
|
|
|
@ -83,13 +83,6 @@
|
|||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.modal.small,
|
||||
.modal.medium {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalContainer {
|
||||
position: fixed;
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
&.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
|||
import translate from 'Utilities/String/translate';
|
||||
import ArtistSearchInputConnector from './ArtistSearchInputConnector';
|
||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
import styles from './PageHeader.css';
|
||||
|
||||
class PageHeader extends Component {
|
||||
|
@ -83,6 +83,7 @@ class PageHeader extends Component {
|
|||
size={14}
|
||||
title={translate('Donate')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.translation}
|
||||
title={translate('SuggestTranslationChange')}
|
||||
|
@ -90,7 +91,8 @@ class PageHeader extends Component {
|
|||
to="https://translate.servarr.com/projects/servarr/lidarr/"
|
||||
size={24}
|
||||
/>
|
||||
<PageHeaderActionsMenuConnector
|
||||
|
||||
<PageHeaderActionsMenu
|
||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
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;
|
|
@ -0,0 +1,87 @@
|
|||
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;
|
|
@ -1,56 +0,0 @@
|
|||
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;
|
||||
|
||||
// Both successful and failed commands need to be
|
||||
// completed, otherwise they spin until they timeout.
|
||||
// completed, otherwise they spin until they time out.
|
||||
|
||||
if (status === 'completed' || status === 'failed') {
|
||||
this.props.dispatchFinishCommand(resource);
|
||||
|
@ -224,10 +224,58 @@ class SignalRConnector extends Component {
|
|||
repopulatePage('trackFileUpdated');
|
||||
};
|
||||
|
||||
handleDownloadclient = ({ action, resource }) => {
|
||||
const section = 'settings.downloadClients';
|
||||
|
||||
if (action === 'created' || action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...resource });
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||
}
|
||||
};
|
||||
|
||||
handleHealth = () => {
|
||||
this.props.dispatchFetchHealth();
|
||||
};
|
||||
|
||||
handleImportlist = ({ action, resource }) => {
|
||||
const section = 'settings.importLists';
|
||||
|
||||
if (action === 'created' || action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...resource });
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||
}
|
||||
};
|
||||
|
||||
handleIndexer = ({ action, resource }) => {
|
||||
const section = 'settings.indexers';
|
||||
|
||||
if (action === 'created' || action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...resource });
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||
}
|
||||
};
|
||||
|
||||
handleMetadata = ({ action, resource }) => {
|
||||
const section = 'settings.metadata';
|
||||
|
||||
if (action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...resource });
|
||||
}
|
||||
};
|
||||
|
||||
handleNotification = ({ action, resource }) => {
|
||||
const section = 'settings.notifications';
|
||||
|
||||
if (action === 'created' || action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...resource });
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||
}
|
||||
};
|
||||
|
||||
handleArtist = (body) => {
|
||||
const action = body.action;
|
||||
const section = 'artist';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
line-height: 1.52857143;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.tableContainer {
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.headerCell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
height: 25px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.pager {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.headerCell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/Content/Images/Icons/mstile-150x150.png"/>
|
||||
<TileColor>#00ccff</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
11
frontend/src/Content/browserconfig.xml
Normal file
11
frontend/src/Content/browserconfig.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?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>
|
19
frontend/src/Content/manifest.json
Normal file
19
frontend/src/Content/manifest.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"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 CHECK = 'check';
|
||||
export const DEVICE = 'device';
|
||||
export const PLAYLIST = 'playlist';
|
||||
export const KEY_VALUE_LIST = 'keyValueList';
|
||||
export const PLAYLIST = 'playlist';
|
||||
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
|
||||
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
|
||||
export const FLOAT = 'float';
|
||||
|
@ -34,8 +34,8 @@ export const all = [
|
|||
CAPTCHA,
|
||||
CHECK,
|
||||
DEVICE,
|
||||
PLAYLIST,
|
||||
KEY_VALUE_LIST,
|
||||
PLAYLIST,
|
||||
MONITOR_ALBUMS_SELECT,
|
||||
MONITOR_NEW_ITEMS_SELECT,
|
||||
FLOAT,
|
||||
|
|
|
@ -11,6 +11,7 @@ import Scroller from 'Components/Scroller/Scroller';
|
|||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectAlbumRow from './SelectAlbumRow';
|
||||
import styles from './SelectAlbumModalContent.css';
|
||||
|
@ -19,6 +20,7 @@ const columns = [
|
|||
{
|
||||
name: 'title',
|
||||
label: () => translate('AlbumTitle'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
@ -29,6 +31,7 @@ const columns = [
|
|||
{
|
||||
name: 'releaseDate',
|
||||
label: () => translate('ReleaseDate'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
@ -63,16 +66,22 @@ class SelectAlbumModalContent extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
onAlbumSelect,
|
||||
onModalClose,
|
||||
isFetching,
|
||||
...otherProps
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
onSortPress,
|
||||
onAlbumSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const filter = this.state.filter;
|
||||
const filterLower = filter.toLowerCase();
|
||||
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load albums');
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
|
@ -83,27 +92,29 @@ class SelectAlbumModalContent extends Component {
|
|||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder={translate('FilterAlbumPlaceholder')}
|
||||
name="filter"
|
||||
value={filter}
|
||||
autoFocus={true}
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
autoFocus={false}
|
||||
>
|
||||
{
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{error ? <div>{errorMessage}</div> : null}
|
||||
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder={translate('FilterAlbumPlaceholder')}
|
||||
name="filter"
|
||||
value={filter}
|
||||
autoFocus={true}
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
|
||||
{isPopulated && !!items.length ? (
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
|
@ -122,7 +133,7 @@ class SelectAlbumModalContent extends Component {
|
|||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
) : null}
|
||||
</Scroller>
|
||||
</ModalBody>
|
||||
|
||||
|
@ -137,8 +148,13 @@ class SelectAlbumModalContent extends Component {
|
|||
}
|
||||
|
||||
SelectAlbumModalContent.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onAlbumSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -3,18 +3,14 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
clearInteractiveImportAlbums,
|
||||
fetchInteractiveImportAlbums,
|
||||
saveInteractiveImportItem,
|
||||
setInteractiveImportAlbumsSort,
|
||||
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import { clearAlbums, fetchAlbums, setAlbumsSort } from 'Store/Actions/albumSelectionActions';
|
||||
import { saveInteractiveImportItem, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import SelectAlbumModalContent from './SelectAlbumModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('interactiveImport.albums'),
|
||||
createClientSideCollectionSelector('albumSelection'),
|
||||
(albums) => {
|
||||
return albums;
|
||||
}
|
||||
|
@ -22,9 +18,9 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchInteractiveImportAlbums,
|
||||
setInteractiveImportAlbumsSort,
|
||||
clearInteractiveImportAlbums,
|
||||
fetchAlbums,
|
||||
setAlbumsSort,
|
||||
clearAlbums,
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem
|
||||
};
|
||||
|
@ -39,20 +35,20 @@ class SelectAlbumModalContentConnector extends Component {
|
|||
artistId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchInteractiveImportAlbums({ artistId });
|
||||
this.props.fetchAlbums({ artistId });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// This clears the albums for the queue and hides the queue
|
||||
// We'll need another place to store albums for manual import
|
||||
this.props.clearInteractiveImportAlbums();
|
||||
this.props.clearAlbums();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey, sortDirection) => {
|
||||
this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection });
|
||||
this.props.setAlbumsSort({ sortKey, sortDirection });
|
||||
};
|
||||
|
||||
onAlbumSelect = (albumId) => {
|
||||
|
@ -82,6 +78,7 @@ class SelectAlbumModalContentConnector extends Component {
|
|||
return (
|
||||
<SelectAlbumModalContent
|
||||
{...this.props}
|
||||
onSortPress={this.onSortPress}
|
||||
onAlbumSelect={this.onAlbumSelect}
|
||||
/>
|
||||
);
|
||||
|
@ -92,9 +89,9 @@ SelectAlbumModalContentConnector.propTypes = {
|
|||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
artistId: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchInteractiveImportAlbums: PropTypes.func.isRequired,
|
||||
setInteractiveImportAlbumsSort: PropTypes.func.isRequired,
|
||||
clearInteractiveImportAlbums: PropTypes.func.isRequired,
|
||||
fetchAlbums: PropTypes.func.isRequired,
|
||||
setAlbumsSort: PropTypes.func.isRequired,
|
||||
clearAlbums: PropTypes.func.isRequired,
|
||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
|
@ -130,7 +131,8 @@ class AddNewItem extends Component {
|
|||
<div className={styles.helpText}>
|
||||
{translate('FailedLoadingSearchResults')}
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
|
||||
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
|
@ -8,6 +8,7 @@ import ParseToolbarButton from 'Parse/ParseToolbarButton';
|
|||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
|
||||
|
||||
function CustomFormatSettingsPage() {
|
||||
return (
|
||||
|
@ -17,11 +18,13 @@ function CustomFormatSettingsPage() {
|
|||
// @ts-ignore
|
||||
showSave={false}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<ParseToolbarButton />
|
||||
</Fragment>
|
||||
|
||||
<ManageCustomFormatsToolbarButton />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditCustomFormatModal from './EditCustomFormatModal';
|
||||
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
|
@ -36,6 +37,7 @@ class EditCustomFormatModalConnector extends Component {
|
|||
}
|
||||
|
||||
EditCustomFormatModalConnector.propTypes = {
|
||||
...EditCustomFormatModalContentConnector.propTypes,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
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;
|
|
@ -0,0 +1,16 @@
|
|||
.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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'modalFooter': string;
|
||||
'selected': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,125 @@
|
|||
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;
|
|
@ -0,0 +1,20 @@
|
|||
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;
|
|
@ -0,0 +1,16 @@
|
|||
.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;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// 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;
|
|
@ -0,0 +1,244 @@
|
|||
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;
|
|
@ -0,0 +1,12 @@
|
|||
.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;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// 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;
|
|
@ -0,0 +1,126 @@
|
|||
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;
|
|
@ -0,0 +1,28 @@
|
|||
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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
|
@ -52,12 +52,13 @@ function EditSpecificationModalContent(props) {
|
|||
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
<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>
|
||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||
</div>
|
||||
<div>
|
||||
{'Regular expressions can be tested '}
|
||||
<Link to="http://regexstorm.net/tester">Here</Link>
|
||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
|
|
@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
bulkDeleteDownloadClients,
|
||||
bulkEditDownloadClients,
|
||||
|
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
|||
typeof ManageDownloadClientsModalRow
|
||||
>['onSelectedChange'];
|
||||
|
||||
const COLUMNS = [
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: () => translate('Name'),
|
||||
|
@ -82,8 +82,6 @@ const COLUMNS = [
|
|||
|
||||
interface ManageDownloadClientsModalContentProps {
|
||||
onModalClose(): void;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ManageDownloadClientsModalContent(
|
||||
|
@ -220,9 +218,9 @@ function ManageDownloadClientsModalContent(
|
|||
|
||||
{error ? <div>{errorMessage}</div> : null}
|
||||
|
||||
{isPopulated && !error && !items.length && (
|
||||
{isPopulated && !error && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||
<Table
|
||||
|
|
|
@ -156,6 +156,7 @@ class GeneralSettings extends Component {
|
|||
/>
|
||||
|
||||
<LoggingSettings
|
||||
advancedSettings={advancedSettings}
|
||||
settings={settings}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -15,12 +15,14 @@ const logLevelOptions = [
|
|||
|
||||
function LoggingSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
logLevel
|
||||
logLevel,
|
||||
logSizeLimit
|
||||
} = settings;
|
||||
|
||||
return (
|
||||
|
@ -39,11 +41,30 @@ function LoggingSettings(props) {
|
|||
{...logLevel}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
LoggingSettings.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -292,7 +292,7 @@ function EditImportListModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('ImportListTagsHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -198,9 +198,9 @@ function ManageImportListsModalContent(
|
|||
|
||||
{error ? <div>{errorMessage}</div> : null}
|
||||
|
||||
{isPopulated && !error && !items.length && (
|
||||
{isPopulated && !error && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||
<Table
|
||||
|
|
|
@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
bulkDeleteIndexers,
|
||||
bulkEditIndexers,
|
||||
|
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
|||
typeof ManageIndexersModalRow
|
||||
>['onSelectedChange'];
|
||||
|
||||
const COLUMNS = [
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: () => translate('Name'),
|
||||
|
@ -82,8 +82,6 @@ const COLUMNS = [
|
|||
|
||||
interface ManageIndexersModalContentProps {
|
||||
onModalClose(): void;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
|
@ -215,9 +213,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||
|
||||
{error ? <div>{errorMessage}</div> : null}
|
||||
|
||||
{isPopulated && !error && !items.length && (
|
||||
{isPopulated && !error && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||
<Table
|
||||
|
|
|
@ -191,26 +191,21 @@ class MediaManagement extends Component {
|
|||
<FieldSet
|
||||
legend={translate('Importing')}
|
||||
>
|
||||
{
|
||||
!isWindows &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>
|
||||
{translate('SkipFreeSpaceCheck')}
|
||||
</FormLabel>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipFreeSpaceCheckWhenImporting"
|
||||
helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipFreeSpaceCheckWhenImporting"
|
||||
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -15,11 +14,11 @@ function createMapStateToProps() {
|
|||
(state) => state.settings.advancedSettings,
|
||||
(state) => state.settings.namingExamples,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, examples, sectionSettings) => {
|
||||
(advancedSettings, namingExamples, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
examples: examples.item,
|
||||
examplesPopulated: !_.isEmpty(examples.item),
|
||||
examples: namingExamples.item,
|
||||
examplesPopulated: namingExamples.isPopulated,
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
|
|
|
@ -94,9 +94,9 @@ class RootFolder extends Component {
|
|||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteRootFolderModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteRootFolder')}
|
||||
message={translate('DeleteRootFolderMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
title={translate('RemoveRootFolder')}
|
||||
message={translate('RemoveRootFolderArtistsMessageText', { name })}
|
||||
confirmLabel={translate('Remove')}
|
||||
onConfirm={this.onConfirmDeleteRootFolder}
|
||||
onCancel={this.onDeleteRootFolderModalClose}
|
||||
/>
|
||||
|
|
|
@ -105,7 +105,7 @@ function EditNotificationModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('NotificationsTagsArtistHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -87,9 +87,9 @@ function EditDelayProfileModalContent(props) {
|
|||
|
||||
{
|
||||
!isFetching && !!error ?
|
||||
<div>
|
||||
{translate('UnableToAddANewQualityProfilePleaseTryAgain')}
|
||||
</div> :
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddDelayProfileError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
|
@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) {
|
|||
{
|
||||
id === 1 ?
|
||||
<Alert>
|
||||
{translate('DefaultDelayProfileHelpText')}
|
||||
{translate('DefaultDelayProfileArtist')}
|
||||
</Alert> :
|
||||
|
||||
<FormGroup>
|
||||
|
@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) {
|
|||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
{...tags}
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('DelayProfileArtistTagsHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -119,7 +119,7 @@ function EditReleaseProfileModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('ReleaseProfileTagArtistHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -24,19 +24,19 @@
|
|||
height: 20px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
.track {
|
||||
top: 9px;
|
||||
margin: 0 5px;
|
||||
height: 3px;
|
||||
background-color: var(--sliderAccentColor);
|
||||
box-shadow: 0 0 0 #000;
|
||||
|
||||
&:nth-child(3n+1) {
|
||||
&:nth-child(3n + 1) {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
.thumb {
|
||||
top: 1px;
|
||||
z-index: 0 !important;
|
||||
width: 18px;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'bar': string;
|
||||
'handle': string;
|
||||
'kilobitsPerSecond': string;
|
||||
'quality': string;
|
||||
'qualityDefinition': string;
|
||||
|
@ -10,7 +8,9 @@ interface CssExports {
|
|||
'sizeLimit': string;
|
||||
'sizes': string;
|
||||
'slider': string;
|
||||
'thumb': string;
|
||||
'title': string;
|
||||
'track': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
|
|
@ -55,6 +55,27 @@ class QualityDefinition extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
trackRenderer(props, state) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={styles.track}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
thumbRenderer(props, state) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={styles.thumb}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
|
@ -174,6 +195,7 @@ class QualityDefinition extends Component {
|
|||
|
||||
<div className={styles.sizeLimit}>
|
||||
<ReactSlider
|
||||
className={styles.slider}
|
||||
min={slider.min}
|
||||
max={slider.max}
|
||||
step={slider.step}
|
||||
|
@ -182,9 +204,9 @@ class QualityDefinition extends Component {
|
|||
withTracks={true}
|
||||
allowCross={false}
|
||||
snapDragDisabled={true}
|
||||
className={styles.slider}
|
||||
trackClassName={styles.bar}
|
||||
thumbClassName={styles.handle}
|
||||
pearling={true}
|
||||
renderThumb={this.thumbRenderer}
|
||||
renderTrack={this.trackRenderer}
|
||||
onChange={this.onSliderChange}
|
||||
onAfterChange={this.onAfterSliderChange}
|
||||
/>
|
||||
|
|
|
@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
|
|||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
|
||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
|
||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
|
|
@ -4,11 +4,13 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import Tags from './Tags';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.tags,
|
||||
createSortedSectionSelector('tags', sortByProp('label')),
|
||||
(tags) => {
|
||||
const isFetching = tags.isFetching || tags.details.isFetching;
|
||||
const error = tags.error || tags.details.error;
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createSetClientSideCollectionSortReducer
|
||||
from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
|
@ -21,6 +26,9 @@ export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat';
|
|||
export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
|
||||
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
|
||||
export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
|
||||
export const BULK_EDIT_CUSTOM_FORMATS = 'settings/downloadClients/bulkEditCustomFormats';
|
||||
export const BULK_DELETE_CUSTOM_FORMATS = 'settings/downloadClients/bulkDeleteCustomFormats';
|
||||
export const SET_MANAGE_CUSTOM_FORMATS_SORT = 'settings/downloadClients/setManageCustomFormatsSort';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
@ -28,6 +36,9 @@ export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
|
|||
export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
|
||||
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
|
||||
export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT);
|
||||
export const bulkEditCustomFormats = createThunk(BULK_EDIT_CUSTOM_FORMATS);
|
||||
export const bulkDeleteCustomFormats = createThunk(BULK_DELETE_CUSTOM_FORMATS);
|
||||
export const setManageCustomFormatsSort = createAction(SET_MANAGE_CUSTOM_FORMATS_SORT);
|
||||
|
||||
export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
|
||||
return {
|
||||
|
@ -47,20 +58,30 @@ export default {
|
|||
// State
|
||||
|
||||
defaultState: {
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
items: [],
|
||||
pendingChanges: {},
|
||||
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: {
|
||||
includeCustomFormatWhenRenaming: false
|
||||
},
|
||||
error: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: ({ name }) => {
|
||||
return name.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
|
@ -82,7 +103,10 @@ export default {
|
|||
}));
|
||||
|
||||
createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch);
|
||||
}
|
||||
},
|
||||
|
||||
[BULK_EDIT_CUSTOM_FORMATS]: createBulkEditItemHandler(section, '/customformat/bulk'),
|
||||
[BULK_DELETE_CUSTOM_FORMATS]: createBulkRemoveItemHandler(section, '/customformat/bulk')
|
||||
},
|
||||
|
||||
//
|
||||
|
@ -102,7 +126,9 @@ export default {
|
|||
newState.pendingChanges = pendingChanges;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
},
|
||||
|
||||
[SET_MANAGE_CUSTOM_FORMATS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -96,8 +96,8 @@ export default {
|
|||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
name: ({ name }) => {
|
||||
return name.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -100,8 +100,8 @@ export default {
|
|||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
name: ({ name }) => {
|
||||
return name.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
86
frontend/src/Store/Actions/albumSelectionActions.js
Normal file
86
frontend/src/Store/Actions/albumSelectionActions.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
import moment from 'moment';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'albumSelection';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isReprocessing: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
sortKey: 'title',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
items: [],
|
||||
sortPredicates: {
|
||||
title: ({ title }) => {
|
||||
return title.toLocaleLowerCase();
|
||||
},
|
||||
|
||||
releaseDate: function({ releaseDate }, direction) {
|
||||
if (releaseDate) {
|
||||
return moment(releaseDate).unix();
|
||||
}
|
||||
|
||||
if (direction === sortDirections.DESCENDING) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'albumSelection.sortKey',
|
||||
'albumSelection.sortDirection'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_ALBUMS = 'albumSelection/fetchAlbums';
|
||||
export const SET_ALBUMS_SORT = 'albumSelection/setAlbumsSort';
|
||||
export const CLEAR_ALBUMS = 'albumSelection/clearAlbums';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchAlbums = createThunk(FETCH_ALBUMS);
|
||||
export const setAlbumsSort = createAction(SET_ALBUMS_SORT);
|
||||
export const clearAlbums = createAction(CLEAR_ALBUMS);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_ALBUMS]: createFetchHandler(section, '/album')
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[SET_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||
|
||||
[CLEAR_ALBUMS]: (state) => {
|
||||
return updateSectionState(state, section, {
|
||||
...defaultState,
|
||||
sortKey: state.sortKey,
|
||||
sortDirection: state.sortDirection
|
||||
});
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
|
@ -151,7 +151,7 @@ export const defaultState = {
|
|||
{
|
||||
name: 'genres',
|
||||
label: () => translate('Genres'),
|
||||
isSortable: false,
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
|
|
|
@ -150,7 +150,7 @@ export const defaultState = {
|
|||
},
|
||||
{
|
||||
key: 'importFailed',
|
||||
label: () => translate('ImportFailed'),
|
||||
label: () => translate('ImportCompleteFailed'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as albums from './albumActions';
|
||||
import * as albumHistory from './albumHistoryActions';
|
||||
import * as albumSelection from './albumSelectionActions';
|
||||
import * as app from './appActions';
|
||||
import * as artist from './artistActions';
|
||||
import * as artistHistory from './artistHistoryActions';
|
||||
|
@ -29,14 +30,18 @@ import * as wanted from './wantedActions';
|
|||
|
||||
export default [
|
||||
app,
|
||||
albums,
|
||||
albumHistory,
|
||||
albumSelection,
|
||||
artist,
|
||||
artistHistory,
|
||||
artistIndex,
|
||||
blocklist,
|
||||
captcha,
|
||||
calendar,
|
||||
commands,
|
||||
customFilters,
|
||||
albums,
|
||||
trackFiles,
|
||||
albumHistory,
|
||||
history,
|
||||
interactiveImportActions,
|
||||
oAuth,
|
||||
|
@ -47,9 +52,6 @@ export default [
|
|||
providerOptions,
|
||||
queue,
|
||||
releases,
|
||||
artist,
|
||||
artistHistory,
|
||||
artistIndex,
|
||||
search,
|
||||
settings,
|
||||
system,
|
||||
|
|
|
@ -16,7 +16,6 @@ import createSetClientSideCollectionSortReducer from './Creators/Reducers/create
|
|||
|
||||
export const section = 'interactiveImport';
|
||||
|
||||
const albumsSection = `${section}.albums`;
|
||||
const trackFilesSection = `${section}.trackFiles`;
|
||||
let abortCurrentFetchRequest = null;
|
||||
let abortCurrentRequest = null;
|
||||
|
@ -58,15 +57,6 @@ export const defaultState = {
|
|||
}
|
||||
},
|
||||
|
||||
albums: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
sortKey: 'albumTitle',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
items: []
|
||||
},
|
||||
|
||||
trackFiles: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
|
@ -97,10 +87,6 @@ export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
|
|||
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
|
||||
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
|
||||
|
||||
export const FETCH_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/fetchInteractiveImportAlbums';
|
||||
export const SET_INTERACTIVE_IMPORT_ALBUMS_SORT = 'interactiveImport/clearInteractiveImportAlbumsSort';
|
||||
export const CLEAR_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/clearInteractiveImportAlbums';
|
||||
|
||||
export const FETCH_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/fetchInteractiveImportTrackFiles';
|
||||
export const CLEAR_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/clearInteractiveImportTrackFiles';
|
||||
|
||||
|
@ -117,10 +103,6 @@ export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
|
|||
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
|
||||
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
|
||||
|
||||
export const fetchInteractiveImportAlbums = createThunk(FETCH_INTERACTIVE_IMPORT_ALBUMS);
|
||||
export const setInteractiveImportAlbumsSort = createAction(SET_INTERACTIVE_IMPORT_ALBUMS_SORT);
|
||||
export const clearInteractiveImportAlbums = createAction(CLEAR_INTERACTIVE_IMPORT_ALBUMS);
|
||||
|
||||
export const fetchInteractiveImportTrackFiles = createThunk(FETCH_INTERACTIVE_IMPORT_TRACKFILES);
|
||||
export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_IMPORT_TRACKFILES);
|
||||
|
||||
|
@ -253,8 +235,6 @@ export const actionHandlers = handleThunks({
|
|||
});
|
||||
},
|
||||
|
||||
[FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler(albumsSection, '/album'),
|
||||
|
||||
[FETCH_INTERACTIVE_IMPORT_TRACKFILES]: createFetchHandler(trackFilesSection, '/trackFile')
|
||||
});
|
||||
|
||||
|
@ -336,14 +316,6 @@ export const reducers = createHandleActions({
|
|||
return Object.assign({}, state, { importMode: payload.importMode });
|
||||
},
|
||||
|
||||
[SET_INTERACTIVE_IMPORT_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(albumsSection),
|
||||
|
||||
[CLEAR_INTERACTIVE_IMPORT_ALBUMS]: (state) => {
|
||||
return updateSectionState(state, albumsSection, {
|
||||
...defaultState.albums
|
||||
});
|
||||
},
|
||||
|
||||
[CLEAR_INTERACTIVE_IMPORT_TRACKFILES]: (state) => {
|
||||
return updateSectionState(state, trackFilesSection, {
|
||||
...defaultState.trackFiles
|
||||
|
|
|
@ -52,6 +52,12 @@ export const defaultState = {
|
|||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'albums.lastSearchTime',
|
||||
label: () => translate('LastSearched'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
// {
|
||||
// name: 'status',
|
||||
// label: 'Status',
|
||||
|
@ -131,6 +137,12 @@ export const defaultState = {
|
|||
// label: 'Status',
|
||||
// isVisible: true
|
||||
// },
|
||||
{
|
||||
name: 'albums.lastSearchTime',
|
||||
label: () => translate('LastSearched'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: () => translate('Actions'),
|
||||
|
|
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