mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-05 13:40:08 -05:00
Compare commits
245 Commits
v1.7.1.368
...
v1.9.3.401
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c664eaa9b5 | ||
|
|
b7e57f0c08 | ||
|
|
c06bf0e4ea | ||
|
|
c6db30c35a | ||
|
|
75c30dd318 | ||
|
|
6e7bf55dbd | ||
|
|
eb642dd2f9 | ||
|
|
19a196e2c7 | ||
|
|
93ec6cf89b | ||
|
|
52c6b56a4c | ||
|
|
82688d8a55 | ||
|
|
c81cbc801a | ||
|
|
993d189c61 | ||
|
|
1901af5a51 | ||
|
|
c1b399be39 | ||
|
|
2100e96570 | ||
|
|
3ff144421d | ||
|
|
f37ccba3f9 | ||
|
|
181cb2e0fe | ||
|
|
93c81bb7d3 | ||
|
|
7dd289b5f9 | ||
|
|
09cef8cf94 | ||
|
|
ca08c818e6 | ||
|
|
3e95bc4056 | ||
|
|
e241112915 | ||
|
|
0d98c12fa2 | ||
|
|
a0bcf5c9ae | ||
|
|
e318a47b3a | ||
|
|
b8df720c6c | ||
|
|
9625be723d | ||
|
|
d4b037db78 | ||
|
|
add2988789 | ||
|
|
9869c2272a | ||
|
|
4c8b0c9eec | ||
|
|
43cb22ff2b | ||
|
|
3cabc0589a | ||
|
|
cdb3ed36f6 | ||
|
|
840f2ae3e6 | ||
|
|
3ed6ef0336 | ||
|
|
c2ae0cce03 | ||
|
|
934b908b37 | ||
|
|
6c831f11a6 | ||
|
|
9adbfd2391 | ||
|
|
4a7cc82f0d | ||
|
|
c061c309bd | ||
|
|
0f3a77c336 | ||
|
|
478d5a624f | ||
|
|
3283d144f5 | ||
|
|
1a9ec4febd | ||
|
|
0598211319 | ||
|
|
0b0d6b7590 | ||
|
|
86cec51ebe | ||
|
|
80e5ac4aa9 | ||
|
|
ee5ed0c91b | ||
|
|
ba278930ed | ||
|
|
6449b89eb6 | ||
|
|
73b85e240e | ||
|
|
6338460ff4 | ||
|
|
0463e66881 | ||
|
|
bd75621437 | ||
|
|
9615c1183d | ||
|
|
bbf042ed55 | ||
|
|
98e948dbb2 | ||
|
|
2af9f7eb8d | ||
|
|
96413f99c7 | ||
|
|
d44b946d30 | ||
|
|
fe9cad5697 | ||
|
|
098be3cff6 | ||
|
|
8f2fea0be8 | ||
|
|
8d035c6c1f | ||
|
|
7dbfa74c40 | ||
|
|
caaf50ed9c | ||
|
|
b472a022a6 | ||
|
|
0a439a4a96 | ||
|
|
4410636b97 | ||
|
|
ba3ebc7574 | ||
|
|
2ce49a0785 | ||
|
|
d7df946c2b | ||
|
|
3dd3c80b54 | ||
|
|
0f160707d3 | ||
|
|
b608e38454 | ||
|
|
c873b3ffac | ||
|
|
07b98f4137 | ||
|
|
09606af351 | ||
|
|
1d79b92fca | ||
|
|
fbcf1b03c5 | ||
|
|
dee98ac46f | ||
|
|
4267b8a244 | ||
|
|
00dc55996c | ||
|
|
b912cc6110 | ||
|
|
56f0c137f8 | ||
|
|
1b8ff9b989 | ||
|
|
bfecf35a8b | ||
|
|
80da5ce165 | ||
|
|
60ca0db26f | ||
|
|
288a3d1495 | ||
|
|
4c42907eb2 | ||
|
|
6300eb1442 | ||
|
|
e4c0edf24c | ||
|
|
74a9fa784a | ||
|
|
1b0c9adf24 | ||
|
|
0eaa538e8a | ||
|
|
39a54eb8f6 | ||
|
|
5ad6237785 | ||
|
|
9fee4f914f | ||
|
|
ba2aab6bb3 | ||
|
|
5c8ae82f11 | ||
|
|
bcbeac1e83 | ||
|
|
b36d793d85 | ||
|
|
b0162ccc5b | ||
|
|
f0892eb4b8 | ||
|
|
e456979467 | ||
|
|
66ca47b615 | ||
|
|
2b7771bfe0 | ||
|
|
955bc472a1 | ||
|
|
e024bba6b6 | ||
|
|
aeb3b7d8b5 | ||
|
|
a7b25b8b93 | ||
|
|
130257fdd4 | ||
|
|
b618f23bc0 | ||
|
|
a758161e31 | ||
|
|
27928103c5 | ||
|
|
d5b3961e8a | ||
|
|
307adf053e | ||
|
|
31261f66ad | ||
|
|
5dbb59dfaa | ||
|
|
25c1803d0e | ||
|
|
9f4c9d3344 | ||
|
|
dfb00d9bb1 | ||
|
|
f7727855b5 | ||
|
|
1e4c67dcdb | ||
|
|
26afcb0071 | ||
|
|
7a937e85a4 | ||
|
|
7cd82321b4 | ||
|
|
8c9adba516 | ||
|
|
03fa9254e3 | ||
|
|
e66ecf5c95 | ||
|
|
e0dddfa215 | ||
|
|
bcb8afadf8 | ||
|
|
fc4a0979c3 | ||
|
|
5f643b2ced | ||
|
|
6f09b0f4f5 | ||
|
|
95c2531107 | ||
|
|
f83828cc22 | ||
|
|
cdea548ce2 | ||
|
|
cae1da0ce2 | ||
|
|
765f354c51 | ||
|
|
5cbbffb018 | ||
|
|
b2c5448cbf | ||
|
|
3dae84705c | ||
|
|
2321d278d6 | ||
|
|
ea73466f6a | ||
|
|
6961c5a1c6 | ||
|
|
141f1597dc | ||
|
|
1100f350ae | ||
|
|
3c5eefc349 | ||
|
|
0bfb557470 | ||
|
|
c93d6cff63 | ||
|
|
7e4980b855 | ||
|
|
419ef4b3bf | ||
|
|
c56d49ab60 | ||
|
|
1a40924db3 | ||
|
|
d55906d49a | ||
|
|
bc53fab966 | ||
|
|
d897b50f80 | ||
|
|
cc66cee71c | ||
|
|
f5e96f3f51 | ||
|
|
d52e1259a1 | ||
|
|
72e6d66269 | ||
|
|
e51b85449d | ||
|
|
efd5e92ca5 | ||
|
|
d153746a98 | ||
|
|
a1927e1e0f | ||
|
|
630a4ce800 | ||
|
|
8b1dd78300 | ||
|
|
cab50b35aa | ||
|
|
eee1be784b | ||
|
|
269dc5688b | ||
|
|
9bed795c89 | ||
|
|
3b5f151252 | ||
|
|
b3a541c9ff | ||
|
|
bc90fa2d3f | ||
|
|
4b0a896434 | ||
|
|
6be0e08635 | ||
|
|
f618901048 | ||
|
|
809ed940e6 | ||
|
|
7b14c2ee66 | ||
|
|
4528d03931 | ||
|
|
e0b30d34b1 | ||
|
|
8edf483e69 | ||
|
|
cea6aae9e1 | ||
|
|
1697cee680 | ||
|
|
ce8c90a125 | ||
|
|
c8ad3d6edd | ||
|
|
ebe01913c2 | ||
|
|
07cb19f9f3 | ||
|
|
7f51c44829 | ||
|
|
07f816f9fd | ||
|
|
a4a50b880c | ||
|
|
79361d92cb | ||
|
|
ecda75152e | ||
|
|
37a4e7c228 | ||
|
|
1a66d23bfe | ||
|
|
a26aa4bd1e | ||
|
|
a5d83459e9 | ||
|
|
4bfaab4b21 | ||
|
|
5764950b10 | ||
|
|
470b57316a | ||
|
|
f546b9a3b0 | ||
|
|
cc28c90e39 | ||
|
|
6e21e892bc | ||
|
|
62d868f0e9 | ||
|
|
27b36fe501 | ||
|
|
fc80efd15f | ||
|
|
9b75ba6ca0 | ||
|
|
d42649c4df | ||
|
|
53adfb750c | ||
|
|
ac487f9b40 | ||
|
|
6dd354bf1a | ||
|
|
b747d0a321 | ||
|
|
0e6cec6f54 | ||
|
|
65cf7c1009 | ||
|
|
5f9c3585f4 | ||
|
|
a9d1d4be90 | ||
|
|
a94ed11b21 | ||
|
|
3fab8fb0db | ||
|
|
5e52627799 | ||
|
|
b9a28f243e | ||
|
|
146e7ca7b6 | ||
|
|
1488fb7570 | ||
|
|
0fc52ae16f | ||
|
|
5218bea705 | ||
|
|
ac33330c7c | ||
|
|
041a7c571f | ||
|
|
5d73c6aa91 | ||
|
|
ef9a3a4f2a | ||
|
|
3ce3f8acdd | ||
|
|
9bac2992b5 | ||
|
|
4a88b70f40 | ||
|
|
c9b1d0d958 | ||
|
|
a5b5e7a3a5 | ||
|
|
376202e2af | ||
|
|
6b698b33be | ||
|
|
1706728230 | ||
|
|
cb520b2264 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Bug Report
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first'
|
||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -6,6 +6,3 @@ contact_links:
|
||||
- name: Support via Discord
|
||||
url: https://prowlarr.com/discord
|
||||
about: Chat with users and devs on support and setup related topics.
|
||||
- name: Support via Reddit
|
||||
url: https://reddit.com/r/prowlarr
|
||||
about: Discuss and search thru support topics.
|
||||
|
||||
3
.github/label-actions.yml
vendored
3
.github/label-actions.yml
vendored
@@ -4,8 +4,7 @@
|
||||
comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord)
|
||||
or [Subreddit](https://reddit.com/r/prowlarr)
|
||||
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord).
|
||||
close: true
|
||||
close-reason: 'not planned'
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas
|
||||
|
||||
[](https://wiki.servarr.com/prowlarr)
|
||||
[](https://prowlarr.com/discord)
|
||||
[](https://www.reddit.com/r/Prowlarr)
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.7.1'
|
||||
majorVersion: '1.9.3'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.408'
|
||||
dotnetVersion: '6.0.413'
|
||||
nodeVersion: '16.X'
|
||||
innoVersion: '6.2.0'
|
||||
nodeVersion: '16.x'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
@@ -27,6 +27,10 @@ trigger:
|
||||
include:
|
||||
- develop
|
||||
- master
|
||||
paths:
|
||||
exclude:
|
||||
- .github
|
||||
- src/Prowlarr.Api.*/openapi.json
|
||||
|
||||
pr:
|
||||
branches:
|
||||
@@ -34,8 +38,9 @@ pr:
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- .github
|
||||
- src/NzbDrone.Core/Localization/Core
|
||||
- src/Prowlarr.API.*/openapi.json
|
||||
- src/Prowlarr.Api.*/openapi.json
|
||||
|
||||
stages:
|
||||
- stage: Setup
|
||||
@@ -349,7 +354,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create FreeBSD Core Core tar
|
||||
displayName: Create freebsd-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).freebsd-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -528,8 +533,8 @@ stages:
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres
|
||||
displayName: Unit Native LinuxCore with Postgres Database
|
||||
- job: Unit_LinuxCore_Postgres14
|
||||
displayName: Unit Native LinuxCore with Postgres14 Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
@@ -565,6 +570,7 @@ stages:
|
||||
-e POSTGRES_PASSWORD=prowlarr \
|
||||
-e POSTGRES_USER=prowlarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
@@ -577,7 +583,60 @@ stages:
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
||||
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres15
|
||||
displayName: Unit Native LinuxCore with Postgres15 Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
|
||||
artifactName: linux-x64-tests
|
||||
Prowlarr__Postgres__Host: 'localhost'
|
||||
Prowlarr__Postgres__Port: '5432'
|
||||
Prowlarr__Postgres__User: 'prowlarr'
|
||||
Prowlarr__Postgres__Password: 'prowlarr'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: $(artifactName)
|
||||
targetPath: $(testsFolder)
|
||||
- bash: find ${TESTSFOLDER} -name "Prowlarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
docker run -d --name=postgres15 \
|
||||
-e POSTGRES_PASSWORD=prowlarr \
|
||||
-e POSTGRES_USER=prowlarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:15
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
ls -lR ${TESTSFOLDER}
|
||||
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||
displayName: Run Tests
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish Test Results
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- stage: Integration
|
||||
@@ -663,8 +722,8 @@ stages:
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_LinuxCore_Postgres
|
||||
displayName: Integration Native LinuxCore with Postgres Database
|
||||
- job: Integration_LinuxCore_Postgres14
|
||||
displayName: Integration Native LinuxCore with Postgres14 Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
@@ -710,6 +769,7 @@ stages:
|
||||
-e POSTGRES_PASSWORD=prowlarr \
|
||||
-e POSTGRES_USER=prowlarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
@@ -720,7 +780,70 @@ stages:
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
||||
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
|
||||
- job: Integration_LinuxCore_Postgres15
|
||||
displayName: Integration Native LinuxCore with Postgres Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
|
||||
Prowlarr__Postgres__Host: 'localhost'
|
||||
Prowlarr__Postgres__Port: '5432'
|
||||
Prowlarr__Postgres__User: 'prowlarr'
|
||||
Prowlarr__Postgres__Password: 'prowlarr'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'linux-x64-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: Packages
|
||||
itemPattern: '**/$(pattern)'
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||
displayName: Extract Package
|
||||
- bash: |
|
||||
mkdir -p ./bin/
|
||||
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Prowlarr/. ./bin/
|
||||
displayName: Move Package Contents
|
||||
- bash: |
|
||||
docker run -d --name=postgres15 \
|
||||
-e POSTGRES_PASSWORD=prowlarr \
|
||||
-e POSTGRES_USER=prowlarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:15
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||
displayName: Run Integration Tests
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
@@ -1003,7 +1126,7 @@ stages:
|
||||
git add .
|
||||
if git status | grep -q modified
|
||||
then
|
||||
git commit -am 'Automated API Docs update [skip ci]'
|
||||
git commit -am 'Automated API Docs update'
|
||||
git push -f --set-upstream origin api-docs
|
||||
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/prowlarr/prowlarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
|
||||
else
|
||||
|
||||
13
build.sh
13
build.sh
@@ -392,22 +392,21 @@ then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$FRONTEND" = "YES" ];
|
||||
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
|
||||
then
|
||||
YarnInstall
|
||||
RunWebpack
|
||||
fi
|
||||
|
||||
if [ "$LINT" = "YES" ];
|
||||
then
|
||||
if [ -z "$FRONTEND" ];
|
||||
then
|
||||
YarnInstall
|
||||
fi
|
||||
|
||||
LintUI
|
||||
fi
|
||||
|
||||
if [ "$FRONTEND" = "YES" ];
|
||||
then
|
||||
RunWebpack
|
||||
fi
|
||||
|
||||
if [ "$PACKAGES" = "YES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
|
||||
@@ -26,7 +26,8 @@ module.exports = {
|
||||
globals: {
|
||||
expect: false,
|
||||
chai: false,
|
||||
sinon: false
|
||||
sinon: false,
|
||||
JSX: true
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
|
||||
@@ -4,14 +4,14 @@ module.exports = {
|
||||
plugins: [
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-proposal-optional-chaining', { loose }],
|
||||
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
|
||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||
['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
|
||||
|
||||
// Stage 2
|
||||
'@babel/plugin-proposal-export-namespace-from',
|
||||
'@babel/plugin-transform-export-namespace-from',
|
||||
|
||||
// Stage 3
|
||||
['@babel/plugin-proposal-class-properties', { loose }],
|
||||
['@babel/plugin-transform-class-properties', { loose }],
|
||||
'@babel/plugin-syntax-dynamic-import'
|
||||
],
|
||||
env: {
|
||||
|
||||
@@ -35,7 +35,7 @@ module.exports = (env) => {
|
||||
},
|
||||
|
||||
entry: {
|
||||
index: 'index.js'
|
||||
index: 'index.ts'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
@@ -65,23 +65,23 @@ module.exports = (env) => {
|
||||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: '[name].js',
|
||||
filename: '[name]-[contenthash].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
optimization: {
|
||||
moduleIds: 'deterministic',
|
||||
chunkIds: 'named',
|
||||
splitChunks: {
|
||||
chunks: 'initial',
|
||||
name: 'vendors'
|
||||
}
|
||||
chunkIds: isProduction ? 'deterministic' : 'named'
|
||||
},
|
||||
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
|
||||
experiments: {
|
||||
topLevelAwait: true
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProduction,
|
||||
@@ -89,13 +89,15 @@ module.exports = (env) => {
|
||||
}),
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css'
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'frontend/src/index.ejs',
|
||||
filename: 'index.html',
|
||||
publicPath: '/'
|
||||
publicPath: '/',
|
||||
inject: false
|
||||
}),
|
||||
|
||||
new FileManagerPlugin({
|
||||
|
||||
@@ -5,9 +5,9 @@ import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import HistoryConnector from 'History/HistoryConnector';
|
||||
import IndexerIndex from 'Indexer/Index/IndexerIndex';
|
||||
import StatsConnector from 'Indexer/Stats/StatsConnector';
|
||||
import IndexerStats from 'Indexer/Stats/IndexerStats';
|
||||
import SearchIndexConnector from 'Search/SearchIndexConnector';
|
||||
import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSettingsConnector';
|
||||
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
|
||||
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||
@@ -60,7 +60,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/indexers/stats"
|
||||
component={StatsConnector}
|
||||
component={IndexerStats}
|
||||
/>
|
||||
|
||||
{/*
|
||||
@@ -98,7 +98,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/settings/applications"
|
||||
component={ApplicationSettingsConnector}
|
||||
component={ApplicationSettings}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.version {
|
||||
margin: 0 3px;
|
||||
font-weight: bold;
|
||||
font-family: var(--defaultFontFamily);
|
||||
}
|
||||
|
||||
.maintenance {
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
@@ -64,12 +65,12 @@ function AppUpdatedModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Prowlarr Updated
|
||||
{translate('AppUpdated', { appName: 'Prowlarr' })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
Version <span className={styles.version}>{version}</span> of Prowlarr has been installed, in order to get the latest changes you'll need to reload Prowlarr.
|
||||
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Prowlarr', version })} blockClassName={styles.version} />
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -77,16 +78,14 @@ function AppUpdatedModalContent(props) {
|
||||
<div>
|
||||
{
|
||||
!update.changes &&
|
||||
<div className={styles.maintenance}>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!update.changes &&
|
||||
<div>
|
||||
<div className={styles.changes}>
|
||||
What's new?
|
||||
{translate('WhatsNew')}
|
||||
</div>
|
||||
|
||||
<UpdateChanges
|
||||
@@ -113,14 +112,14 @@ function AppUpdatedModalContent(props) {
|
||||
<Button
|
||||
onPress={onSeeChangesPress}
|
||||
>
|
||||
Recent Changes
|
||||
{translate('RecentChanges')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Reload
|
||||
{translate('Reload')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('ConnectionLostMessage')}
|
||||
{translate('ConnectionLostToBackend', { appName: 'Prowlarr' })}
|
||||
</div>
|
||||
|
||||
<div className={styles.automatic}>
|
||||
{translate('ConnectionLostAutomaticMessage')}
|
||||
{translate('ConnectionLostReconnect', { appName: 'Prowlarr' })}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
48
frontend/src/App/State/AppSectionState.ts
Normal file
48
frontend/src/App/State/AppSectionState.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppSectionDeleteState {
|
||||
isDeleting: boolean;
|
||||
deleteError: Error;
|
||||
}
|
||||
|
||||
export interface AppSectionSaveState {
|
||||
isSaving: boolean;
|
||||
saveError: Error;
|
||||
}
|
||||
|
||||
export interface PagedAppSectionState {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface AppSectionSchemaState<T> {
|
||||
isSchemaFetching: boolean;
|
||||
isSchemaPopulated: boolean;
|
||||
schemaError: Error;
|
||||
schema: {
|
||||
items: T[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppSectionItemState<T> {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
item: T;
|
||||
}
|
||||
|
||||
interface AppSectionState<T> {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
items: T[];
|
||||
sortKey: string;
|
||||
sortDirection: SortDirection;
|
||||
}
|
||||
|
||||
export default AppSectionState;
|
||||
58
frontend/src/App/State/AppState.ts
Normal file
58
frontend/src/App/State/AppState.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import IndexerAppState, {
|
||||
IndexerHistoryAppState,
|
||||
IndexerIndexAppState,
|
||||
IndexerStatusAppState,
|
||||
} from './IndexerAppState';
|
||||
import IndexerStatsAppState from './IndexerStatsAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
|
||||
interface FilterBuilderPropOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FilterBuilderProp<T> {
|
||||
name: string;
|
||||
label: string;
|
||||
type: string;
|
||||
valueType?: string;
|
||||
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
|
||||
}
|
||||
|
||||
export interface PropertyFilter {
|
||||
key: string;
|
||||
value: boolean | string | number | string[] | number[];
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
key: string;
|
||||
label: string;
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface CustomFilter {
|
||||
id: number;
|
||||
type: string;
|
||||
label: string;
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
indexerHistory: IndexerHistoryAppState;
|
||||
indexerIndex: IndexerIndexAppState;
|
||||
indexerStats: IndexerStatsAppState;
|
||||
indexerStatus: IndexerStatusAppState;
|
||||
indexers: IndexerAppState;
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
tags: TagsAppState;
|
||||
}
|
||||
|
||||
export default AppState;
|
||||
8
frontend/src/App/State/ClientSideCollectionAppState.ts
Normal file
8
frontend/src/App/State/ClientSideCollectionAppState.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { CustomFilter } from './AppState';
|
||||
|
||||
interface ClientSideCollectionAppState {
|
||||
totalItems: number;
|
||||
customFilters: CustomFilter[];
|
||||
}
|
||||
|
||||
export default ClientSideCollectionAppState;
|
||||
6
frontend/src/App/State/CommandAppState.ts
Normal file
6
frontend/src/App/State/CommandAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Command from 'Commands/Command';
|
||||
|
||||
export type CommandAppState = AppSectionState<Command>;
|
||||
|
||||
export default CommandAppState;
|
||||
10
frontend/src/App/State/HistoryAppState.ts
Normal file
10
frontend/src/App/State/HistoryAppState.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Column from 'Components/Table/Column';
|
||||
import History from 'typings/History';
|
||||
|
||||
interface HistoryAppState extends AppSectionState<History> {
|
||||
pageSize: number;
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
export default HistoryAppState;
|
||||
40
frontend/src/App/State/IndexerAppState.ts
Normal file
40
frontend/src/App/State/IndexerAppState.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import Column from 'Components/Table/Column';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import Indexer, { IndexerStatus } from 'Indexer/Indexer';
|
||||
import History from 'typings/History';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from './AppSectionState';
|
||||
import { Filter, FilterBuilderProp } from './AppState';
|
||||
|
||||
export interface IndexerIndexAppState {
|
||||
isTestingAll: boolean;
|
||||
sortKey: string;
|
||||
sortDirection: SortDirection;
|
||||
secondarySortKey: string;
|
||||
secondarySortDirection: SortDirection;
|
||||
view: string;
|
||||
|
||||
tableOptions: {
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
selectedFilterKey: string;
|
||||
filterBuilderProps: FilterBuilderProp<Indexer>[];
|
||||
filters: Filter[];
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
interface IndexerAppState
|
||||
extends AppSectionState<Indexer>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
itemMap: Record<number, number>;
|
||||
}
|
||||
|
||||
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
|
||||
|
||||
export type IndexerHistoryAppState = AppSectionState<History>;
|
||||
|
||||
export default IndexerAppState;
|
||||
13
frontend/src/App/State/IndexerStatsAppState.ts
Normal file
13
frontend/src/App/State/IndexerStatsAppState.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { AppSectionItemState } from 'App/State/AppSectionState';
|
||||
import { Filter, FilterBuilderProp } from 'App/State/AppState';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import { IndexerStats } from 'typings/IndexerStats';
|
||||
|
||||
export interface IndexerStatsAppState
|
||||
extends AppSectionItemState<IndexerStats> {
|
||||
filterBuilderProps: FilterBuilderProp<Indexer>[];
|
||||
selectedFilterKey: string;
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
export default IndexerStatsAppState;
|
||||
10
frontend/src/App/State/ReleaseAppState.ts
Normal file
10
frontend/src/App/State/ReleaseAppState.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Release from 'typings/Release';
|
||||
|
||||
interface ReleaseAppState
|
||||
extends AppSectionState<Release>,
|
||||
AppSectionDeleteState {}
|
||||
|
||||
export default ReleaseAppState;
|
||||
42
frontend/src/App/State/SettingsAppState.ts
Normal file
42
frontend/src/App/State/SettingsAppState.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Application from 'typings/Application';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import Notification from 'typings/Notification';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
|
||||
export interface AppProfileAppState
|
||||
extends AppSectionState<Application>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ApplicationAppState
|
||||
extends AppSectionState<Application>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface NotificationAppState
|
||||
extends AppSectionState<Notification>,
|
||||
AppSectionDeleteState {}
|
||||
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
appProfiles: AppProfileAppState;
|
||||
applications: ApplicationAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
notifications: NotificationAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
10
frontend/src/App/State/SystemAppState.ts
Normal file
10
frontend/src/App/State/SystemAppState.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
|
||||
interface SystemAppState {
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
28
frontend/src/App/State/TagsAppState.ts
Normal file
28
frontend/src/App/State/TagsAppState.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
export interface Tag extends ModelBase {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface TagDetail extends ModelBase {
|
||||
label: string;
|
||||
applicationIds: number[];
|
||||
indexerIds: number[];
|
||||
indexerProxyIds: number[];
|
||||
notificationIds: number[];
|
||||
}
|
||||
|
||||
export interface TagDetailAppState
|
||||
extends AppSectionState<TagDetail>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
|
||||
details: TagDetailAppState;
|
||||
}
|
||||
|
||||
export default TagsAppState;
|
||||
37
frontend/src/Commands/Command.ts
Normal file
37
frontend/src/Commands/Command.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export interface CommandBody {
|
||||
sendUpdatesToClient: boolean;
|
||||
updateScheduledTask: boolean;
|
||||
completionMessage: string;
|
||||
requiresDiskAccess: boolean;
|
||||
isExclusive: boolean;
|
||||
isLongRunning: boolean;
|
||||
name: string;
|
||||
lastExecutionTime: string;
|
||||
lastStartTime: string;
|
||||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
seriesId?: number;
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
name: string;
|
||||
commandName: string;
|
||||
message: string;
|
||||
body: CommandBody;
|
||||
priority: string;
|
||||
status: string;
|
||||
result: string;
|
||||
queued: string;
|
||||
started: string;
|
||||
ended: string;
|
||||
duration: string;
|
||||
trigger: string;
|
||||
stateChangeTime: string;
|
||||
sendUpdatesToClient: boolean;
|
||||
updateScheduledTask: boolean;
|
||||
lastExecutionTime: string;
|
||||
}
|
||||
|
||||
export default Command;
|
||||
@@ -2,6 +2,7 @@ import Chart from 'chart.js/auto';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { defaultFontFamily } from 'Styles/Variables/fonts';
|
||||
|
||||
function getColors(kind) {
|
||||
|
||||
@@ -39,7 +40,15 @@ class BarChart extends Component {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: this.props.title
|
||||
align: 'start',
|
||||
text: this.props.title,
|
||||
padding: {
|
||||
bottom: 30
|
||||
},
|
||||
font: {
|
||||
size: 14,
|
||||
family: defaultFontFamily
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: this.props.legend
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Chart from 'chart.js/auto';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { defaultFontFamily } from 'Styles/Variables/fonts';
|
||||
|
||||
function getColors(kind) {
|
||||
|
||||
@@ -22,7 +23,15 @@ class DoughnutChart extends Component {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: this.props.title
|
||||
align: 'start',
|
||||
text: this.props.title,
|
||||
padding: {
|
||||
bottom: 30
|
||||
},
|
||||
font: {
|
||||
size: 14,
|
||||
family: defaultFontFamily
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Chart from 'chart.js/auto';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { defaultFontFamily } from 'Styles/Variables/fonts';
|
||||
|
||||
function getColors(index) {
|
||||
|
||||
@@ -36,7 +37,15 @@ class StackedBarChart extends Component {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: this.props.title
|
||||
align: 'start',
|
||||
text: this.props.title,
|
||||
padding: {
|
||||
bottom: 30
|
||||
},
|
||||
font: {
|
||||
size: 14,
|
||||
family: defaultFontFamily
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,7 +23,9 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
||||
info,
|
||||
} = props;
|
||||
|
||||
const [detailedError, setDetailedError] = useState(null);
|
||||
const [detailedError, setDetailedError] = useState<
|
||||
StackTrace.StackFrame[] | null
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
|
||||
@@ -20,12 +20,12 @@ import styles from './FileBrowserModalContent.css';
|
||||
const columns = [
|
||||
{
|
||||
name: 'type',
|
||||
label: translate('Type'),
|
||||
label: () => translate('Type'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: translate('Name'),
|
||||
label: () => translate('Name'),
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -198,11 +198,13 @@ class FilterBuilderRow extends Component {
|
||||
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
|
||||
|
||||
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
|
||||
const { name, label } = availablePropFilter;
|
||||
|
||||
return {
|
||||
key: availablePropFilter.name,
|
||||
value: availablePropFilter.label
|
||||
key: name,
|
||||
value: typeof label === 'function' ? label() : label
|
||||
};
|
||||
});
|
||||
}).sort((a, b) => a.value.localeCompare(b.value));
|
||||
|
||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.indexers,
|
||||
(qualityProfiles) => {
|
||||
(indexers) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = qualityProfiles;
|
||||
} = indexers;
|
||||
|
||||
const tagList = items.map((item) => {
|
||||
return {
|
||||
|
||||
@@ -3,9 +3,24 @@ import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const privacyTypes = [
|
||||
{ id: 'public', name: translate('Public') },
|
||||
{ id: 'private', name: translate('Private') },
|
||||
{ id: 'semiPrivate', name: translate('SemiPrivate') }
|
||||
{
|
||||
id: 'public',
|
||||
get name() {
|
||||
return translate('Public');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'private',
|
||||
get name() {
|
||||
return translate('Private');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'semiPrivate',
|
||||
get name() {
|
||||
return translate('SemiPrivate');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function PrivacyFilterBuilderRowValue(props) {
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
&:hover {
|
||||
background-color: var(--inputHoverBackgroundColor);
|
||||
}
|
||||
|
||||
&.isDisabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.optionCheck {
|
||||
|
||||
@@ -270,6 +270,7 @@ FormInputGroup.propTypes = {
|
||||
helpTexts: PropTypes.arrayOf(PropTypes.string),
|
||||
helpTextWarning: PropTypes.string,
|
||||
helpLink: PropTypes.string,
|
||||
autoFocus: PropTypes.bool,
|
||||
includeNoChange: PropTypes.bool,
|
||||
includeNoChangeDisabled: PropTypes.bool,
|
||||
selectedValueOptions: PropTypes.object,
|
||||
|
||||
@@ -33,11 +33,11 @@ function HintedSelectInputOption(props) {
|
||||
isMobile && styles.isMobile
|
||||
)}
|
||||
>
|
||||
<div>{value}</div>
|
||||
<div>{typeof value === 'function' ? value() : value}</div>
|
||||
|
||||
{
|
||||
hint != null &&
|
||||
<div className={styles.hintText}>
|
||||
<div className={styles.hintText} title={hint}>
|
||||
{hint}
|
||||
</div>
|
||||
}
|
||||
@@ -48,7 +48,7 @@ function HintedSelectInputOption(props) {
|
||||
|
||||
HintedSelectInputOption.propTypes = {
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
hint: PropTypes.node,
|
||||
depth: PropTypes.number,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -24,7 +24,7 @@ function HintedSelectInputSelectedValue(props) {
|
||||
>
|
||||
<div className={styles.valueText}>
|
||||
{
|
||||
isMultiSelect &&
|
||||
isMultiSelect ?
|
||||
value.map((key, index) => {
|
||||
const v = valuesMap[key];
|
||||
return (
|
||||
@@ -32,26 +32,28 @@ function HintedSelectInputSelectedValue(props) {
|
||||
{v ? v.value : key}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}) :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isMultiSelect && value
|
||||
isMultiSelect ? null : value
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
hint != null && includeHint &&
|
||||
hint != null && includeHint ?
|
||||
<div className={styles.hintText}>
|
||||
{hint}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</EnhancedSelectInputSelectedValue>
|
||||
);
|
||||
}
|
||||
|
||||
HintedSelectInputSelectedValue.propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hint: PropTypes.string,
|
||||
isMultiSelect: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import _ from 'lodash';
|
||||
import { groupBy, map } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
(state) => state.indexers,
|
||||
createSortedSectionSelector('indexers', sortByName),
|
||||
(value, indexers) => {
|
||||
const values = [];
|
||||
const groupedIndexers = _.map(_.groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
|
||||
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
|
||||
|
||||
groupedIndexers.forEach((element) => {
|
||||
values.push({
|
||||
@@ -25,6 +27,7 @@ function createMapStateToProps() {
|
||||
values.push({
|
||||
key: indexer.id,
|
||||
value: indexer.name,
|
||||
hint: `(${indexer.id})`,
|
||||
isDisabled: !indexer.enable,
|
||||
parentKey: element.protocol === 'usenet' ? -1 : -2
|
||||
});
|
||||
@@ -50,7 +53,6 @@ class IndexersSelectInputConnector extends Component {
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
|
||||
@@ -10,7 +10,7 @@ function parseValue(props, value) {
|
||||
} = props;
|
||||
|
||||
if (value == null || value === '') {
|
||||
return min;
|
||||
return null;
|
||||
}
|
||||
|
||||
let newValue = isFloat ? parseFloat(value) : parseInt(value);
|
||||
@@ -41,7 +41,7 @@ class NumberInput extends Component {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { value } = this.props;
|
||||
|
||||
if (value !== prevProps.value && !this.state.isFocused) {
|
||||
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
|
||||
this.setState({
|
||||
value: value == null ? '' : value.toString()
|
||||
});
|
||||
|
||||
@@ -68,6 +68,7 @@ class PathInputConnector extends Component {
|
||||
}
|
||||
|
||||
PathInputConnector.propTypes = {
|
||||
...PathInput.props,
|
||||
includeFiles: PropTypes.bool.isRequired,
|
||||
dispatchFetchPaths: PropTypes.func.isRequired,
|
||||
dispatchClearPaths: PropTypes.func.isRequired
|
||||
|
||||
@@ -61,7 +61,7 @@ class SelectInput extends Component {
|
||||
value={key}
|
||||
{...otherOptionProps}
|
||||
>
|
||||
{optionValue}
|
||||
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||
</option>
|
||||
);
|
||||
})
|
||||
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
disabledClassName: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
|
||||
@@ -41,7 +41,7 @@ class Icon extends PureComponent {
|
||||
return (
|
||||
<span
|
||||
className={containerClassName}
|
||||
title={title}
|
||||
title={typeof title === 'function' ? title() : title}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
@@ -58,7 +58,7 @@ Icon.propTypes = {
|
||||
name: PropTypes.object.isRequired,
|
||||
kind: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
title: PropTypes.string,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
fixedWidth: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import styles from './Link.css';
|
||||
|
||||
class Link extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onClick = (event) => {
|
||||
const {
|
||||
isDisabled,
|
||||
onPress
|
||||
} = this.props;
|
||||
|
||||
if (!isDisabled && onPress) {
|
||||
onPress(event);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
component,
|
||||
to,
|
||||
target,
|
||||
isDisabled,
|
||||
noRouter,
|
||||
onPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const linkProps = { target };
|
||||
let el = component;
|
||||
|
||||
if (to) {
|
||||
if ((/\w+?:\/\//).test(to)) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_blank';
|
||||
linkProps.rel = 'noreferrer';
|
||||
} else if (noRouter) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else {
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
linkProps.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (el === 'button' || el === 'input') {
|
||||
linkProps.type = otherProps.type || 'button';
|
||||
linkProps.disabled = isDisabled;
|
||||
}
|
||||
|
||||
linkProps.className = classNames(
|
||||
className,
|
||||
styles.link,
|
||||
to && styles.to,
|
||||
isDisabled && 'isDisabled'
|
||||
);
|
||||
|
||||
const props = {
|
||||
...otherProps,
|
||||
...linkProps
|
||||
};
|
||||
|
||||
props.onClick = this.onClick;
|
||||
|
||||
return (
|
||||
React.createElement(el, props)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
className: PropTypes.string,
|
||||
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
to: PropTypes.string,
|
||||
target: PropTypes.string,
|
||||
isDisabled: PropTypes.bool,
|
||||
noRouter: PropTypes.bool,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
Link.defaultProps = {
|
||||
component: 'button',
|
||||
noRouter: false
|
||||
};
|
||||
|
||||
export default Link;
|
||||
96
frontend/src/Components/Link/Link.tsx
Normal file
96
frontend/src/Components/Link/Link.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
ComponentClass,
|
||||
FunctionComponent,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import styles from './Link.css';
|
||||
|
||||
interface ReactRouterLinkProps {
|
||||
to?: string;
|
||||
}
|
||||
|
||||
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
|
||||
className?: string;
|
||||
component?:
|
||||
| string
|
||||
| FunctionComponent<LinkProps>
|
||||
| ComponentClass<LinkProps, unknown>;
|
||||
to?: string;
|
||||
target?: string;
|
||||
isDisabled?: boolean;
|
||||
noRouter?: boolean;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
}
|
||||
function Link(props: LinkProps) {
|
||||
const {
|
||||
className,
|
||||
component = 'button',
|
||||
to,
|
||||
target,
|
||||
type,
|
||||
isDisabled,
|
||||
noRouter = false,
|
||||
onPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: SyntheticEvent) => {
|
||||
if (!isDisabled && onPress) {
|
||||
onPress(event);
|
||||
}
|
||||
},
|
||||
[isDisabled, onPress]
|
||||
);
|
||||
|
||||
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
|
||||
target,
|
||||
};
|
||||
let el = component;
|
||||
|
||||
if (to) {
|
||||
if (/\w+?:\/\//.test(to)) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_blank';
|
||||
linkProps.rel = 'noreferrer';
|
||||
} else if (noRouter) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
linkProps.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (el === 'button' || el === 'input') {
|
||||
linkProps.type = type || 'button';
|
||||
linkProps.disabled = isDisabled;
|
||||
}
|
||||
|
||||
linkProps.className = classNames(
|
||||
className,
|
||||
styles.link,
|
||||
to && styles.to,
|
||||
isDisabled && 'isDisabled'
|
||||
);
|
||||
|
||||
const elementProps = {
|
||||
...otherProps,
|
||||
type,
|
||||
...linkProps,
|
||||
};
|
||||
|
||||
elementProps.onClick = onClick;
|
||||
|
||||
return React.createElement(el, elementProps);
|
||||
}
|
||||
|
||||
export default Link;
|
||||
@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
kind,
|
||||
isSpinning,
|
||||
error,
|
||||
children,
|
||||
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
|
||||
const showIcon = wasSuccessful || hasWarning || hasError;
|
||||
|
||||
let iconName = icons.CHECK;
|
||||
let iconKind = kinds.SUCCESS;
|
||||
let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
|
||||
|
||||
if (hasWarning) {
|
||||
iconName = icons.WARNING;
|
||||
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
|
||||
|
||||
return (
|
||||
<SpinnerButton
|
||||
kind={kind}
|
||||
isSpinning={isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
|
||||
}
|
||||
|
||||
SpinnerErrorButton.propTypes = {
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
children: PropTypes.node.isRequired
|
||||
|
||||
@@ -10,27 +10,55 @@ class InlineMarkdown extends Component {
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
data
|
||||
data,
|
||||
blockClassName
|
||||
} = this.props;
|
||||
|
||||
// For now only replace links
|
||||
// For now only replace links or code blocks (not both)
|
||||
const markdownBlocks = [];
|
||||
if (data) {
|
||||
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
|
||||
const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
|
||||
|
||||
let endIndex = 0;
|
||||
let match = null;
|
||||
while ((match = regex.exec(data)) !== null) {
|
||||
|
||||
while ((match = linkRegex.exec(data)) !== null) {
|
||||
if (match.index > endIndex) {
|
||||
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
||||
}
|
||||
|
||||
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
|
||||
endIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (endIndex !== data.length) {
|
||||
if (endIndex !== data.length && markdownBlocks.length > 0) {
|
||||
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
||||
}
|
||||
|
||||
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
|
||||
|
||||
endIndex = 0;
|
||||
match = null;
|
||||
let matchedCode = false;
|
||||
|
||||
while ((match = codeRegex.exec(data)) !== null) {
|
||||
matchedCode = true;
|
||||
|
||||
if (match.index > endIndex) {
|
||||
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
||||
}
|
||||
|
||||
markdownBlocks.push(<code key={`code-${match.index}`} className={blockClassName ?? null}>{match[0].substring(1, match[0].length - 1)}</code>);
|
||||
endIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
|
||||
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
||||
}
|
||||
|
||||
if (markdownBlocks.length === 0) {
|
||||
markdownBlocks.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
return <span className={className}>{markdownBlocks}</span>;
|
||||
@@ -39,7 +67,8 @@ class InlineMarkdown extends Component {
|
||||
|
||||
InlineMarkdown.propTypes = {
|
||||
className: PropTypes.string,
|
||||
data: PropTypes.string
|
||||
data: PropTypes.string,
|
||||
blockClassName: PropTypes.string
|
||||
};
|
||||
|
||||
export default InlineMarkdown;
|
||||
|
||||
@@ -33,7 +33,7 @@ class FilterMenuContent extends Component {
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
{typeof filter.label === 'function' ? filter.label() : filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ function ErrorPage(props) {
|
||||
const {
|
||||
version,
|
||||
isLocalStorageSupported,
|
||||
translationsError,
|
||||
indexersError,
|
||||
indexerStatusError,
|
||||
indexerCategoriesError,
|
||||
@@ -21,6 +22,8 @@ function ErrorPage(props) {
|
||||
|
||||
if (!isLocalStorageSupported) {
|
||||
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
|
||||
} else if (translationsError) {
|
||||
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
|
||||
} else if (indexersError) {
|
||||
errorMessage = getErrorMessage(indexersError, 'Failed to load indexers from API');
|
||||
} else if (indexerStatusError) {
|
||||
@@ -55,6 +58,7 @@ function ErrorPage(props) {
|
||||
ErrorPage.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
isLocalStorageSupported: PropTypes.bool.isRequired,
|
||||
translationsError: PropTypes.object,
|
||||
indexersError: PropTypes.object,
|
||||
indexerStatusError: PropTypes.object,
|
||||
indexerCategoriesError: PropTypes.object,
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
import { fetchIndexers } from 'Store/Actions/indexerActions';
|
||||
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
|
||||
@@ -54,6 +54,7 @@ const selectIsPopulated = createSelector(
|
||||
(state) => state.indexerStatus.isPopulated,
|
||||
(state) => state.settings.indexerCategories.isPopulated,
|
||||
(state) => state.system.status.isPopulated,
|
||||
(state) => state.app.translations.isPopulated,
|
||||
(
|
||||
customFiltersIsPopulated,
|
||||
tagsIsPopulated,
|
||||
@@ -63,7 +64,8 @@ const selectIsPopulated = createSelector(
|
||||
indexersIsPopulated,
|
||||
indexerStatusIsPopulated,
|
||||
indexerCategoriesIsPopulated,
|
||||
systemStatusIsPopulated
|
||||
systemStatusIsPopulated,
|
||||
translationsIsPopulated
|
||||
) => {
|
||||
return (
|
||||
customFiltersIsPopulated &&
|
||||
@@ -74,7 +76,8 @@ const selectIsPopulated = createSelector(
|
||||
indexersIsPopulated &&
|
||||
indexerStatusIsPopulated &&
|
||||
indexerCategoriesIsPopulated &&
|
||||
systemStatusIsPopulated
|
||||
systemStatusIsPopulated &&
|
||||
translationsIsPopulated
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -89,6 +92,7 @@ const selectErrors = createSelector(
|
||||
(state) => state.indexerStatus.error,
|
||||
(state) => state.settings.indexerCategories.error,
|
||||
(state) => state.system.status.error,
|
||||
(state) => state.app.translations.error,
|
||||
(
|
||||
customFiltersError,
|
||||
tagsError,
|
||||
@@ -98,7 +102,8 @@ const selectErrors = createSelector(
|
||||
indexersError,
|
||||
indexerStatusError,
|
||||
indexerCategoriesError,
|
||||
systemStatusError
|
||||
systemStatusError,
|
||||
translationsError
|
||||
) => {
|
||||
const hasError = !!(
|
||||
customFiltersError ||
|
||||
@@ -109,7 +114,8 @@ const selectErrors = createSelector(
|
||||
indexersError ||
|
||||
indexerStatusError ||
|
||||
indexerCategoriesError ||
|
||||
systemStatusError
|
||||
systemStatusError ||
|
||||
translationsError
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -122,7 +128,8 @@ const selectErrors = createSelector(
|
||||
indexersError,
|
||||
indexerStatusError,
|
||||
indexerCategoriesError,
|
||||
systemStatusError
|
||||
systemStatusError,
|
||||
translationsError
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -184,6 +191,9 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatchFetchStatus() {
|
||||
dispatch(fetchStatus());
|
||||
},
|
||||
dispatchFetchTranslations() {
|
||||
dispatch(fetchTranslations());
|
||||
},
|
||||
onResize(dimensions) {
|
||||
dispatch(saveDimensions(dimensions));
|
||||
},
|
||||
@@ -217,6 +227,7 @@ class PageConnector extends Component {
|
||||
this.props.dispatchFetchUISettings();
|
||||
this.props.dispatchFetchGeneralSettings();
|
||||
this.props.dispatchFetchStatus();
|
||||
this.props.dispatchFetchTranslations();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +253,7 @@ class PageConnector extends Component {
|
||||
dispatchFetchUISettings,
|
||||
dispatchFetchGeneralSettings,
|
||||
dispatchFetchStatus,
|
||||
dispatchFetchTranslations,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -282,6 +294,7 @@ PageConnector.propTypes = {
|
||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||
dispatchFetchTranslations: PropTypes.func.isRequired,
|
||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import React, { forwardRef, ReactNode, useCallback } from 'react';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react';
|
||||
import Scroller, { OnScroll } from 'Components/Scroller/Scroller';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
||||
interface PageContentBodyProps {
|
||||
className: string;
|
||||
innerClassName: string;
|
||||
className?: string;
|
||||
innerClassName?: string;
|
||||
children: ReactNode;
|
||||
initialScrollTop?: number;
|
||||
onScroll?: (payload) => void;
|
||||
onScroll?: (payload: OnScroll) => void;
|
||||
}
|
||||
|
||||
const PageContentBody = forwardRef(
|
||||
(
|
||||
props: PageContentBodyProps,
|
||||
ref: React.MutableRefObject<HTMLDivElement>
|
||||
) => {
|
||||
(props: PageContentBodyProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const {
|
||||
className = styles.contentBody,
|
||||
innerClassName = styles.innerContentBody,
|
||||
@@ -26,7 +23,7 @@ const PageContentBody = forwardRef(
|
||||
} = props;
|
||||
|
||||
const onScrollWrapper = useCallback(
|
||||
(payload) => {
|
||||
(payload: OnScroll) => {
|
||||
if (onScroll && !isLocked()) {
|
||||
onScroll(payload);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.jumpBar {
|
||||
z-index: $pageJumpBarZIndex;
|
||||
display: flex;
|
||||
align-content: stretch;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -20,12 +20,12 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
|
||||
const links = [
|
||||
{
|
||||
iconName: icons.MOVIE_CONTINUING,
|
||||
title: translate('Indexers'),
|
||||
title: () => translate('Indexers'),
|
||||
to: '/',
|
||||
alias: '/indexers',
|
||||
children: [
|
||||
{
|
||||
title: translate('Stats'),
|
||||
title: () => translate('Stats'),
|
||||
to: '/indexers/stats'
|
||||
}
|
||||
]
|
||||
@@ -33,47 +33,47 @@ const links = [
|
||||
|
||||
{
|
||||
iconName: icons.SEARCH,
|
||||
title: translate('Search'),
|
||||
title: () => translate('Search'),
|
||||
to: '/search'
|
||||
},
|
||||
|
||||
{
|
||||
iconName: icons.ACTIVITY,
|
||||
title: translate('History'),
|
||||
title: () => translate('History'),
|
||||
to: '/history'
|
||||
},
|
||||
|
||||
{
|
||||
iconName: icons.SETTINGS,
|
||||
title: translate('Settings'),
|
||||
title: () => translate('Settings'),
|
||||
to: '/settings',
|
||||
children: [
|
||||
{
|
||||
title: translate('Indexers'),
|
||||
title: () => translate('Indexers'),
|
||||
to: '/settings/indexers'
|
||||
},
|
||||
{
|
||||
title: translate('Apps'),
|
||||
title: () => translate('Apps'),
|
||||
to: '/settings/applications'
|
||||
},
|
||||
{
|
||||
title: translate('DownloadClients'),
|
||||
title: () => translate('DownloadClients'),
|
||||
to: '/settings/downloadclients'
|
||||
},
|
||||
{
|
||||
title: translate('Connect'),
|
||||
title: () => translate('Connect'),
|
||||
to: '/settings/connect'
|
||||
},
|
||||
{
|
||||
title: translate('Tags'),
|
||||
title: () => translate('Tags'),
|
||||
to: '/settings/tags'
|
||||
},
|
||||
{
|
||||
title: translate('General'),
|
||||
title: () => translate('General'),
|
||||
to: '/settings/general'
|
||||
},
|
||||
{
|
||||
title: translate('UI'),
|
||||
title: () => translate('UI'),
|
||||
to: '/settings/ui'
|
||||
}
|
||||
]
|
||||
@@ -81,32 +81,32 @@ const links = [
|
||||
|
||||
{
|
||||
iconName: icons.SYSTEM,
|
||||
title: translate('System'),
|
||||
title: () => translate('System'),
|
||||
to: '/system/status',
|
||||
children: [
|
||||
{
|
||||
title: translate('Status'),
|
||||
title: () => translate('Status'),
|
||||
to: '/system/status',
|
||||
statusComponent: HealthStatusConnector
|
||||
},
|
||||
{
|
||||
title: translate('Tasks'),
|
||||
title: () => translate('Tasks'),
|
||||
to: '/system/tasks'
|
||||
},
|
||||
{
|
||||
title: translate('Backup'),
|
||||
title: () => translate('Backup'),
|
||||
to: '/system/backup'
|
||||
},
|
||||
{
|
||||
title: translate('Updates'),
|
||||
title: () => translate('Updates'),
|
||||
to: '/system/updates'
|
||||
},
|
||||
{
|
||||
title: translate('Events'),
|
||||
title: () => translate('Events'),
|
||||
to: '/system/events'
|
||||
},
|
||||
{
|
||||
title: translate('LogFiles'),
|
||||
title: () => translate('LogFiles'),
|
||||
to: '/system/logs/files'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
|
||||
}
|
||||
|
||||
<span className={isChildItem ? styles.noIcon : null}>
|
||||
{title}
|
||||
{typeof title === 'function' ? title() : title}
|
||||
</span>
|
||||
|
||||
{
|
||||
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
|
||||
|
||||
PageSidebarItem.propTypes = {
|
||||
iconName: PropTypes.object,
|
||||
title: PropTypes.string.isRequired,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
to: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool,
|
||||
isActiveParent: PropTypes.bool,
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import classNames from 'classnames';
|
||||
import { throttle } from 'lodash';
|
||||
import React, { forwardRef, ReactNode, useEffect, useRef } from 'react';
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
MutableRefObject,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import styles from './Scroller.css';
|
||||
|
||||
export interface OnScroll {
|
||||
scrollLeft: number;
|
||||
scrollTop: number;
|
||||
}
|
||||
|
||||
interface ScrollerProps {
|
||||
className?: string;
|
||||
scrollDirection?: ScrollDirection;
|
||||
@@ -12,11 +24,11 @@ interface ScrollerProps {
|
||||
scrollTop?: number;
|
||||
initialScrollTop?: number;
|
||||
children?: ReactNode;
|
||||
onScroll?: (payload) => void;
|
||||
onScroll?: (payload: OnScroll) => void;
|
||||
}
|
||||
|
||||
const Scroller = forwardRef(
|
||||
(props: ScrollerProps, ref: React.MutableRefObject<HTMLDivElement>) => {
|
||||
(props: ScrollerProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const {
|
||||
className,
|
||||
autoFocus = false,
|
||||
@@ -30,7 +42,7 @@ const Scroller = forwardRef(
|
||||
} = props;
|
||||
|
||||
const internalRef = useRef();
|
||||
const currentRef = ref ?? internalRef;
|
||||
const currentRef = (ref as MutableRefObject<HTMLDivElement>) ?? internalRef;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
|
||||
@@ -1,58 +1,66 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './RelativeDateCell.css';
|
||||
|
||||
class RelativeDateCell extends PureComponent {
|
||||
function createRelativeDateCellSelector() {
|
||||
return createSelector(createUISettingsSelector(), (uiSettings) => {
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function RelativeDateCell(props) {
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
date,
|
||||
includeSeconds,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
component: Component,
|
||||
dispatch,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
const {
|
||||
className,
|
||||
date,
|
||||
includeSeconds,
|
||||
component: Component,
|
||||
dispatch,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
if (!date) {
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||
useSelector(createRelativeDateCellSelector());
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
|
||||
{...otherProps}
|
||||
>
|
||||
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
|
||||
</Component>
|
||||
);
|
||||
if (!date) {
|
||||
return <Component className={className} {...otherProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
title={formatDateTime(date, longDateFormat, timeFormat, {
|
||||
includeSeconds,
|
||||
includeRelativeDay: !showRelativeDates
|
||||
})}
|
||||
{...otherProps}
|
||||
>
|
||||
{getRelativeDate(date, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
includeSeconds,
|
||||
timeForToday: true
|
||||
})}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
RelativeDateCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
date: PropTypes.string,
|
||||
includeSeconds: PropTypes.bool.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
component: PropTypes.elementType,
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'markAsFailedButton': string;
|
||||
'cell': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,25 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './TableRowCellButton.css';
|
||||
|
||||
function TableRowCellButton({ className, ...otherProps }) {
|
||||
return (
|
||||
<Link
|
||||
className={className}
|
||||
component={TableRowCell}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TableRowCellButton.propTypes = {
|
||||
className: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
TableRowCellButton.defaultProps = {
|
||||
className: styles.cell
|
||||
};
|
||||
|
||||
export default TableRowCellButton;
|
||||
19
frontend/src/Components/Table/Cells/TableRowCellButton.tsx
Normal file
19
frontend/src/Components/Table/Cells/TableRowCellButton.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import Link, { LinkProps } from 'Components/Link/Link';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './TableRowCellButton.css';
|
||||
|
||||
interface TableRowCellButtonProps extends LinkProps {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function TableRowCellButton(props: TableRowCellButtonProps) {
|
||||
const { className = styles.cell, ...otherProps } = props;
|
||||
|
||||
return (
|
||||
<Link className={className} component={TableRowCell} {...otherProps} />
|
||||
);
|
||||
}
|
||||
|
||||
export default TableRowCellButton;
|
||||
@@ -1,8 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'fullWidthChart': string;
|
||||
'halfWidthChart': string;
|
||||
'input': string;
|
||||
'selectCell': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
type PropertyFunction<T> = () => T;
|
||||
|
||||
interface Column {
|
||||
name: string;
|
||||
label: string;
|
||||
columnLabel: string;
|
||||
isSortable: boolean;
|
||||
label: string | PropertyFunction<string> | React.ReactNode;
|
||||
columnLabel?: string;
|
||||
isSortable?: boolean;
|
||||
isVisible: boolean;
|
||||
isModifiable?: boolean;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ function Table(props) {
|
||||
{...getTableHeaderCellProps(otherProps)}
|
||||
{...column}
|
||||
>
|
||||
{column.label}
|
||||
{typeof column.label === 'function' ? column.label() : column.label}
|
||||
</TableHeaderCell>
|
||||
);
|
||||
})
|
||||
@@ -121,6 +121,7 @@ function Table(props) {
|
||||
}
|
||||
|
||||
Table.propTypes = {
|
||||
...TableHeaderCell.props,
|
||||
className: PropTypes.string,
|
||||
horizontalScroll: PropTypes.bool.isRequired,
|
||||
selectAll: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
|
||||
const {
|
||||
className,
|
||||
name,
|
||||
label,
|
||||
columnLabel,
|
||||
isSortable,
|
||||
isVisible,
|
||||
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
|
||||
{...otherProps}
|
||||
component="th"
|
||||
className={className}
|
||||
title={columnLabel}
|
||||
label={typeof label === 'function' ? label() : label}
|
||||
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{children}
|
||||
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
|
||||
TableHeaderCell.propTypes = {
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
columnLabel: PropTypes.string,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
|
||||
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
isSortable: PropTypes.bool,
|
||||
isVisible: PropTypes.bool,
|
||||
isModifiable: PropTypes.bool,
|
||||
|
||||
@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
|
||||
isDisabled={isModifiable === false}
|
||||
onChange={onVisibleChange}
|
||||
/>
|
||||
{label}
|
||||
{typeof label === 'function' ? label() : label}
|
||||
</label>
|
||||
|
||||
{
|
||||
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
|
||||
|
||||
TableOptionsColumn.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
isModifiable: PropTypes.bool.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
|
||||
@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
|
||||
|
||||
<TableOptionsColumn
|
||||
name={name}
|
||||
label={label}
|
||||
label={typeof label === 'function' ? label() : label}
|
||||
isVisible={isVisible}
|
||||
isModifiable={isModifiable}
|
||||
index={index}
|
||||
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
|
||||
|
||||
TableOptionsColumnDragSource.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
isModifiable: PropTypes.bool.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
|
||||
@@ -192,7 +192,7 @@ class TableOptionsModal extends Component {
|
||||
<TableOptionsColumnDragSource
|
||||
key={name}
|
||||
name={name}
|
||||
label={label || columnLabel}
|
||||
label={columnLabel || label}
|
||||
isVisible={isVisible}
|
||||
isModifiable={true}
|
||||
index={index}
|
||||
@@ -210,7 +210,7 @@ class TableOptionsModal extends Component {
|
||||
<TableOptionsColumn
|
||||
key={name}
|
||||
name={name}
|
||||
label={label || columnLabel}
|
||||
label={columnLabel || label}
|
||||
isVisible={isVisible}
|
||||
index={index}
|
||||
isModifiable={false}
|
||||
|
||||
@@ -6,37 +6,51 @@ import translate from 'Utilities/String/translate';
|
||||
export const shortcuts = {
|
||||
OPEN_KEYBOARD_SHORTCUTS_MODAL: {
|
||||
key: '?',
|
||||
name: translate('OpenThisModal')
|
||||
get name() {
|
||||
return translate('OpenThisModal');
|
||||
}
|
||||
},
|
||||
|
||||
CLOSE_MODAL: {
|
||||
key: 'Esc',
|
||||
name: translate('CloseCurrentModal')
|
||||
get name() {
|
||||
return translate('CloseCurrentModal');
|
||||
}
|
||||
},
|
||||
|
||||
ACCEPT_CONFIRM_MODAL: {
|
||||
key: 'Enter',
|
||||
name: translate('AcceptConfirmationModal')
|
||||
get name() {
|
||||
return translate('AcceptConfirmationModal');
|
||||
}
|
||||
},
|
||||
|
||||
MOVIE_SEARCH_INPUT: {
|
||||
key: 's',
|
||||
name: translate('FocusSearchBox')
|
||||
get name() {
|
||||
return translate('FocusSearchBox');
|
||||
}
|
||||
},
|
||||
|
||||
SAVE_SETTINGS: {
|
||||
key: 'mod+s',
|
||||
name: translate('SaveSettings')
|
||||
get name() {
|
||||
return translate('SaveSettings');
|
||||
}
|
||||
},
|
||||
|
||||
SCROLL_TOP: {
|
||||
key: 'mod+home',
|
||||
name: translate('MovieIndexScrollTop')
|
||||
get name() {
|
||||
return translate('MovieIndexScrollTop');
|
||||
}
|
||||
},
|
||||
|
||||
SCROLL_BOTTOM: {
|
||||
key: 'mod+end',
|
||||
name: translate('MovieIndexScrollBottom')
|
||||
get name() {
|
||||
return translate('MovieIndexScrollBottom');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
|
||||
function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
||||
function ScrollPosition(props) {
|
||||
interface WrappedComponentProps {
|
||||
initialScrollTop: number;
|
||||
}
|
||||
|
||||
interface ScrollPositionProps {
|
||||
history: RouteComponentProps['history'];
|
||||
location: RouteComponentProps['location'];
|
||||
match: RouteComponentProps['match'];
|
||||
}
|
||||
|
||||
function withScrollPosition(
|
||||
WrappedComponent: React.FC<WrappedComponentProps>,
|
||||
scrollPositionKey: string
|
||||
) {
|
||||
function ScrollPosition(props: ScrollPositionProps) {
|
||||
const { history } = props;
|
||||
|
||||
const initialScrollTop =
|
||||
history.action === 'POP' ||
|
||||
(history.location.state && history.location.state.restoreScrollPosition)
|
||||
? scrollPositions[scrollPositionKey]
|
||||
: 0;
|
||||
history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0;
|
||||
|
||||
return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
|
||||
}
|
||||
|
||||
ScrollPosition.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
return ScrollPosition;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
|
||||
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
@@ -63,71 +63,63 @@ function AuthenticationRequiredModalContent(props) {
|
||||
className={styles.authRequiredAlert}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{authenticationRequiredWarning}
|
||||
{translate('AuthenticationRequiredWarning', { appName: 'Prowlarr' })}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
isPopulated && !error ?
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Authentication')}</FormLabel>
|
||||
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpText={translate('AuthenticationMethodHelpText', { appName: 'Prowlarr' })}
|
||||
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
||||
helpLink="https://wiki.servarr.com/prowlarr/faq#forced-authentication"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -72,8 +72,10 @@ import {
|
||||
faLanguage as fasLanguage,
|
||||
faLaptop as fasLaptop,
|
||||
faLevelUpAlt as fasLevelUpAlt,
|
||||
faListCheck as fasListCheck,
|
||||
faLocationArrow as fasLocationArrow,
|
||||
faLock as fasLock,
|
||||
faMagnet as fasMagnet,
|
||||
faMedkit as fasMedkit,
|
||||
faMinus as fasMinus,
|
||||
faMusic as fasMusic,
|
||||
@@ -180,6 +182,8 @@ export const INTERACTIVE = fasUser;
|
||||
export const KEYBOARD = farKeyboard;
|
||||
export const LOCK = fasLock;
|
||||
export const LOGOUT = fasSignOutAlt;
|
||||
export const MAGNET = fasMagnet;
|
||||
export const MANAGE = fasListCheck;
|
||||
export const MEDIA_INFO = farFileInvoice;
|
||||
export const MISSING = fasExclamationTriangle;
|
||||
export const MONITORED = fasBookmark;
|
||||
|
||||
@@ -10,6 +10,7 @@ export const INFO = 'info';
|
||||
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
||||
export const CATEGORY_SELECT = 'newznabCategorySelect';
|
||||
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
||||
export const FLOAT = 'float';
|
||||
export const NUMBER = 'number';
|
||||
export const OAUTH = 'oauth';
|
||||
export const PASSWORD = 'password';
|
||||
@@ -35,6 +36,7 @@ export const all = [
|
||||
INFO,
|
||||
MOVIE_MONITORED_SELECT,
|
||||
CATEGORY_SELECT,
|
||||
FLOAT,
|
||||
NUMBER,
|
||||
OAUTH,
|
||||
PASSWORD,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.markAsFailedButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryDetails from './HistoryDetails';
|
||||
import styles from './HistoryDetailsModal.css';
|
||||
|
||||
function getHeaderTitle(eventType) {
|
||||
switch (eventType) {
|
||||
@@ -33,10 +30,8 @@ function HistoryDetailsModal(props) {
|
||||
eventType,
|
||||
indexer,
|
||||
data,
|
||||
isMarkingAsFailed,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
onMarkAsFailedPress,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
@@ -61,18 +56,6 @@ function HistoryDetailsModal(props) {
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<SpinnerButton
|
||||
className={styles.markAsFailedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isMarkingAsFailed}
|
||||
onPress={onMarkAsFailedPress}
|
||||
>
|
||||
Mark as Failed
|
||||
</SpinnerButton>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
@@ -89,10 +72,8 @@ HistoryDetailsModal.propTypes = {
|
||||
eventType: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
isMarkingAsFailed: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ class HistoryOptions extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="historyCleanupDays"
|
||||
unit={translate('days')}
|
||||
value={historyCleanupDays}
|
||||
helpText={translate('HistoryCleanupDaysHelpText')}
|
||||
helpTextWarning={translate('HistoryCleanupDaysHelpTextWarning')}
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
@@ -14,24 +14,79 @@ import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import HistoryRowParameter from './HistoryRowParameter';
|
||||
import styles from './HistoryRow.css';
|
||||
|
||||
const historyParameters = [
|
||||
export const historyParameters = [
|
||||
{ key: historyDataTypes.IMDB_ID, title: 'IMDb' },
|
||||
{ key: historyDataTypes.TMDB_ID, title: 'TMDb' },
|
||||
{ key: historyDataTypes.TVDB_ID, title: 'TVDb' },
|
||||
{ key: historyDataTypes.TRAKT_ID, title: 'Trakt' },
|
||||
{ key: historyDataTypes.R_ID, title: 'TvRage' },
|
||||
{ key: historyDataTypes.TVMAZE_ID, title: 'TvMaze' },
|
||||
{ key: historyDataTypes.SEASON, title: translate('Season') },
|
||||
{ key: historyDataTypes.EPISODE, title: translate('Episode') },
|
||||
{ key: historyDataTypes.ARTIST, title: translate('Artist') },
|
||||
{ key: historyDataTypes.ALBUM, title: translate('Album') },
|
||||
{ key: historyDataTypes.LABEL, title: translate('Label') },
|
||||
{ key: historyDataTypes.TRACK, title: translate('Track') },
|
||||
{ key: historyDataTypes.YEAR, title: translate('Year') },
|
||||
{ key: historyDataTypes.GENRE, title: translate('Genre') },
|
||||
{ key: historyDataTypes.AUTHOR, title: translate('Author') },
|
||||
{ key: historyDataTypes.TITLE, title: translate('Title') },
|
||||
{ key: historyDataTypes.PUBLISHER, title: translate('Publisher') }
|
||||
{
|
||||
key: historyDataTypes.SEASON,
|
||||
get title() {
|
||||
return translate('Season');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: historyDataTypes.EPISODE,
|
||||
get title() {
|
||||
return translate('Episode');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: historyDataTypes.ARTIST,
|
||||
get title() {
|
||||
return translate('Artist');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: historyDataTypes.ALBUM,
|
||||
get title() {
|
||||
return translate('Album');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: historyDataTypes.LABEL,
|
||||
get title() {
|
||||
return translate('Label');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: historyDataTypes.TRACK,
|
||||
get title() {
|
||||
return translate('Track');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: historyDataTypes.YEAR,
|
||||
get title() {
|
||||
return translate('Year');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: historyDataTypes.GENRE,
|
||||
get title() {
|
||||
return translate('Genre');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: historyDataTypes.AUTHOR,
|
||||
get title() {
|
||||
return translate('Author');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: historyDataTypes.TITLE,
|
||||
get title() {
|
||||
return translate('Title');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: historyDataTypes.PUBLISHER,
|
||||
get title() {
|
||||
return translate('Publisher');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class HistoryRow extends Component {
|
||||
@@ -298,7 +353,7 @@ class HistoryRow extends Component {
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={date}
|
||||
className={styles.date}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import styles from './HistoryRowParameter.css';
|
||||
|
||||
class HistoryRowParameter extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
value
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.parameter}>
|
||||
<div className={styles.info}>
|
||||
<span>
|
||||
{
|
||||
title
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.value}
|
||||
>
|
||||
{
|
||||
value
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HistoryRowParameter.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default HistoryRowParameter;
|
||||
44
frontend/src/History/HistoryRowParameter.tsx
Normal file
44
frontend/src/History/HistoryRowParameter.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './HistoryRowParameter.css';
|
||||
|
||||
interface HistoryRowParameterProps {
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function HistoryRowParameter(props: HistoryRowParameterProps) {
|
||||
const { title, value } = props;
|
||||
|
||||
const type = title.toLowerCase();
|
||||
|
||||
let link = null;
|
||||
|
||||
if (type === 'imdb') {
|
||||
link = <Link to={`https://imdb.com/title/${value}/`}>{value}</Link>;
|
||||
} else if (type === 'tmdb') {
|
||||
link = (
|
||||
<Link to={`https://www.themoviedb.org/movie/${value}`}>{value}</Link>
|
||||
);
|
||||
} else if (type === 'tvdb') {
|
||||
link = (
|
||||
<Link to={`https://www.thetvdb.com/?tab=series&id=${value}`}>
|
||||
{value}
|
||||
</Link>
|
||||
);
|
||||
} else if (type === 'tvmaze') {
|
||||
link = <Link to={`https://www.tvmaze.com/shows/${value}/_`}>{value}</Link>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.parameter}>
|
||||
<div className={styles.info}>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.value}>{link ? link : value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HistoryRowParameter;
|
||||
@@ -40,7 +40,6 @@
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
margin-right: 12px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.filterContainer:last-child {
|
||||
@@ -53,17 +52,22 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.filterInput {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filterRow {
|
||||
flex-direction: column;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filterContainer {
|
||||
margin-right: 0;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
@@ -72,3 +76,30 @@
|
||||
margin-left: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $breakpointSmall) {
|
||||
.filterContainer {
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.available {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointExtraSmall) {
|
||||
.modalFooter {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.available {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'alert': string;
|
||||
'available': string;
|
||||
'filterContainer': string;
|
||||
'filterInput': string;
|
||||
'filterLabel': string;
|
||||
'filterRow': string;
|
||||
'indexers': string;
|
||||
'modalBody': string;
|
||||
'modalFooter': string;
|
||||
'scroller': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -22,31 +22,31 @@ import styles from './AddIndexerModalContent.css';
|
||||
const columns = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: translate('Protocol'),
|
||||
label: () => translate('Protocol'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sortName',
|
||||
label: translate('Name'),
|
||||
label: () => translate('Name'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
label: translate('Language'),
|
||||
label: () => translate('Language'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: translate('Description'),
|
||||
label: () => translate('Description'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'privacy',
|
||||
label: translate('Privacy'),
|
||||
label: () => translate('Privacy'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
}
|
||||
@@ -66,15 +66,21 @@ const protocols = [
|
||||
const privacyLevels = [
|
||||
{
|
||||
key: 'private',
|
||||
value: translate('Private')
|
||||
get value() {
|
||||
return translate('Private');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'semiPrivate',
|
||||
value: translate('SemiPrivate')
|
||||
get value() {
|
||||
return translate('SemiPrivate');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'public',
|
||||
value: translate('Public')
|
||||
get value() {
|
||||
return translate('Public');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -257,6 +263,7 @@ class AddIndexerModalContent extends Component {
|
||||
<SelectIndexerRowConnector
|
||||
key={`${indexer.implementation}-${indexer.name}`}
|
||||
implementation={indexer.implementation}
|
||||
implementationName={indexer.implementationName}
|
||||
{...indexer}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
@@ -278,12 +285,18 @@ class AddIndexerModalContent extends Component {
|
||||
</Scroller>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<div className={styles.available}>
|
||||
{
|
||||
isPopulated ?
|
||||
translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
|
||||
@@ -49,8 +49,8 @@ class AddIndexerModalContentConnector extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerSelect = ({ implementation, name }) => {
|
||||
this.props.selectIndexerSchema({ implementation, name });
|
||||
onIndexerSelect = ({ implementation, implementationName, name }) => {
|
||||
this.props.selectIndexerSchema({ implementation, implementationName, name });
|
||||
this.props.onSelectIndexer();
|
||||
};
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@ class AddIndexerPresetMenuItem extends Component {
|
||||
onPress = () => {
|
||||
const {
|
||||
name,
|
||||
implementation
|
||||
implementation,
|
||||
implementationName
|
||||
} = this.props;
|
||||
|
||||
this.props.onPress({
|
||||
name,
|
||||
implementation
|
||||
implementation,
|
||||
implementationName
|
||||
});
|
||||
};
|
||||
|
||||
@@ -26,6 +28,7 @@ class AddIndexerPresetMenuItem extends Component {
|
||||
const {
|
||||
name,
|
||||
implementation,
|
||||
implementationName,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -43,6 +46,7 @@ class AddIndexerPresetMenuItem extends Component {
|
||||
AddIndexerPresetMenuItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -17,10 +17,11 @@ class SelectIndexerRow extends Component {
|
||||
onPress = () => {
|
||||
const {
|
||||
implementation,
|
||||
implementationName,
|
||||
name
|
||||
} = this.props;
|
||||
|
||||
this.props.onIndexerSelect({ implementation, name });
|
||||
this.props.onIndexerSelect({ implementation, implementationName, name });
|
||||
};
|
||||
|
||||
//
|
||||
@@ -81,6 +82,7 @@ SelectIndexerRow.propTypes = {
|
||||
language: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
onIndexerSelect: PropTypes.func.isRequired,
|
||||
isExistingIndexer: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import DeleteIndexerModalContentConnector from './DeleteIndexerModalContentConnector';
|
||||
|
||||
function DeleteIndexerModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<DeleteIndexerModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
DeleteIndexerModal.propTypes = {
|
||||
...DeleteIndexerModalContentConnector.propTypes,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DeleteIndexerModal;
|
||||
25
frontend/src/Indexer/Delete/DeleteIndexerModal.tsx
Normal file
25
frontend/src/Indexer/Delete/DeleteIndexerModal.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
|
||||
|
||||
interface DeleteIndexerModalProps {
|
||||
isOpen: boolean;
|
||||
indexerId: number;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function DeleteIndexerModal(props: DeleteIndexerModalProps) {
|
||||
const { isOpen, indexerId, onModalClose } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
|
||||
<DeleteIndexerModalContent
|
||||
indexerId={indexerId}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteIndexerModal;
|
||||
@@ -1,88 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
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 { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class DeleteIndexerModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
deleteFiles: false,
|
||||
addImportExclusion: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDeleteFilesChange = ({ value }) => {
|
||||
this.setState({ deleteFiles: value });
|
||||
};
|
||||
|
||||
onAddImportExclusionChange = ({ value }) => {
|
||||
this.setState({ addImportExclusion: value });
|
||||
};
|
||||
|
||||
onDeleteMovieConfirmed = () => {
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
const addImportExclusion = this.state.addImportExclusion;
|
||||
|
||||
this.setState({ deleteFiles: false, addImportExclusion: false });
|
||||
this.props.onDeletePress(deleteFiles, addImportExclusion);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Delete - {name}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{`Are you sure you want to delete ${name} from Prowlarr`}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteMovieConfirmed}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteIndexerModalContent.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DeleteIndexerModalContent;
|
||||
54
frontend/src/Indexer/Delete/DeleteIndexerModalContent.tsx
Normal file
54
frontend/src/Indexer/Delete/DeleteIndexerModalContent.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
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 { kinds } from 'Helpers/Props';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import { deleteIndexer } from 'Store/Actions/indexerActions';
|
||||
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface DeleteIndexerModalContentProps {
|
||||
indexerId: number;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
const { indexerId, onModalClose } = props;
|
||||
|
||||
const { name } = useSelector(
|
||||
createIndexerSelectorForHook(indexerId)
|
||||
) as Indexer;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onConfirmDelete = useCallback(() => {
|
||||
dispatch(deleteIndexer({ id: indexerId }));
|
||||
|
||||
onModalClose();
|
||||
}, [indexerId, dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('Delete')} - {name}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{translate('AreYouSureYouWantToDeleteIndexer', { name })}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={onConfirmDelete}>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteIndexerModalContent;
|
||||
@@ -1,57 +0,0 @@
|
||||
import { push } from 'connected-react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteIndexer } from 'Store/Actions/indexerActions';
|
||||
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
|
||||
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createIndexerSelector(),
|
||||
(indexer) => {
|
||||
return indexer;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
deleteIndexer,
|
||||
push
|
||||
};
|
||||
|
||||
class DeleteIndexerModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDeletePress = () => {
|
||||
this.props.deleteIndexer({
|
||||
id: this.props.indexerId
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DeleteIndexerModalContent
|
||||
{...this.props}
|
||||
onDeletePress={this.onDeletePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteIndexerModalContentConnector.propTypes = {
|
||||
indexerId: PropTypes.number.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
deleteIndexer: PropTypes.func.isRequired,
|
||||
push: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DeleteIndexerModalContentConnector);
|
||||
@@ -61,7 +61,7 @@ function EditIndexerModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{`${id ? translate('EditIndexer') : translate('AddIndexer')} - ${indexerDisplayName}`}
|
||||
{id ? translate('EditIndexerImplementation', { implementationName: indexerDisplayName }) : translate('AddIndexerImplementation', { implementationName: indexerDisplayName })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -187,6 +187,7 @@ function EditIndexerModalContent(props) {
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('IndexerTagsHelpText')}
|
||||
helpTextWarning={translate('IndexerTagsHelpTextWarning')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SelectProvider } from 'App/SelectContext';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import IndexerAppState, {
|
||||
IndexerIndexAppState,
|
||||
} from 'App/State/IndexerAppState';
|
||||
import { APP_INDEXER_SYNC } from 'Commands/commandNames';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@@ -18,7 +22,7 @@ import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import NoIndexer from 'Indexer/NoIndexer';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { testAllIndexers } from 'Store/Actions/indexerActions';
|
||||
import { cloneIndexer, testAllIndexers } from 'Store/Actions/indexerActions';
|
||||
import {
|
||||
setIndexerFilter,
|
||||
setIndexerSort,
|
||||
@@ -41,9 +45,7 @@ import IndexerIndexTable from './Table/IndexerIndexTable';
|
||||
import IndexerIndexTableOptions from './Table/IndexerIndexTableOptions';
|
||||
import styles from './IndexerIndex.css';
|
||||
|
||||
function getViewComponent() {
|
||||
return IndexerIndexTable;
|
||||
}
|
||||
const getViewComponent = () => IndexerIndexTable;
|
||||
|
||||
interface IndexerIndexProps {
|
||||
initialScrollTop?: number;
|
||||
@@ -64,29 +66,22 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
sortKey,
|
||||
sortDirection,
|
||||
view,
|
||||
} = useSelector(
|
||||
createIndexerClientSideCollectionItemsSelector('indexerIndex')
|
||||
);
|
||||
}: IndexerAppState & IndexerIndexAppState & ClientSideCollectionAppState =
|
||||
useSelector(createIndexerClientSideCollectionItemsSelector('indexerIndex'));
|
||||
|
||||
const isSyncingIndexers = useSelector(
|
||||
createCommandExecutingSelector(APP_INDEXER_SYNC)
|
||||
);
|
||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||
const dispatch = useDispatch();
|
||||
const scrollerRef = useRef<HTMLDivElement>();
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false);
|
||||
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
|
||||
const onAppIndexerSyncPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: APP_INDEXER_SYNC,
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const onAddIndexerPress = useCallback(() => {
|
||||
setIsAddIndexerModalOpen(true);
|
||||
}, [setIsAddIndexerModalOpen]);
|
||||
@@ -103,6 +98,24 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
setIsEditIndexerModalOpen(false);
|
||||
}, [setIsEditIndexerModalOpen]);
|
||||
|
||||
const onCloneIndexerPress = useCallback(
|
||||
(id: number) => {
|
||||
dispatch(cloneIndexer({ id }));
|
||||
|
||||
setIsEditIndexerModalOpen(true);
|
||||
},
|
||||
[dispatch, setIsEditIndexerModalOpen]
|
||||
);
|
||||
|
||||
const onAppIndexerSyncPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: APP_INDEXER_SYNC,
|
||||
forceSync: true,
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const onTestAllPress = useCallback(() => {
|
||||
dispatch(testAllIndexers());
|
||||
}, [dispatch]);
|
||||
@@ -112,37 +125,37 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
}, [isSelectMode, setIsSelectMode]);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setIndexerTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onSortSelect = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setIndexerSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onFilterSelect = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setIndexerFilter({ selectedFilterKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onJumpBarItemPress = useCallback(
|
||||
(character) => {
|
||||
(character: string) => {
|
||||
setJumpToCharacter(character);
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
({ scrollTop }) => {
|
||||
setJumpToCharacter(null);
|
||||
scrollPositions.seriesIndex = scrollTop;
|
||||
({ scrollTop }: { scrollTop: number }) => {
|
||||
setJumpToCharacter(undefined);
|
||||
scrollPositions.indexerIndex = scrollTop;
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
);
|
||||
@@ -155,7 +168,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
};
|
||||
}
|
||||
|
||||
const characters = items.reduce((acc, item) => {
|
||||
const characters = items.reduce((acc: Record<string, number>, item) => {
|
||||
let char = item.sortName.charAt(0);
|
||||
|
||||
if (!isNaN(Number(char))) {
|
||||
@@ -190,7 +203,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
|
||||
return (
|
||||
<SelectProvider items={items}>
|
||||
<PageContent>
|
||||
<PageContent title={translate('Indexers')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
@@ -277,6 +290,8 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
<PageContentBody
|
||||
ref={scrollerRef}
|
||||
className={styles.contentBody}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
innerClassName={styles[`${view}InnerContentBody`]}
|
||||
initialScrollTop={props.initialScrollTop}
|
||||
onScroll={onScroll}
|
||||
@@ -297,6 +312,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
jumpToCharacter={jumpToCharacter}
|
||||
isSelectMode={isSelectMode}
|
||||
isSmallScreen={isSmallScreen}
|
||||
onCloneIndexerPress={onCloneIndexerPress}
|
||||
/>
|
||||
|
||||
<IndexerIndexFooter />
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setIndexerFilter } from 'Store/Actions/indexerIndexActions';
|
||||
|
||||
function createIndexerSelector() {
|
||||
return createSelector(
|
||||
(state) => state.indexers.items,
|
||||
(state: AppState) => state.indexers.items,
|
||||
(indexers) => {
|
||||
return indexers;
|
||||
}
|
||||
@@ -15,14 +16,20 @@ function createIndexerSelector() {
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.indexerIndex.filterBuilderProps,
|
||||
(state: AppState) => state.indexerIndex.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default function IndexerIndexFilterModal(props) {
|
||||
interface IndexerIndexFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function IndexerIndexFilterModal(
|
||||
props: IndexerIndexFilterModalProps
|
||||
) {
|
||||
const sectionItems = useSelector(createIndexerSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'indexerIndex';
|
||||
@@ -30,7 +37,7 @@ export default function IndexerIndexFilterModal(props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setIndexerFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
@@ -38,6 +45,7 @@ export default function IndexerIndexFilterModal(props) {
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import IndexerAppState from 'App/State/IndexerAppState';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
@@ -13,7 +14,7 @@ import styles from './IndexerIndexFooter.css';
|
||||
function createUnoptimizedSelector() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('indexers', 'indexerIndex'),
|
||||
(indexers) => {
|
||||
(indexers: IndexerAppState) => {
|
||||
return indexers.items.map((s) => {
|
||||
const { protocol, privacy, enable } = s;
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { CustomFilter } from 'App/State/AppState';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import { align } from 'Helpers/Props';
|
||||
import IndexerIndexFilterModal from 'Indexer/Index/IndexerIndexFilterModal';
|
||||
|
||||
function IndexerIndexFilterMenu(props) {
|
||||
interface IndexerIndexFilterMenuProps {
|
||||
selectedFilterKey: string | number;
|
||||
filters: object[];
|
||||
customFilters: CustomFilter[];
|
||||
isDisabled: boolean;
|
||||
onFilterSelect(filterName: string): unknown;
|
||||
}
|
||||
|
||||
function IndexerIndexFilterMenu(props: IndexerIndexFilterMenuProps) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
@@ -26,15 +34,6 @@ function IndexerIndexFilterMenu(props) {
|
||||
);
|
||||
}
|
||||
|
||||
IndexerIndexFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
IndexerIndexFilterMenu.defaultProps = {
|
||||
showCustomFilters: false,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import SortMenu from 'Components/Menu/SortMenu';
|
||||
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||
import { align, sortDirections } from 'Helpers/Props';
|
||||
import { align } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function IndexerIndexSortMenu(props) {
|
||||
interface IndexerIndexSortMenuProps {
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
isDisabled: boolean;
|
||||
onSortSelect(sortKey: string): unknown;
|
||||
}
|
||||
|
||||
function IndexerIndexSortMenu(props: IndexerIndexSortMenuProps) {
|
||||
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
|
||||
|
||||
return (
|
||||
@@ -79,11 +86,4 @@ function IndexerIndexSortMenu(props) {
|
||||
);
|
||||
}
|
||||
|
||||
IndexerIndexSortMenu.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default IndexerIndexSortMenu;
|
||||
|
||||
@@ -7,7 +7,8 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { bulkDeleteIndexers } from 'Store/Actions/indexerIndexActions';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import { bulkDeleteIndexers } from 'Store/Actions/indexerActions';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteIndexerModalContent.css';
|
||||
@@ -20,21 +21,21 @@ interface DeleteIndexerModalContentProps {
|
||||
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
const { indexerIds, onModalClose } = props;
|
||||
|
||||
const allIndexers = useSelector(createAllIndexersSelector());
|
||||
const allIndexers: Indexer[] = useSelector(createAllIndexersSelector());
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const selectedIndexers = useMemo(() => {
|
||||
const indexers = indexerIds.map((id) => {
|
||||
const indexers = useMemo((): Indexer[] => {
|
||||
const indexerList = indexerIds.map((id) => {
|
||||
return allIndexers.find((s) => s.id === id);
|
||||
});
|
||||
}) as Indexer[];
|
||||
|
||||
return orderBy(indexers, ['sortName']);
|
||||
return orderBy(indexerList, ['sortName']);
|
||||
}, [indexerIds, allIndexers]);
|
||||
|
||||
const onDeleteIndexerConfirmed = useCallback(() => {
|
||||
dispatch(
|
||||
bulkDeleteIndexers({
|
||||
indexerIds,
|
||||
ids: indexerIds,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -47,13 +48,13 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{translate('DeleteSelectedIndexersMessageText', [
|
||||
selectedIndexers.length,
|
||||
])}
|
||||
{translate('DeleteSelectedIndexersMessageText', {
|
||||
count: indexers.length,
|
||||
})}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{selectedIndexers.map((s) => {
|
||||
{indexers.map((s) => {
|
||||
return (
|
||||
<li key={s.id}>
|
||||
<span>{s.name}</span>
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 { inputTypes, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditIndexerModalContent.css';
|
||||
|
||||
@@ -15,6 +15,10 @@ interface SavePayload {
|
||||
enable?: boolean;
|
||||
appProfileId?: number;
|
||||
priority?: number;
|
||||
minimumSeeders?: number;
|
||||
seedRatio?: number;
|
||||
seedTime?: number;
|
||||
packSeedTime?: number;
|
||||
}
|
||||
|
||||
interface EditIndexerModalContentProps {
|
||||
@@ -26,9 +30,25 @@ interface EditIndexerModalContentProps {
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
const enableOptions = [
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||
{ key: 'true', value: translate('Enabled') },
|
||||
{ key: 'false', value: translate('Disabled') },
|
||||
{
|
||||
key: NO_CHANGE,
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'true',
|
||||
get value() {
|
||||
return translate('Enabled');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'false',
|
||||
get value() {
|
||||
return translate('Disabled');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
@@ -37,6 +57,14 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
const [enable, setEnable] = useState(NO_CHANGE);
|
||||
const [appProfileId, setAppProfileId] = useState<string | number>(NO_CHANGE);
|
||||
const [priority, setPriority] = useState<null | string | number>(null);
|
||||
const [minimumSeeders, setMinimumSeeders] = useState<null | string | number>(
|
||||
null
|
||||
);
|
||||
const [seedRatio, setSeedRatio] = useState<null | string | number>(null);
|
||||
const [seedTime, setSeedTime] = useState<null | string | number>(null);
|
||||
const [packSeedTime, setPackSeedTime] = useState<null | string | number>(
|
||||
null
|
||||
);
|
||||
|
||||
const save = useCallback(() => {
|
||||
let hasChanges = false;
|
||||
@@ -57,15 +85,45 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
payload.priority = priority as number;
|
||||
}
|
||||
|
||||
if (minimumSeeders !== null) {
|
||||
hasChanges = true;
|
||||
payload.minimumSeeders = minimumSeeders as number;
|
||||
}
|
||||
|
||||
if (seedRatio !== null) {
|
||||
hasChanges = true;
|
||||
payload.seedRatio = seedRatio as number;
|
||||
}
|
||||
|
||||
if (seedTime !== null) {
|
||||
hasChanges = true;
|
||||
payload.seedTime = seedTime as number;
|
||||
}
|
||||
|
||||
if (packSeedTime !== null) {
|
||||
hasChanges = true;
|
||||
payload.packSeedTime = packSeedTime as number;
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
onSavePress(payload);
|
||||
}
|
||||
|
||||
onModalClose();
|
||||
}, [enable, appProfileId, priority, onSavePress, onModalClose]);
|
||||
}, [
|
||||
enable,
|
||||
appProfileId,
|
||||
priority,
|
||||
minimumSeeders,
|
||||
seedRatio,
|
||||
seedTime,
|
||||
packSeedTime,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }) => {
|
||||
({ name, value }: { name: string; value: string }) => {
|
||||
switch (name) {
|
||||
case 'enable':
|
||||
setEnable(value);
|
||||
@@ -76,6 +134,18 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
case 'priority':
|
||||
setPriority(value);
|
||||
break;
|
||||
case 'minimumSeeders':
|
||||
setMinimumSeeders(value);
|
||||
break;
|
||||
case 'seedRatio':
|
||||
setSeedRatio(value);
|
||||
break;
|
||||
case 'seedTime':
|
||||
setSeedTime(value);
|
||||
break;
|
||||
case 'packSeedTime':
|
||||
setPackSeedTime(value);
|
||||
break;
|
||||
default:
|
||||
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
||||
}
|
||||
@@ -94,7 +164,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
<ModalHeader>{translate('EditSelectedIndexers')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Enable')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -106,21 +176,22 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('SyncProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.APP_PROFILE_SELECT}
|
||||
name="appProfileId"
|
||||
value={appProfileId}
|
||||
helpText={translate('AppProfileSelectHelpText')}
|
||||
includeNoChange={true}
|
||||
includeNoChangeDisabled={false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Priority')}</FormLabel>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('IndexerPriority')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
@@ -128,6 +199,57 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
value={priority}
|
||||
min={1}
|
||||
max={50}
|
||||
helpText={translate('IndexerPriorityHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('AppsMinimumSeeders')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="minimumSeeders"
|
||||
value={minimumSeeders}
|
||||
helpText={translate('AppsMinimumSeedersHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('SeedRatio')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="seedRatio"
|
||||
value={seedRatio}
|
||||
helpText={translate('SeedRatioHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('SeedTime')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="seedTime"
|
||||
value={seedTime}
|
||||
unit={translate('minutes')}
|
||||
helpText={translate('SeedTimeHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('PackSeedTime')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="packSeedTime"
|
||||
value={packSeedTime}
|
||||
unit={translate('minutes')}
|
||||
helpText={translate('PackSeedTimeHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -135,7 +257,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<div className={styles.selected}>
|
||||
{translate('CountIndexersSelected', [selectedCount])}
|
||||
{translate('CountIndexersSelected', { count: selectedCount })}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import translate from 'Utilities/String/translate';
|
||||
interface IndexerIndexSelectAllButtonProps {
|
||||
label: string;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
}
|
||||
|
||||
function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) {
|
||||
|
||||
@@ -2,11 +2,12 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { saveIndexerEditor } from 'Store/Actions/indexerIndexActions';
|
||||
import { bulkEditIndexers } from 'Store/Actions/indexerActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import DeleteIndexerModal from './Delete/DeleteIndexerModal';
|
||||
@@ -14,8 +15,18 @@ import EditIndexerModal from './Edit/EditIndexerModal';
|
||||
import TagsModal from './Tags/TagsModal';
|
||||
import styles from './IndexerIndexSelectFooter.css';
|
||||
|
||||
interface SavePayload {
|
||||
enable?: boolean;
|
||||
appProfileId?: number;
|
||||
priority?: number;
|
||||
minimumSeeders?: number;
|
||||
seedRatio?: number;
|
||||
seedTime?: number;
|
||||
packSeedTime?: number;
|
||||
}
|
||||
|
||||
const indexersEditorSelector = createSelector(
|
||||
(state) => state.indexers,
|
||||
(state: AppState) => state.indexers,
|
||||
(indexers) => {
|
||||
const { isSaving, isDeleting, deleteError } = indexers;
|
||||
|
||||
@@ -59,14 +70,14 @@ function IndexerIndexSelectFooter() {
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onSavePress = useCallback(
|
||||
(payload) => {
|
||||
(payload: SavePayload) => {
|
||||
setIsSavingIndexer(true);
|
||||
setIsEditModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveIndexerEditor({
|
||||
bulkEditIndexers({
|
||||
...payload,
|
||||
indexerIds,
|
||||
ids: indexerIds,
|
||||
})
|
||||
);
|
||||
},
|
||||
@@ -82,13 +93,13 @@ function IndexerIndexSelectFooter() {
|
||||
}, [setIsTagsModalOpen]);
|
||||
|
||||
const onApplyTagsPress = useCallback(
|
||||
(tags, applyTags) => {
|
||||
(tags: number[], applyTags: string) => {
|
||||
setIsSavingTags(true);
|
||||
setIsTagsModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveIndexerEditor({
|
||||
indexerIds,
|
||||
bulkEditIndexers({
|
||||
ids: indexerIds,
|
||||
tags,
|
||||
applyTags,
|
||||
})
|
||||
@@ -154,7 +165,7 @@ function IndexerIndexSelectFooter() {
|
||||
</div>
|
||||
|
||||
<div className={styles.selected}>
|
||||
{translate('CountIndexersSelected', [selectedCount])}
|
||||
{translate('CountIndexersSelected', { count: selectedCount })}
|
||||
</div>
|
||||
|
||||
<EditIndexerModal
|
||||
|
||||
@@ -7,7 +7,7 @@ interface IndexerIndexSelectModeButtonProps {
|
||||
label: string;
|
||||
iconName: IconDefinition;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { concat, uniq } from 'lodash';
|
||||
import { uniq } from 'lodash';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Tag } from 'App/State/TagsAppState';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -12,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -26,29 +28,35 @@ interface TagsModalContentProps {
|
||||
function TagsModalContent(props: TagsModalContentProps) {
|
||||
const { indexerIds, onModalClose, onApplyTagsPress } = props;
|
||||
|
||||
const allIndexers = useSelector(createAllIndexersSelector());
|
||||
const tagList = useSelector(createTagsSelector());
|
||||
const allIndexers: Indexer[] = useSelector(createAllIndexersSelector());
|
||||
const tagList: Tag[] = useSelector(createTagsSelector());
|
||||
|
||||
const [tags, setTags] = useState<number[]>([]);
|
||||
const [applyTags, setApplyTags] = useState('add');
|
||||
|
||||
const indexerTags = useMemo(() => {
|
||||
const indexers = indexerIds.map((id) => {
|
||||
return allIndexers.find((s) => s.id === id);
|
||||
});
|
||||
const tags = indexerIds.reduce((acc: number[], id) => {
|
||||
const s = allIndexers.find((s) => s.id === id);
|
||||
|
||||
return uniq(concat(...indexers.map((s) => s.tags)));
|
||||
if (s) {
|
||||
acc.push(...s.tags);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return uniq(tags);
|
||||
}, [indexerIds, allIndexers]);
|
||||
|
||||
const onTagsChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: number[] }) => {
|
||||
setTags(value);
|
||||
},
|
||||
[setTags]
|
||||
);
|
||||
|
||||
const onApplyTagsChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setApplyTags(value);
|
||||
},
|
||||
[setApplyTags]
|
||||
@@ -90,10 +98,10 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
value={applyTags}
|
||||
values={applyTagsOptions}
|
||||
helpTexts={[
|
||||
translate('ApplyTagsHelpTexts1'),
|
||||
translate('ApplyTagsHelpTexts2'),
|
||||
translate('ApplyTagsHelpTexts3'),
|
||||
translate('ApplyTagsHelpTexts4'),
|
||||
translate('ApplyTagsHelpTextHowToApplyIndexers'),
|
||||
translate('ApplyTagsHelpTextAdd'),
|
||||
translate('ApplyTagsHelpTextRemove'),
|
||||
translate('ApplyTagsHelpTextReplace'),
|
||||
]}
|
||||
onChange={onApplyTagsChange}
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,11 @@
|
||||
|
||||
.priority,
|
||||
.protocol,
|
||||
.privacy {
|
||||
.privacy,
|
||||
.minimumSeeders,
|
||||
.seedRatio,
|
||||
.seedTime,
|
||||
.packSeedTime {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 90px;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user