mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-05 13:40:08 -05:00
Compare commits
356 Commits
v1.11.2.41
...
v1.23.1.47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32d23d6636 | ||
|
|
b31b695887 | ||
|
|
33de32b138 | ||
|
|
753b53a529 | ||
|
|
123535b9a5 | ||
|
|
7a5fa452f0 | ||
|
|
281e712542 | ||
|
|
c2c34ecf53 | ||
|
|
615193617c | ||
|
|
1b58d50b6d | ||
|
|
99f9a0b4e6 | ||
|
|
696001a8bb | ||
|
|
31f057c097 | ||
|
|
0391537a60 | ||
|
|
521c1f760c | ||
|
|
3bf9b4f90f | ||
|
|
af86a6d34e | ||
|
|
3ecf5c6166 | ||
|
|
4da3e7b2b3 | ||
|
|
66f38f1566 | ||
|
|
04b513ad14 | ||
|
|
1ce7fda8bb | ||
|
|
6d09fad675 | ||
|
|
ad061e7ece | ||
|
|
3155343bcc | ||
|
|
f5e91f7bfd | ||
|
|
1d69f2ed3f | ||
|
|
1d233dbcab | ||
|
|
1aafb0b201 | ||
|
|
d7d5a2dd42 | ||
|
|
8060a65ef6 | ||
|
|
379071f838 | ||
|
|
5cbbd060a4 | ||
|
|
ef19673a76 | ||
|
|
c3cf8a6ebb | ||
|
|
c22b27525a | ||
|
|
eec3b01f5b | ||
|
|
e67a127a02 | ||
|
|
a074ebc951 | ||
|
|
d1cd814663 | ||
|
|
ac76646a20 | ||
|
|
6549f799f6 | ||
|
|
cca55fd66c | ||
|
|
2f67d2813a | ||
|
|
9a7a5fdc38 | ||
|
|
f1fdec6822 | ||
|
|
5464b23329 | ||
|
|
4c99971882 | ||
|
|
cc7769b601 | ||
|
|
cb2ed7daf9 | ||
|
|
78508094c8 | ||
|
|
b0f755a30c | ||
|
|
9d1384792a | ||
|
|
ea17116998 | ||
|
|
2c23681fc5 | ||
|
|
17aa2832ea | ||
|
|
5f3a329ef2 | ||
|
|
96f49da79e | ||
|
|
c7dfde0ce9 | ||
|
|
8cf32020f7 | ||
|
|
a5ed5a0e60 | ||
|
|
3279936fc9 | ||
|
|
8abccc709e | ||
|
|
76f30e7682 | ||
|
|
ab289b3e42 | ||
|
|
ef7e04065c | ||
|
|
d1084039b3 | ||
|
|
7bada440d2 | ||
|
|
803c4752db | ||
|
|
c0777474c0 | ||
|
|
66dcea5604 | ||
|
|
a2a12d2450 | ||
|
|
39593bd5a8 | ||
|
|
45d8a8a4e6 | ||
|
|
a4546c77ce | ||
|
|
d69bf6360a | ||
|
|
da9ce5b5c3 | ||
|
|
e092098101 | ||
|
|
1a89a79b74 | ||
|
|
cb6bf49922 | ||
|
|
4bcaba0be0 | ||
|
|
220ef723c7 | ||
|
|
9c599a6be4 | ||
|
|
715ce1fc6c | ||
|
|
8c3a192dd0 | ||
|
|
d22bf93dfd | ||
|
|
886054fdf8 | ||
|
|
4188510586 | ||
|
|
fedebca5e1 | ||
|
|
e2ce6437e9 | ||
|
|
bdae60bac9 | ||
|
|
2d6c818aec | ||
|
|
a1d19852dc | ||
|
|
104c95f28f | ||
|
|
55fa1ec637 | ||
|
|
b27a3d8272 | ||
|
|
089d450b46 | ||
|
|
358ac7434d | ||
|
|
9cd505fd8a | ||
|
|
20ac2687df | ||
|
|
9f075c09a2 | ||
|
|
3793538ba4 | ||
|
|
4c4b16d234 | ||
|
|
f5790bec2e | ||
|
|
6c0d08de56 | ||
|
|
ba344756b1 | ||
|
|
dfda86aca3 | ||
|
|
df6f83ed69 | ||
|
|
218d92a1ac | ||
|
|
df2b529d01 | ||
|
|
0ef42dbb4d | ||
|
|
1a428197b2 | ||
|
|
09d7983845 | ||
|
|
6e01f3187a | ||
|
|
468436b9f7 | ||
|
|
76c288a6e4 | ||
|
|
f95f67a7ca | ||
|
|
11864247eb | ||
|
|
74509ea7c9 | ||
|
|
948fe0a6dc | ||
|
|
a4257cbcde | ||
|
|
2929c3c898 | ||
|
|
2c5f2187c8 | ||
|
|
401ef88971 | ||
|
|
4fb3754048 | ||
|
|
596efe8fb0 | ||
|
|
076a4f2574 | ||
|
|
9561371a47 | ||
|
|
16254cf5f9 | ||
|
|
649a03e5a0 | ||
|
|
dd21d9b521 | ||
|
|
68b895d2ad | ||
|
|
634016ae1b | ||
|
|
83c6751847 | ||
|
|
04bb0c51b1 | ||
|
|
d2e9621de9 | ||
|
|
cb673ddc42 | ||
|
|
440618f2b6 | ||
|
|
ae79d45664 | ||
|
|
1877ccb513 | ||
|
|
b3098f2e4c | ||
|
|
3e0af062c1 | ||
|
|
858f85c50d | ||
|
|
938848be65 | ||
|
|
615f5899cc | ||
|
|
5a6b1313e8 | ||
|
|
ab7debb34b | ||
|
|
eee21de795 | ||
|
|
15fabbe7d0 | ||
|
|
6aef48c6e7 | ||
|
|
b29bc923fc | ||
|
|
b223e9b0cc | ||
|
|
77a982a7da | ||
|
|
ab3dc765b4 | ||
|
|
0261201360 | ||
|
|
1da3954879 | ||
|
|
742dd5ff54 | ||
|
|
a85406e3b7 | ||
|
|
73cdaf3d44 | ||
|
|
e26fa2dbf4 | ||
|
|
64be68a22d | ||
|
|
478a185968 | ||
|
|
4ff5d11a03 | ||
|
|
6000952b76 | ||
|
|
5fee2c4cd9 | ||
|
|
21d553cf1b | ||
|
|
782b2d3761 | ||
|
|
e1da3eee80 | ||
|
|
09af2da6b9 | ||
|
|
e3e9094d42 | ||
|
|
94634234ff | ||
|
|
a48d6029d9 | ||
|
|
9cc150b105 | ||
|
|
6a97d99876 | ||
|
|
c957168040 | ||
|
|
61bc35b3fa | ||
|
|
a84210c452 | ||
|
|
8af6ea1d8f | ||
|
|
1a894ac583 | ||
|
|
4f6e05414c | ||
|
|
5096a088d4 | ||
|
|
6581bddba3 | ||
|
|
292af28d42 | ||
|
|
37a6d03d52 | ||
|
|
fe35d450f0 | ||
|
|
6a9e27bc06 | ||
|
|
a989bf82ea | ||
|
|
ccc8d8002f | ||
|
|
eaaf8db486 | ||
|
|
c32fa7a84b | ||
|
|
57e21a78ee | ||
|
|
9cdf5d18d8 | ||
|
|
41b0a1211b | ||
|
|
1b8f09f2ce | ||
|
|
2f85de6b69 | ||
|
|
b2ef9d5b0a | ||
|
|
c80262d75b | ||
|
|
2a312d93ec | ||
|
|
e09df2fff3 | ||
|
|
f0c7d13b20 | ||
|
|
4dac60bef9 | ||
|
|
5aefb46790 | ||
|
|
41b043e551 | ||
|
|
5447fad1fc | ||
|
|
6a1e01abbd | ||
|
|
2803ad5ba0 | ||
|
|
8fa8a13036 | ||
|
|
41ce79ccce | ||
|
|
14ae062db2 | ||
|
|
d55a38da4a | ||
|
|
ab289cfc86 | ||
|
|
12671e9905 | ||
|
|
a33023b8c6 | ||
|
|
a3e134ce0b | ||
|
|
ee7c821cab | ||
|
|
ee4cf93aee | ||
|
|
2cacfba81f | ||
|
|
02e420580e | ||
|
|
d99398d3f8 | ||
|
|
9ea8335aa0 | ||
|
|
52a91a50b2 | ||
|
|
680bf46e25 | ||
|
|
d279c97f15 | ||
|
|
7d5d338c8e | ||
|
|
721ae1cac0 | ||
|
|
3881c9d753 | ||
|
|
131b344119 | ||
|
|
d226e52881 | ||
|
|
583815b4f7 | ||
|
|
f0a8d22e84 | ||
|
|
50c6f15e12 | ||
|
|
2e3a95f389 | ||
|
|
3d52096eb4 | ||
|
|
e981cacbda | ||
|
|
218371a318 | ||
|
|
30fd7c8c2a | ||
|
|
96d2d61fa0 | ||
|
|
17ff86aaea | ||
|
|
7f8c1ace14 | ||
|
|
dc0edb7bc1 | ||
|
|
2ac996c9f9 | ||
|
|
2ebabd69b5 | ||
|
|
b3738f1602 | ||
|
|
882152b911 | ||
|
|
a25e79031f | ||
|
|
cc85060b1b | ||
|
|
00bd9c241a | ||
|
|
1283e06f95 | ||
|
|
ab0108778a | ||
|
|
099b04f718 | ||
|
|
ecdc0a51a9 | ||
|
|
6c7c37affe | ||
|
|
45d378a2d9 | ||
|
|
007601cb19 | ||
|
|
5f0d6e2fdd | ||
|
|
ede9879c99 | ||
|
|
7287abc77c | ||
|
|
8c653b5c09 | ||
|
|
15c6b3c308 | ||
|
|
9676447c74 | ||
|
|
5d35f1dcc7 | ||
|
|
858f16195e | ||
|
|
a1a5dd574e | ||
|
|
a5ecc2dc9f | ||
|
|
7d46660583 | ||
|
|
22cbf40e3c | ||
|
|
25821c758f | ||
|
|
6153737a78 | ||
|
|
07adb45d63 | ||
|
|
02bc40b9b6 | ||
|
|
83e7e30e4f | ||
|
|
ae870fd46a | ||
|
|
33b7ba8725 | ||
|
|
dd2567a85f | ||
|
|
264ff8f885 | ||
|
|
629c6a8891 | ||
|
|
0ce2f96789 | ||
|
|
cd7d1571db | ||
|
|
4558f55282 | ||
|
|
21589fda57 | ||
|
|
3496263cd2 | ||
|
|
1bb1ec6106 | ||
|
|
2bfb838933 | ||
|
|
9eb291f578 | ||
|
|
8cf892124c | ||
|
|
47fb886930 | ||
|
|
5034a211cb | ||
|
|
ed1364b6ff | ||
|
|
71e18b616d | ||
|
|
f7bf21df68 | ||
|
|
d764e3405d | ||
|
|
16baceb784 | ||
|
|
5d2b80d15a | ||
|
|
a20a81f424 | ||
|
|
ebb66e9086 | ||
|
|
cb8797693e | ||
|
|
255c6335ae | ||
|
|
155cd53dcd | ||
|
|
ae70a96c10 | ||
|
|
16c0daf090 | ||
|
|
34c78c5a9d | ||
|
|
dd5b108ffd | ||
|
|
0b83986255 | ||
|
|
2bd25fb6f3 | ||
|
|
0f5eb5d3a3 | ||
|
|
c9434c61e3 | ||
|
|
ee969b7a06 | ||
|
|
cfdf88a1e2 | ||
|
|
70a5dae293 | ||
|
|
b53f8d4552 | ||
|
|
9668e91b21 | ||
|
|
49857693c0 | ||
|
|
850315ad1c | ||
|
|
86124d4319 | ||
|
|
4f28d583d7 | ||
|
|
01f3930211 | ||
|
|
7c7114c87a | ||
|
|
ef8e6d774b | ||
|
|
2960fc37d9 | ||
|
|
8bddf753bb | ||
|
|
cff24b3fd4 | ||
|
|
031d81330d | ||
|
|
6201b42fbd | ||
|
|
7022054dd7 | ||
|
|
c9b663247c | ||
|
|
0b0a0cfa5b | ||
|
|
3c0fea8b7c | ||
|
|
ac97952fd7 | ||
|
|
c3e40c0564 | ||
|
|
ce615a77c2 | ||
|
|
0f6dfe389c | ||
|
|
25d94a9286 | ||
|
|
52a690b41a | ||
|
|
56c8c3d6c6 | ||
|
|
5cee8990b3 | ||
|
|
9679e88717 | ||
|
|
f4203993ba | ||
|
|
4be0715fe3 | ||
|
|
f9c9d4a0e0 | ||
|
|
bf47380f7b | ||
|
|
e27a46f578 | ||
|
|
af4e69f8fb | ||
|
|
1b2106d4f0 | ||
|
|
bdfbda3805 | ||
|
|
cb98b10468 | ||
|
|
ae1bc8366c | ||
|
|
67eeb4373c | ||
|
|
9d40a64be4 | ||
|
|
20cc146d82 | ||
|
|
8beff32117 | ||
|
|
8b7c488173 | ||
|
|
1dabbc94f8 | ||
|
|
52667b979b | ||
|
|
14f9a75a73 | ||
|
|
ed17d91a7b | ||
|
|
f54280b888 |
13
.devcontainer/Prowlarr.code-workspace
Normal file
13
.devcontainer/Prowlarr.code-workspace
Normal file
@@ -0,0 +1,13 @@
|
||||
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||
// the frontend has vscode settings that are distinct from the backend
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../frontend"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,19 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||
{
|
||||
"name": "Prowlarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [9696],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
28
.github/labeler.yml
vendored
28
.github/labeler.yml
vendored
@@ -1,19 +1,31 @@
|
||||
'Area: API':
|
||||
- src/Prowlarr.Api.V1/**/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/Prowlarr.Api.V1/**/*
|
||||
|
||||
'Area: Db-migration':
|
||||
- src/NzbDrone.Core/Datastore/Migration/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/NzbDrone.Core/Datastore/Migration/*
|
||||
|
||||
'Area: Download Clients':
|
||||
- src/NzbDrone.Core/Download/Clients/**/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/NzbDrone.Core/Download/Clients/**/*
|
||||
|
||||
'Area: Indexer':
|
||||
- src/NzbDrone.Core/Indexers/**/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/NzbDrone.Core/Indexers/**/*
|
||||
|
||||
'Area: Notifications':
|
||||
- src/NzbDrone.Core/Notifications/**/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/NzbDrone.Core/Notifications/**/*
|
||||
|
||||
'Area: UI':
|
||||
- frontend/**/*
|
||||
- package.json
|
||||
- yarn.lock
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- frontend/**/*
|
||||
- package.json
|
||||
- yarn.lock
|
||||
2
.github/workflows/label-actions.yml
vendored
2
.github/workflows/label-actions.yml
vendored
@@ -18,6 +18,6 @@ jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v3
|
||||
- uses: dessant/label-actions@v4
|
||||
with:
|
||||
process-only: 'issues, prs'
|
||||
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -9,4 +9,4 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
- uses: actions/labeler@v5
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '90'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -127,6 +127,7 @@ coverage*.xml
|
||||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# VS outout folders
|
||||
bin
|
||||
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
||||
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||
"name": "Run Prowlarr",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build dotnet",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/_output/net6.0/Prowlarr",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
.vscode/tasks.json
vendored
Normal file
44
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build dotnet",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"msbuild",
|
||||
"-restore",
|
||||
"${workspaceFolder}/src/Prowlarr.sln",
|
||||
"-p:GenerateFullPaths=true",
|
||||
"-p:Configuration=Debug",
|
||||
"-p:Platform=Posix",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/Prowlarr.sln",
|
||||
"-property:GenerateFullPaths=true",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Prowlarr.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,18 +9,18 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.11.2'
|
||||
majorVersion: '1.23.1'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.417'
|
||||
nodeVersion: '16.X'
|
||||
innoVersion: '6.2.0'
|
||||
dotnetVersion: '6.0.424'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.2'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
macImage: 'macOS-12'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
@@ -166,10 +166,10 @@ stages:
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -1075,10 +1075,10 @@ stages:
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -1169,7 +1169,7 @@ stages:
|
||||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@1
|
||||
- task: SonarCloudPrepare@2
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
@@ -1187,25 +1187,21 @@ stages:
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@1
|
||||
- task: SonarCloudAnalyze@2
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@4
|
||||
- task: reportgenerator@5
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: Publish Coverage Report
|
||||
inputs:
|
||||
codeCoverageTool: 'cobertura'
|
||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
||||
reportDirectory: './CoverageResults/combined/'
|
||||
publishCodeCoverageResults: true
|
||||
|
||||
- stage: Report_Out
|
||||
dependsOn:
|
||||
- Analyze
|
||||
- Installer
|
||||
- Unit_Test
|
||||
- Integration
|
||||
- Automation
|
||||
|
||||
2
build.sh
2
build.sh
@@ -254,7 +254,7 @@ InstallInno()
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
|
||||
16
docs.sh
Normal file → Executable file
16
docs.sh
Normal file → Executable file
@@ -1,3 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
PLATFORM=$1
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
@@ -21,17 +25,23 @@ slnFile=src/Prowlarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
application=Prowlarr.Console.dll
|
||||
else
|
||||
application=Prowlarr.dll
|
||||
fi
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 &
|
||||
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||
|
||||
sleep 30
|
||||
sleep 45
|
||||
|
||||
kill %1
|
||||
|
||||
|
||||
@@ -357,11 +357,16 @@ module.exports = {
|
||||
],
|
||||
|
||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
||||
'no-shadow': 'off',
|
||||
// These should be enabled after cleaning things up
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'no-shadow': 'off',
|
||||
'prettier/prettier': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
@@ -374,7 +379,41 @@ module.exports = {
|
||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
// React Hooks
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
|
||||
// React
|
||||
'react/function-component-definition': 'error',
|
||||
'react/hook-use-state': 'error',
|
||||
'react/jsx-boolean-value': ['error', 'always'],
|
||||
'react/jsx-curly-brace-presence': [
|
||||
'error',
|
||||
{ props: 'never', children: 'never' }
|
||||
],
|
||||
'react/jsx-fragments': 'error',
|
||||
'react/jsx-handler-names': [
|
||||
'error',
|
||||
{
|
||||
eventHandlerPrefix: 'on',
|
||||
eventHandlerPropPrefix: 'on'
|
||||
}
|
||||
],
|
||||
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||
'react/jsx-sort-props': [
|
||||
'error',
|
||||
{
|
||||
callbacksLast: true,
|
||||
noSortAlphabetically: true,
|
||||
reservedFirst: true
|
||||
}
|
||||
],
|
||||
'react/prop-types': 'off',
|
||||
'react/self-closing-comp': 'error'
|
||||
})
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,6 +2,8 @@ const loose = true;
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||
|
||||
@@ -65,7 +65,7 @@ module.exports = (env) => {
|
||||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: '[name]-[contenthash].js',
|
||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
@@ -90,7 +90,7 @@ module.exports = (env) => {
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -190,7 +190,7 @@ module.exports = (env) => {
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ const mixinsFiles = [
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'autoprefixer',
|
||||
['postcss-mixins', {
|
||||
mixinsFiles
|
||||
}],
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import { ConnectedRouter } from 'connected-react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||
import React from 'react';
|
||||
import DocumentTitle from 'react-document-title';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Store } from 'redux';
|
||||
import PageConnector from 'Components/Page/PageConnector';
|
||||
import ApplyTheme from './ApplyTheme';
|
||||
import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
interface AppProps {
|
||||
store: Store;
|
||||
history: ConnectedRouterProps['history'];
|
||||
}
|
||||
|
||||
function App({ store, history }: AppProps) {
|
||||
return (
|
||||
<DocumentTitle title={window.Prowlarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
<ApplyTheme />
|
||||
<PageConnector>
|
||||
<AppRoutes />
|
||||
</PageConnector>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</DocumentTitle>
|
||||
);
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -1,184 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import HistoryConnector from 'History/HistoryConnector';
|
||||
import IndexerIndex from 'Indexer/Index/IndexerIndex';
|
||||
import IndexerStats from 'Indexer/Stats/IndexerStats';
|
||||
import SearchIndexConnector from 'Search/SearchIndexConnector';
|
||||
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
|
||||
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||
import Settings from 'Settings/Settings';
|
||||
import TagSettings from 'Settings/Tags/TagSettings';
|
||||
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
||||
import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||
import Logs from 'System/Logs/Logs';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
|
||||
function AppRoutes(props) {
|
||||
const {
|
||||
app
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
{/*
|
||||
Indexers
|
||||
*/}
|
||||
|
||||
<Route
|
||||
exact={true}
|
||||
path="/"
|
||||
component={IndexerIndex}
|
||||
/>
|
||||
|
||||
{
|
||||
window.Prowlarr.urlBase &&
|
||||
<Route
|
||||
exact={true}
|
||||
path="/"
|
||||
addUrlBase={false}
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect
|
||||
to={getPathWithUrlBase('/')}
|
||||
component={app}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
<Route
|
||||
path="/indexers/stats"
|
||||
component={IndexerStats}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Search
|
||||
*/}
|
||||
|
||||
<Route
|
||||
path="/search"
|
||||
component={SearchIndexConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Activity
|
||||
*/}
|
||||
|
||||
<Route
|
||||
path="/history"
|
||||
component={HistoryConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Settings
|
||||
*/}
|
||||
|
||||
<Route
|
||||
exact={true}
|
||||
path="/settings"
|
||||
component={Settings}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/indexers"
|
||||
component={IndexerSettings}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/applications"
|
||||
component={ApplicationSettings}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/downloadclients"
|
||||
component={DownloadClientSettingsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/connect"
|
||||
component={NotificationSettings}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/tags"
|
||||
component={TagSettings}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/general"
|
||||
component={GeneralSettingsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/ui"
|
||||
component={UISettingsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/development"
|
||||
component={DevelopmentSettingsConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
System
|
||||
*/}
|
||||
|
||||
<Route
|
||||
path="/system/status"
|
||||
component={Status}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/system/tasks"
|
||||
component={Tasks}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/system/backup"
|
||||
component={BackupsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/system/updates"
|
||||
component={UpdatesConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/system/events"
|
||||
component={LogsTableConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/system/logs/files"
|
||||
component={Logs}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Not Found
|
||||
*/}
|
||||
|
||||
<Route
|
||||
path="*"
|
||||
component={NotFound}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
AppRoutes.propTypes = {
|
||||
app: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AppRoutes;
|
||||
117
frontend/src/App/AppRoutes.tsx
Normal file
117
frontend/src/App/AppRoutes.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import HistoryConnector from 'History/HistoryConnector';
|
||||
import IndexerIndex from 'Indexer/Index/IndexerIndex';
|
||||
import IndexerStats from 'Indexer/Stats/IndexerStats';
|
||||
import SearchIndexConnector from 'Search/SearchIndexConnector';
|
||||
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
|
||||
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||
import Settings from 'Settings/Settings';
|
||||
import TagSettings from 'Settings/Tags/TagSettings';
|
||||
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
||||
import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||
import Logs from 'System/Logs/Logs';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
|
||||
function RedirectWithUrlBase() {
|
||||
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||
}
|
||||
|
||||
function AppRoutes() {
|
||||
return (
|
||||
<Switch>
|
||||
{/*
|
||||
Indexers
|
||||
*/}
|
||||
|
||||
<Route exact={true} path="/" component={IndexerIndex} />
|
||||
|
||||
{window.Prowlarr.urlBase && (
|
||||
<Route
|
||||
exact={true}
|
||||
path="/"
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
addUrlBase={false}
|
||||
render={RedirectWithUrlBase}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Route path="/indexers/stats" component={IndexerStats} />
|
||||
|
||||
{/*
|
||||
Search
|
||||
*/}
|
||||
|
||||
<Route path="/search" component={SearchIndexConnector} />
|
||||
|
||||
{/*
|
||||
Activity
|
||||
*/}
|
||||
|
||||
<Route path="/history" component={HistoryConnector} />
|
||||
|
||||
{/*
|
||||
Settings
|
||||
*/}
|
||||
|
||||
<Route exact={true} path="/settings" component={Settings} />
|
||||
|
||||
<Route path="/settings/indexers" component={IndexerSettings} />
|
||||
|
||||
<Route path="/settings/applications" component={ApplicationSettings} />
|
||||
|
||||
<Route
|
||||
path="/settings/downloadclients"
|
||||
component={DownloadClientSettingsConnector}
|
||||
/>
|
||||
|
||||
<Route path="/settings/connect" component={NotificationSettings} />
|
||||
|
||||
<Route path="/settings/tags" component={TagSettings} />
|
||||
|
||||
<Route path="/settings/general" component={GeneralSettingsConnector} />
|
||||
|
||||
<Route path="/settings/ui" component={UISettingsConnector} />
|
||||
|
||||
<Route
|
||||
path="/settings/development"
|
||||
component={DevelopmentSettingsConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
System
|
||||
*/}
|
||||
|
||||
<Route path="/system/status" component={Status} />
|
||||
|
||||
<Route path="/system/tasks" component={Tasks} />
|
||||
|
||||
<Route path="/system/backup" component={BackupsConnector} />
|
||||
|
||||
<Route path="/system/updates" component={UpdatesConnector} />
|
||||
|
||||
<Route path="/system/events" component={LogsTableConnector} />
|
||||
|
||||
<Route path="/system/logs/files" component={Logs} />
|
||||
|
||||
{/*
|
||||
Not Found
|
||||
*/}
|
||||
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppRoutes;
|
||||
@@ -1,30 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector';
|
||||
|
||||
function AppUpdatedModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AppUpdatedModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AppUpdatedModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AppUpdatedModal;
|
||||
28
frontend/src/App/AppUpdatedModal.tsx
Normal file
28
frontend/src/App/AppUpdatedModal.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AppUpdatedModalContent from './AppUpdatedModalContent';
|
||||
|
||||
interface AppUpdatedModalProps {
|
||||
isOpen: boolean;
|
||||
onModalClose: (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
function AppUpdatedModal(props: AppUpdatedModalProps) {
|
||||
const { isOpen, onModalClose } = props;
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
location.reload();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AppUpdatedModalContent onModalClose={handleModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppUpdatedModal;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import AppUpdatedModal from './AppUpdatedModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onModalClose() {
|
||||
location.reload();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(AppUpdatedModal);
|
||||
@@ -1,139 +0,0 @@
|
||||
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';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AppUpdatedModalContent.css';
|
||||
|
||||
function mergeUpdates(items, version, prevVersion) {
|
||||
let installedIndex = items.findIndex((u) => u.version === version);
|
||||
let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
|
||||
|
||||
if (installedIndex === -1) {
|
||||
installedIndex = 0;
|
||||
}
|
||||
|
||||
if (installedPreviouslyIndex === -1) {
|
||||
installedPreviouslyIndex = items.length;
|
||||
} else if (installedPreviouslyIndex === installedIndex && items.length) {
|
||||
installedPreviouslyIndex += 1;
|
||||
}
|
||||
|
||||
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
|
||||
|
||||
if (!appliedUpdates.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const appliedChanges = { new: [], fixed: [] };
|
||||
appliedUpdates.forEach((u) => {
|
||||
if (u.changes) {
|
||||
appliedChanges.new.push(... u.changes.new);
|
||||
appliedChanges.fixed.push(... u.changes.fixed);
|
||||
}
|
||||
});
|
||||
|
||||
const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
|
||||
|
||||
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
||||
mergedUpdate.changes = null;
|
||||
}
|
||||
|
||||
return mergedUpdate;
|
||||
}
|
||||
|
||||
function AppUpdatedModalContent(props) {
|
||||
const {
|
||||
version,
|
||||
prevVersion,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
onSeeChangesPress,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
const update = mergeUpdates(items, version, prevVersion);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AppUpdated')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
||||
</div>
|
||||
|
||||
{
|
||||
isPopulated && !error && !!update &&
|
||||
<div>
|
||||
{
|
||||
!update.changes &&
|
||||
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!update.changes &&
|
||||
<div>
|
||||
<div className={styles.changes}>
|
||||
{translate('WhatsNew')}
|
||||
</div>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isPopulated && !error &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onSeeChangesPress}
|
||||
>
|
||||
{translate('RecentChanges')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Reload')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
AppUpdatedModalContent.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
prevVersion: PropTypes.string,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSeeChangesPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AppUpdatedModalContent;
|
||||
145
frontend/src/App/AppUpdatedModalContent.tsx
Normal file
145
frontend/src/App/AppUpdatedModalContent.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
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';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||
import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||
import Update from 'typings/Update';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AppState from './State/AppState';
|
||||
import styles from './AppUpdatedModalContent.css';
|
||||
|
||||
function mergeUpdates(items: Update[], version: string, prevVersion?: string) {
|
||||
let installedIndex = items.findIndex((u) => u.version === version);
|
||||
let installedPreviouslyIndex = items.findIndex(
|
||||
(u) => u.version === prevVersion
|
||||
);
|
||||
|
||||
if (installedIndex === -1) {
|
||||
installedIndex = 0;
|
||||
}
|
||||
|
||||
if (installedPreviouslyIndex === -1) {
|
||||
installedPreviouslyIndex = items.length;
|
||||
} else if (installedPreviouslyIndex === installedIndex && items.length) {
|
||||
installedPreviouslyIndex += 1;
|
||||
}
|
||||
|
||||
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
|
||||
|
||||
if (!appliedUpdates.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const appliedChanges: Update['changes'] = { new: [], fixed: [] };
|
||||
|
||||
appliedUpdates.forEach((u: Update) => {
|
||||
if (u.changes) {
|
||||
appliedChanges.new.push(...u.changes.new);
|
||||
appliedChanges.fixed.push(...u.changes.fixed);
|
||||
}
|
||||
});
|
||||
|
||||
const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], {
|
||||
changes: appliedChanges,
|
||||
});
|
||||
|
||||
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
||||
mergedUpdate.changes = null;
|
||||
}
|
||||
|
||||
return mergedUpdate;
|
||||
}
|
||||
|
||||
interface AppUpdatedModalContentProps {
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { version, prevVersion } = useSelector((state: AppState) => state.app);
|
||||
const { isPopulated, error, items } = useSelector(
|
||||
(state: AppState) => state.system.updates
|
||||
);
|
||||
const previousVersion = usePrevious(version);
|
||||
|
||||
const { onModalClose } = props;
|
||||
|
||||
const update = mergeUpdates(items, version, prevVersion);
|
||||
|
||||
const handleSeeChangesPress = useCallback(() => {
|
||||
window.location.href = `${window.Prowlarr.urlBase}/system/updates`;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUpdates());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (version !== previousVersion) {
|
||||
dispatch(fetchUpdates());
|
||||
}
|
||||
}, [version, previousVersion, dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('AppUpdated')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('AppUpdatedVersion', { version })}
|
||||
blockClassName={styles.version}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isPopulated && !error && !!update ? (
|
||||
<div>
|
||||
{update.changes ? (
|
||||
<div className={styles.maintenance}>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{update.changes ? (
|
||||
<div>
|
||||
<div className={styles.changes}>{translate('WhatsNew')}</div>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isPopulated && !error ? <LoadingIndicator /> : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={handleSeeChangesPress}>
|
||||
{translate('RecentChanges')}
|
||||
</Button>
|
||||
|
||||
<Button kind={kinds.PRIMARY} onPress={onModalClose}>
|
||||
{translate('Reload')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppUpdatedModalContent;
|
||||
@@ -1,78 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||
import AppUpdatedModalContent from './AppUpdatedModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app.version,
|
||||
(state) => state.app.prevVersion,
|
||||
(state) => state.system.updates,
|
||||
(version, prevVersion, updates) => {
|
||||
const {
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = updates;
|
||||
|
||||
return {
|
||||
version,
|
||||
prevVersion,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchFetchUpdates() {
|
||||
dispatch(fetchUpdates());
|
||||
},
|
||||
|
||||
onSeeChangesPress() {
|
||||
window.location = `${window.Prowlarr.urlBase}/system/updates`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class AppUpdatedModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchUpdates();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.version !== this.props.version) {
|
||||
this.props.dispatchFetchUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchUpdates,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AppUpdatedModalContent {...otherProps} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppUpdatedModalContentConnector.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
dispatchFetchUpdates: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector);
|
||||
@@ -1,50 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
||||
(
|
||||
theme
|
||||
) => {
|
||||
return {
|
||||
theme
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ theme, children }) {
|
||||
// Update the CSS Variables
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||
|
||||
// Loop through each array key and set the CSS Variables
|
||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
||||
// Based on our snippet from MDN
|
||||
document.documentElement.style.setProperty(
|
||||
`--${cssVariableKey}`,
|
||||
arrayOfVariableValues[index]
|
||||
);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables(theme);
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
ApplyTheme.propTypes = {
|
||||
theme: PropTypes.string.isRequired,
|
||||
children: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(ApplyTheme);
|
||||
33
frontend/src/App/ApplyTheme.tsx
Normal file
33
frontend/src/App/ApplyTheme.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
import AppState from './State/AppState';
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
||||
(theme) => {
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme() {
|
||||
const theme = useSelector(createThemeSelector());
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables();
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default ApplyTheme;
|
||||
@@ -1,5 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
@@ -10,36 +9,31 @@ import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ConnectionLostModal.css';
|
||||
|
||||
function ConnectionLostModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose
|
||||
} = props;
|
||||
interface ConnectionLostModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function ConnectionLostModal(props: ConnectionLostModalProps) {
|
||||
const { isOpen } = props;
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
location.reload();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('ConnectionLost')}
|
||||
</ModalHeader>
|
||||
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<ModalContent onModalClose={handleModalClose}>
|
||||
<ModalHeader>{translate('ConnectionLost')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('ConnectionLostToBackend')}
|
||||
</div>
|
||||
<div>{translate('ConnectionLostToBackend')}</div>
|
||||
|
||||
<div className={styles.automatic}>
|
||||
{translate('ConnectionLostReconnect')}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
<Button kind={kinds.PRIMARY} onPress={handleModalClose}>
|
||||
{translate('Reload')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
@@ -48,9 +42,4 @@ function ConnectionLostModal(props) {
|
||||
);
|
||||
}
|
||||
|
||||
ConnectionLostModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ConnectionLostModal;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ConnectionLostModal from './ConnectionLostModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onModalClose() {
|
||||
location.reload();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal);
|
||||
@@ -1,4 +1,6 @@
|
||||
import Column from 'Components/Table/Column';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
@@ -17,7 +19,19 @@ export interface AppSectionSaveState {
|
||||
}
|
||||
|
||||
export interface PagedAppSectionState {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
totalRecords?: number;
|
||||
}
|
||||
export interface TableAppSectionState {
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
export interface AppSectionFilterState<T> {
|
||||
selectedFilterKey: string;
|
||||
filters: PropertyFilter[];
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
}
|
||||
|
||||
export interface AppSectionSchemaState<T> {
|
||||
@@ -33,6 +47,7 @@ export interface AppSectionItemState<T> {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
pendingChanges: Partial<T>;
|
||||
item: T;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,20 @@ export interface CustomFilter {
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
version: string;
|
||||
prevVersion?: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
indexerHistory: IndexerHistoryAppState;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Column from 'Components/Table/Column';
|
||||
import History from 'typings/History';
|
||||
|
||||
interface HistoryAppState extends AppSectionState<History> {
|
||||
interface HistoryAppState
|
||||
extends AppSectionState<History>,
|
||||
AppSectionFilterState<History> {
|
||||
pageSize: number;
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ interface IndexerAppState
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
itemMap: Record<number, number>;
|
||||
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import AppSectionState, {
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import { IndexerCategory } from 'Indexer/Indexer';
|
||||
import Application from 'typings/Application';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import Notification from 'typings/Notification';
|
||||
@@ -22,6 +23,13 @@ export interface ApplicationAppState
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export interface IndexerCategoryAppState
|
||||
extends AppSectionState<IndexerCategory>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
@@ -35,6 +43,7 @@ interface SettingsAppState {
|
||||
appProfiles: AppProfileAppState;
|
||||
applications: ApplicationAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
indexerCategories: IndexerCategoryAppState;
|
||||
notifications: NotificationAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import Health from 'typings/Health';
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
import Task from 'typings/Task';
|
||||
import Update from 'typings/Update';
|
||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type HealthAppState = AppSectionState<Health>;
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type TaskAppState = AppSectionState<Task>;
|
||||
export type UpdateAppState = AppSectionState<Update>;
|
||||
|
||||
interface SystemAppState {
|
||||
health: HealthAppState;
|
||||
status: SystemStatusAppState;
|
||||
tasks: TaskAppState;
|
||||
updates: UpdateAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface CommandBody {
|
||||
lastStartTime: string;
|
||||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
seriesId?: number;
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
|
||||
@@ -46,6 +46,10 @@ class StackedBarChart extends Component {
|
||||
size: 14,
|
||||
family: defaultFontFamily
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
position: 'average'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
titleClassName,
|
||||
descriptionClassName,
|
||||
title,
|
||||
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={className}>
|
||||
<DescriptionListItemTitle
|
||||
className={titleClassName}
|
||||
>
|
||||
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
|
||||
}
|
||||
|
||||
DescriptionListItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
titleClassName: PropTypes.string,
|
||||
descriptionClassName: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
|
||||
@@ -63,11 +63,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
||||
<div>{info.componentStack}</div>
|
||||
)}
|
||||
|
||||
{
|
||||
<div className={styles.version}>
|
||||
Version: {window.Prowlarr.version}
|
||||
</div>
|
||||
}
|
||||
<div className={styles.version}>Version: {window.Prowlarr.version}</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { IndexerCategory } from 'Indexer/Indexer';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
const indexerCategoriesSelector = createSelector(
|
||||
(state: AppState) => state.settings.indexerCategories,
|
||||
(categories) => categories.items
|
||||
);
|
||||
|
||||
function CategoryFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
const categories: IndexerCategory[] = useSelector(indexerCategoriesSelector);
|
||||
|
||||
const tagList = categories.reduce(
|
||||
(acc: { id: number; name: string }[], element) => {
|
||||
acc.push({
|
||||
id: element.id,
|
||||
name: `${element.name} (${element.id})`,
|
||||
});
|
||||
|
||||
if (element.subCategories && element.subCategories.length > 0) {
|
||||
element.subCategories.forEach((subCat) => {
|
||||
acc.push({
|
||||
id: subCat.id,
|
||||
name: `${subCat.name} (${subCat.id})`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default CategoryFilterBuilderRowValue;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { maxBy } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
|
||||
if (id) {
|
||||
dispatchSetFilter({ selectedFilterKey: id });
|
||||
} else {
|
||||
const last = customFilters[customFilters.length -1];
|
||||
const last = maxBy(customFilters, 'id');
|
||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||
}
|
||||
|
||||
@@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
labelErrors: [
|
||||
{
|
||||
message: 'Label is required'
|
||||
message: translate('LabelIsRequired')
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -146,13 +147,13 @@ class FilterBuilderModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Custom Filter
|
||||
{translate('CustomFilter')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
Label
|
||||
{translate('Label')}
|
||||
</div>
|
||||
|
||||
<div className={styles.labelInputContainer}>
|
||||
|
||||
@@ -3,10 +3,13 @@ import React, { Component } from 'react';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import PrivacyFilterBuilderRowValue from './PrivacyFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
@@ -55,9 +58,15 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
case filterBuilderValueTypes.BOOL:
|
||||
return BoolFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.CATEGORY:
|
||||
return CategoryFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.DATE:
|
||||
return DateFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||
return HistoryEventTypeFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.INDEXER:
|
||||
return IndexerFilterBuilderRowValueConnector;
|
||||
|
||||
@@ -204,7 +213,7 @@ class FilterBuilderRow extends Component {
|
||||
key: name,
|
||||
value: typeof label === 'function' ? label() : label
|
||||
};
|
||||
}).sort((a, b) => a.value.localeCompare(b.value));
|
||||
}).sort(sortByProp('value'));
|
||||
|
||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { filterBuilderTypes } from 'Helpers/Props';
|
||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createTagListSelector() {
|
||||
@@ -38,7 +38,7 @@ function createTagListSelector() {
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).sort(sortByName);
|
||||
}, []).sort(sortByProp('name'));
|
||||
}
|
||||
|
||||
return _.uniqBy(items, 'id');
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { FilterBuilderProp } from 'App/State/AppState';
|
||||
|
||||
interface FilterBuilderRowOnChangeProps {
|
||||
name: string;
|
||||
value: unknown[];
|
||||
}
|
||||
|
||||
interface FilterBuilderRowValueProps {
|
||||
filterType?: string;
|
||||
filterValue: string | number | object | string[] | number[] | object[];
|
||||
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
|
||||
sectionItem: unknown[];
|
||||
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
|
||||
}
|
||||
|
||||
export default FilterBuilderRowValueProps;
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
const EVENT_TYPE_OPTIONS = [
|
||||
{
|
||||
id: 1,
|
||||
get name() {
|
||||
return translate('Grabbed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
get name() {
|
||||
return translate('IndexerRss');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
get name() {
|
||||
return translate('IndexerQuery');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
get name() {
|
||||
return translate('IndexerAuth');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function HistoryEventTypeFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
||||
}
|
||||
|
||||
export default HistoryEventTypeFilterBuilderRowValue;
|
||||
@@ -37,8 +37,8 @@ class CustomFilter extends Component {
|
||||
dispatchSetFilter
|
||||
} = this.props;
|
||||
|
||||
// Assume that delete and then unmounting means the delete was successful.
|
||||
// Moving this check to a ancestor would be more accurate, but would have
|
||||
// Assume that delete and then unmounting means the deletion was successful.
|
||||
// Moving this check to an ancestor would be more accurate, but would have
|
||||
// more boilerplate.
|
||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||
|
||||
@@ -5,6 +5,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 sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFilter from './CustomFilter';
|
||||
import styles from './CustomFiltersModalContent.css';
|
||||
@@ -30,22 +31,24 @@ function CustomFiltersModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
customFilters
|
||||
.sort((a, b) => sortByProp(a, b, 'label'))
|
||||
.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
|
||||
@@ -4,13 +4,13 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
||||
createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(appProfiles, includeNoChange, includeMixed) => {
|
||||
@@ -24,16 +24,20 @@ function createMapStateToProps() {
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: true
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
get value() {
|
||||
return `(${translate('Mixed')})`;
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
const availabilityOptions = [
|
||||
{ key: 'announced', value: 'Announced' },
|
||||
{ key: 'inCinemas', value: 'In Cinemas' },
|
||||
{ key: 'released', value: 'Released' },
|
||||
{ key: 'preDB', value: 'PreDB' }
|
||||
];
|
||||
|
||||
function AvailabilitySelectInput(props) {
|
||||
const values = [...availabilityOptions];
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeMixed
|
||||
} = props;
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
{...props}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
AvailabilitySelectInput.propTypes = {
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
includeMixed: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
AvailabilitySelectInput.defaultProps = {
|
||||
includeNoChange: false,
|
||||
includeMixed: false
|
||||
};
|
||||
|
||||
export default AvailabilitySelectInput;
|
||||
@@ -3,7 +3,8 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -21,16 +22,17 @@ function createMapStateToProps() {
|
||||
|
||||
const values = items
|
||||
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
|
||||
.sort(sortByName)
|
||||
.sort(sortByProp('name'))
|
||||
.map((downloadClient) => ({
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name
|
||||
value: downloadClient.name,
|
||||
hint: `(${downloadClient.id})`
|
||||
}));
|
||||
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
value: `(${translate('Any')})`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -271,26 +271,29 @@ class EnhancedSelectInput extends Component {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
};
|
||||
|
||||
onSelect = (value) => {
|
||||
if (Array.isArray(this.props.value)) {
|
||||
let newValue = null;
|
||||
const index = this.props.value.indexOf(value);
|
||||
onSelect = (newValue) => {
|
||||
const { name, value, values, onChange } = this.props;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
let arrayValue = null;
|
||||
const index = value.indexOf(newValue);
|
||||
|
||||
if (index === -1) {
|
||||
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
|
||||
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
|
||||
} else {
|
||||
newValue = [...this.props.value];
|
||||
newValue.splice(index, 1);
|
||||
arrayValue = [...value];
|
||||
arrayValue.splice(index, 1);
|
||||
}
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: newValue
|
||||
onChange({
|
||||
name,
|
||||
value: arrayValue
|
||||
});
|
||||
} else {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value
|
||||
onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -485,7 +488,7 @@ class EnhancedSelectInput extends Component {
|
||||
values.map((v, index) => {
|
||||
const hasParent = v.parentKey !== undefined;
|
||||
const depth = hasParent ? 1 : 0;
|
||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
||||
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
|
||||
return (
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
|
||||
@@ -34,7 +34,8 @@ function getSelectOptions(items) {
|
||||
key: option.value,
|
||||
value: option.name,
|
||||
hint: option.hint,
|
||||
parentKey: option.parentValue
|
||||
parentKey: option.parentValue,
|
||||
isDisabled: option.isDisabled
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -147,7 +148,7 @@ EnhancedSelectInputConnector.propTypes = {
|
||||
provider: PropTypes.string.isRequired,
|
||||
providerData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectOptionsProviderAction: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.validationFailures {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
1
frontend/src/Components/Form/Form.css.d.ts
vendored
1
frontend/src/Components/Form/Form.css.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'details': string;
|
||||
'validationFailures': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import styles from './Form.css';
|
||||
|
||||
function Form(props) {
|
||||
@@ -26,6 +28,18 @@ function Form(props) {
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{error.errorMessage}
|
||||
|
||||
{
|
||||
error.detailedDescription ?
|
||||
<Tooltip
|
||||
className={styles.details}
|
||||
anchor={<Icon name={icons.INFO} />}
|
||||
tooltip={error.detailedDescription}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</Alert>
|
||||
);
|
||||
})
|
||||
@@ -39,6 +53,18 @@ function Form(props) {
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{warning.errorMessage}
|
||||
|
||||
{
|
||||
warning.detailedDescription ?
|
||||
<Tooltip
|
||||
className={styles.details}
|
||||
anchor={<Icon name={icons.INFO} />}
|
||||
tooltip={warning.detailedDescription}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</Alert>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@ import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AppProfileSelectInputConnector from './AppProfileSelectInputConnector';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
@@ -37,9 +36,6 @@ function getComponent(type) {
|
||||
case inputTypes.AUTO_COMPLETE:
|
||||
return AutoCompleteInput;
|
||||
|
||||
case inputTypes.AVAILABILITY_SELECT:
|
||||
return AvailabilitySelectInput;
|
||||
|
||||
case inputTypes.CAPTCHA:
|
||||
return CaptchaInputConnector;
|
||||
|
||||
@@ -260,6 +256,7 @@ FormInputGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
values: PropTypes.arrayOf(PropTypes.any),
|
||||
isFloat: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
min: PropTypes.number,
|
||||
|
||||
@@ -25,7 +25,7 @@ function FormInputHelpText(props) {
|
||||
isCheckInput && styles.isCheckInput
|
||||
)}
|
||||
>
|
||||
<div dangerouslySetInnerHTML={{ __html: text }} />
|
||||
{text}
|
||||
|
||||
{
|
||||
link ?
|
||||
|
||||
@@ -4,14 +4,14 @@ 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 sortByProp from 'Utilities/Array/sortByProp';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
createSortedSectionSelector('indexers', sortByName),
|
||||
createSortedSectionSelector('indexers', sortByProp('name')),
|
||||
(value, indexers) => {
|
||||
const values = [];
|
||||
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.input {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
font-family: $passwordFamily;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './PasswordInput.css';
|
||||
|
||||
// Prevent a user from copying (or cutting) the password from the input
|
||||
function onCopy(e) {
|
||||
@@ -13,17 +11,14 @@ function PasswordInput(props) {
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
type="password"
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
PasswordInput.propTypes = {
|
||||
className: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
PasswordInput.defaultProps = {
|
||||
className: styles.input
|
||||
...TextInput.props
|
||||
};
|
||||
|
||||
export default PasswordInput;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
.select {
|
||||
@add-mixin truncate;
|
||||
|
||||
composes: input from '~Components/Form/Input.css';
|
||||
|
||||
padding: 0 11px;
|
||||
padding: 0 30px 0 11px;
|
||||
background-image: none, linear-gradient(-135deg, transparent 50%, var(--inputBackgroundColor) 50%), linear-gradient(-225deg, transparent 50%, var(--inputBackgroundColor) 50%), linear-gradient(var(--inputBackgroundColor) 42%, var(--textColor) 42%);
|
||||
background-position: right 30px center, right bottom, right bottom, right bottom;
|
||||
background-size: 1px 100%, 35px 27px, 30px 35px, 30px 100%;
|
||||
background-repeat: no-repeat;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterMenuItem from './FilterMenuItem';
|
||||
import MenuContent from './MenuContent';
|
||||
@@ -40,18 +41,26 @@ class FilterMenuContent extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
customFilters.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
customFilters.length > 0 ?
|
||||
<MenuItemSeparator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
customFilters
|
||||
.sort(sortByProp('label'))
|
||||
.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import IndexerSearchInputConnector from './IndexerSearchInputConnector';
|
||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
import styles from './PageHeader.css';
|
||||
|
||||
class PageHeader extends Component {
|
||||
@@ -87,7 +87,8 @@ class PageHeader extends Component {
|
||||
to="https://translate.servarr.com/projects/servarr/prowlarr/"
|
||||
size={24}
|
||||
/>
|
||||
<PageHeaderActionsMenuConnector
|
||||
|
||||
<PageHeaderActionsMenu
|
||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './PageHeaderActionsMenu.css';
|
||||
|
||||
function PageHeaderActionsMenu(props) {
|
||||
const {
|
||||
formsAuth,
|
||||
onKeyboardShortcutsPress,
|
||||
onRestartPress,
|
||||
onShutdownPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('Menu')}
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.KEYBOARD}
|
||||
/>
|
||||
{translate('KeyboardShortcuts')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem onPress={onRestartPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.RESTART}
|
||||
/>
|
||||
{translate('Restart')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onPress={onShutdownPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.SHUTDOWN}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
{translate('Shutdown')}
|
||||
</MenuItem>
|
||||
|
||||
{
|
||||
formsAuth &&
|
||||
<div className={styles.separator} />
|
||||
}
|
||||
|
||||
{
|
||||
formsAuth &&
|
||||
<MenuItem
|
||||
to={`${window.Prowlarr.urlBase}/logout`}
|
||||
noRouter={true}
|
||||
>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.LOGOUT}
|
||||
/>
|
||||
Logout
|
||||
</MenuItem>
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PageHeaderActionsMenu.propTypes = {
|
||||
formsAuth: PropTypes.bool.isRequired,
|
||||
onKeyboardShortcutsPress: PropTypes.func.isRequired,
|
||||
onRestartPress: PropTypes.func.isRequired,
|
||||
onShutdownPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default PageHeaderActionsMenu;
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Icon from 'Components/Icon';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './PageHeaderActionsMenu.css';
|
||||
|
||||
interface PageHeaderActionsMenuProps {
|
||||
onKeyboardShortcutsPress(): void;
|
||||
}
|
||||
|
||||
function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
|
||||
const { onKeyboardShortcutsPress } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { authentication, isDocker } = useSelector(
|
||||
(state: AppState) => state.system.status.item
|
||||
);
|
||||
|
||||
const formsAuth = authentication === 'forms';
|
||||
|
||||
const handleRestartPress = useCallback(() => {
|
||||
dispatch(restart());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleShutdownPress = useCallback(() => {
|
||||
dispatch(shutdown());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon name={icons.INTERACTIVE} title={translate('Menu')} />
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||
<Icon className={styles.itemIcon} name={icons.KEYBOARD} />
|
||||
{translate('KeyboardShortcuts')}
|
||||
</MenuItem>
|
||||
|
||||
{isDocker ? null : (
|
||||
<>
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem onPress={handleRestartPress}>
|
||||
<Icon className={styles.itemIcon} name={icons.RESTART} />
|
||||
{translate('Restart')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onPress={handleShutdownPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.SHUTDOWN}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
{translate('Shutdown')}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{formsAuth ? (
|
||||
<>
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem
|
||||
to={`${window.Prowlarr.urlBase}/logout`}
|
||||
noRouter={true}
|
||||
>
|
||||
<Icon className={styles.itemIcon} name={icons.LOGOUT} />
|
||||
{translate('Logout')}
|
||||
</MenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageHeaderActionsMenu;
|
||||
@@ -1,56 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.status,
|
||||
(status) => {
|
||||
return {
|
||||
formsAuth: status.item.authentication === 'forms'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
restart,
|
||||
shutdown
|
||||
};
|
||||
|
||||
class PageHeaderActionsMenuConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRestartPress = () => {
|
||||
this.props.restart();
|
||||
};
|
||||
|
||||
onShutdownPress = () => {
|
||||
this.props.shutdown();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageHeaderActionsMenu
|
||||
{...this.props}
|
||||
onRestartPress={this.onRestartPress}
|
||||
onShutdownPress={this.onShutdownPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageHeaderActionsMenuConnector.propTypes = {
|
||||
restart: PropTypes.func.isRequired,
|
||||
shutdown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);
|
||||
@@ -1,8 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
||||
import AppUpdatedModal from 'App/AppUpdatedModal';
|
||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||
import ConnectionLostModal from 'App/ConnectionLostModal';
|
||||
import SignalRConnector from 'Components/SignalRConnector';
|
||||
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||
@@ -102,12 +102,12 @@ class Page extends Component {
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<AppUpdatedModalConnector
|
||||
<AppUpdatedModal
|
||||
isOpen={this.state.isUpdatedModalOpen}
|
||||
onModalClose={this.onUpdatedModalClose}
|
||||
/>
|
||||
|
||||
<ConnectionLostModalConnector
|
||||
<ConnectionLostModal
|
||||
isOpen={this.state.isConnectionLostModalOpen}
|
||||
onModalClose={this.onConnectionLostModalClose}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ import Scroller from 'Components/Scroller/Scroller';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector';
|
||||
import HealthStatus from 'System/Status/Health/HealthStatus';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MessagesConnector from './Messages/MessagesConnector';
|
||||
import PageSidebarItem from './PageSidebarItem';
|
||||
@@ -87,7 +87,7 @@ const links = [
|
||||
{
|
||||
title: () => translate('Status'),
|
||||
to: '/system/status',
|
||||
statusComponent: HealthStatusConnector
|
||||
statusComponent: HealthStatus
|
||||
},
|
||||
{
|
||||
title: () => translate('Tasks'),
|
||||
|
||||
@@ -2,9 +2,11 @@ import React from 'react';
|
||||
|
||||
type PropertyFunction<T> = () => T;
|
||||
|
||||
// TODO: Convert to generic so `name` can be a type
|
||||
interface Column {
|
||||
name: string;
|
||||
label: string | PropertyFunction<string> | React.ReactNode;
|
||||
className?: string;
|
||||
columnLabel?: string;
|
||||
isSortable?: boolean;
|
||||
isVisible: boolean;
|
||||
|
||||
54
frontend/src/Components/Table/usePaging.ts
Normal file
54
frontend/src/Components/Table/usePaging.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
interface PagingOptions {
|
||||
page: number;
|
||||
totalPages: number;
|
||||
gotoPage: ({ page }: { page: number }) => void;
|
||||
}
|
||||
|
||||
function usePaging(options: PagingOptions) {
|
||||
const { page, totalPages, gotoPage } = options;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleFirstPagePress = useCallback(() => {
|
||||
dispatch(gotoPage({ page: 1 }));
|
||||
}, [dispatch, gotoPage]);
|
||||
|
||||
const handlePreviousPagePress = useCallback(() => {
|
||||
dispatch(gotoPage({ page: Math.max(page - 1, 1) }));
|
||||
}, [page, dispatch, gotoPage]);
|
||||
|
||||
const handleNextPagePress = useCallback(() => {
|
||||
dispatch(gotoPage({ page: Math.min(page + 1, totalPages) }));
|
||||
}, [page, totalPages, dispatch, gotoPage]);
|
||||
|
||||
const handleLastPagePress = useCallback(() => {
|
||||
dispatch(gotoPage({ page: totalPages }));
|
||||
}, [totalPages, dispatch, gotoPage]);
|
||||
|
||||
const handlePageSelect = useCallback(
|
||||
(page: number) => {
|
||||
dispatch(gotoPage({ page }));
|
||||
},
|
||||
[dispatch, gotoPage]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
};
|
||||
}, [
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
]);
|
||||
}
|
||||
|
||||
export default usePaging;
|
||||
@@ -1,14 +1,15 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import Label from './Label';
|
||||
import styles from './TagList.css';
|
||||
|
||||
function TagList({ tags, tagList }) {
|
||||
const sortedTags = tags
|
||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||
.filter((t) => t !== undefined)
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
.filter((tag) => !!tag)
|
||||
.sort(sortByProp('label'));
|
||||
|
||||
return (
|
||||
<div className={styles.tags}>
|
||||
|
||||
@@ -25,14 +25,3 @@
|
||||
font-family: 'Ubuntu Mono';
|
||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
||||
/*
|
||||
* text-security-disc
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: 'text-security-disc';
|
||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -15,5 +15,5 @@
|
||||
"start_url": "../../../../",
|
||||
"theme_color": "#3a3f51",
|
||||
"background_color": "#3a3f51",
|
||||
"display": "minimal-ui"
|
||||
"display": "standalone"
|
||||
}
|
||||
|
||||
3
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
3
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
type DownloadProtocol = 'usenet' | 'torrent' | 'unknown';
|
||||
|
||||
export default DownloadProtocol;
|
||||
9
frontend/src/Helpers/Hooks/useCurrentPage.ts
Normal file
9
frontend/src/Helpers/Hooks/useCurrentPage.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
function useCurrentPage() {
|
||||
const history = useHistory();
|
||||
|
||||
return history.action === 'POP';
|
||||
}
|
||||
|
||||
export default useCurrentPage;
|
||||
17
frontend/src/Helpers/Hooks/useModalOpenState.ts
Normal file
17
frontend/src/Helpers/Hooks/useModalOpenState.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export default function useModalOpenState(
|
||||
initialState: boolean
|
||||
): [boolean, () => void, () => void] {
|
||||
const [isOpen, setIsOpen] = useState(initialState);
|
||||
|
||||
const setModalOpen = useCallback(() => {
|
||||
setIsOpen(true);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const setModalClosed = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, [setIsOpen]);
|
||||
|
||||
return [isOpen, setModalOpen, setModalClosed];
|
||||
}
|
||||
3
frontend/src/Helpers/Props/TooltipPosition.ts
Normal file
3
frontend/src/Helpers/Props/TooltipPosition.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
type TooltipPosition = 'top' | 'right' | 'bottom' | 'left';
|
||||
|
||||
export default TooltipPosition;
|
||||
@@ -2,9 +2,10 @@ export const BOOL = 'bool';
|
||||
export const BYTES = 'bytes';
|
||||
export const DATE = 'date';
|
||||
export const DEFAULT = 'default';
|
||||
export const HISTORY_EVENT_TYPE = 'historyEventType';
|
||||
export const INDEXER = 'indexer';
|
||||
export const PROTOCOL = 'protocol';
|
||||
export const PRIVACY = 'privacy';
|
||||
export const APP_PROFILE = 'appProfile';
|
||||
export const MOVIE_STATUS = 'movieStatus';
|
||||
export const CATEGORY = 'category';
|
||||
export const TAG = 'tag';
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
faChevronCircleRight as fasChevronCircleRight,
|
||||
faChevronCircleUp as fasChevronCircleUp,
|
||||
faCircle as fasCircle,
|
||||
faCircleDown as fasCircleDown,
|
||||
faCloud as fasCloud,
|
||||
faCloudDownloadAlt as fasCloudDownloadAlt,
|
||||
faCog as fasCog,
|
||||
@@ -141,6 +142,7 @@ export const CHECK_INDETERMINATE = fasMinus;
|
||||
export const CHECK_CIRCLE = fasCheckCircle;
|
||||
export const CHECK_SQUARE = fasSquareCheck;
|
||||
export const CIRCLE = fasCircle;
|
||||
export const CIRCLE_DOWN = fasCircleDown;
|
||||
export const CIRCLE_OUTLINE = farCircle;
|
||||
export const CLEAR = fasTrashAlt;
|
||||
export const CLIPBOARD = fasCopy;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export const AUTO_COMPLETE = 'autoComplete';
|
||||
export const APP_PROFILE_SELECT = 'appProfileSelect';
|
||||
export const AVAILABILITY_SELECT = 'availabilitySelect';
|
||||
export const CAPTCHA = 'captcha';
|
||||
export const CARDIGANNCAPTCHA = 'cardigannCaptcha';
|
||||
export const CHECK = 'check';
|
||||
@@ -27,7 +26,6 @@ export const TAG_SELECT = 'tagSelect';
|
||||
export const all = [
|
||||
AUTO_COMPLETE,
|
||||
APP_PROFILE_SELECT,
|
||||
AVAILABILITY_SELECT,
|
||||
CAPTCHA,
|
||||
CARDIGANNCAPTCHA,
|
||||
CHECK,
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import Link from 'Components/Link/Link';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryDetails.css';
|
||||
|
||||
@@ -10,7 +11,10 @@ function HistoryDetails(props) {
|
||||
const {
|
||||
indexer,
|
||||
eventType,
|
||||
data
|
||||
date,
|
||||
data,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
if (eventType === 'indexerQuery' || eventType === 'indexerRss') {
|
||||
@@ -21,7 +25,10 @@ function HistoryDetails(props) {
|
||||
limit,
|
||||
offset,
|
||||
source,
|
||||
url
|
||||
host,
|
||||
url,
|
||||
elapsedTime,
|
||||
cached
|
||||
} = data;
|
||||
|
||||
return (
|
||||
@@ -86,6 +93,15 @@ function HistoryDetails(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('Host')}
|
||||
data={host}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
@@ -94,6 +110,24 @@ function HistoryDetails(props) {
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
elapsedTime ?
|
||||
<DescriptionListItem
|
||||
title={translate('ElapsedTime')}
|
||||
data={`${elapsedTime}ms${cached === '1' ? ' (cached)' : ''}`}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
date ?
|
||||
<DescriptionListItem
|
||||
title={translate('Date')}
|
||||
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
@@ -101,10 +135,19 @@ function HistoryDetails(props) {
|
||||
if (eventType === 'releaseGrabbed') {
|
||||
const {
|
||||
source,
|
||||
host,
|
||||
grabTitle,
|
||||
url
|
||||
url,
|
||||
publishedDate,
|
||||
infoUrl,
|
||||
downloadClient,
|
||||
downloadClientName,
|
||||
elapsedTime,
|
||||
grabMethod
|
||||
} = data;
|
||||
|
||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
{
|
||||
@@ -125,6 +168,15 @@ function HistoryDetails(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('Host')}
|
||||
data={host}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
@@ -134,6 +186,33 @@ function HistoryDetails(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
infoUrl ?
|
||||
<DescriptionListItem
|
||||
title={translate('InfoUrl')}
|
||||
data={<Link to={infoUrl}>{infoUrl}</Link>}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
publishedDate ?
|
||||
<DescriptionListItem
|
||||
title={translate('PublishedDate')}
|
||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
downloadClientNameInfo ?
|
||||
<DescriptionListItem
|
||||
title={translate('DownloadClient')}
|
||||
data={downloadClientNameInfo}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
@@ -142,11 +221,40 @@ function HistoryDetails(props) {
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
elapsedTime ?
|
||||
<DescriptionListItem
|
||||
title={translate('ElapsedTime')}
|
||||
data={`${elapsedTime}ms`}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
grabMethod ?
|
||||
<DescriptionListItem
|
||||
title={translate('Redirected')}
|
||||
data={grabMethod.toLowerCase() === 'redirect' ? translate('Yes') : translate('No')}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
date ?
|
||||
<DescriptionListItem
|
||||
title={translate('Date')}
|
||||
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'indexerAuth') {
|
||||
const { elapsedTime } = data;
|
||||
|
||||
return (
|
||||
<DescriptionList
|
||||
descriptionClassName={styles.description}
|
||||
@@ -160,6 +268,24 @@ function HistoryDetails(props) {
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
elapsedTime ?
|
||||
<DescriptionListItem
|
||||
title={translate('ElapsedTime')}
|
||||
data={`${elapsedTime}ms`}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
date ?
|
||||
<DescriptionListItem
|
||||
title={translate('Date')}
|
||||
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
@@ -171,6 +297,15 @@ function HistoryDetails(props) {
|
||||
title={translate('Name')}
|
||||
data={data.query}
|
||||
/>
|
||||
|
||||
{
|
||||
date ?
|
||||
<DescriptionListItem
|
||||
title={translate('Date')}
|
||||
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
@@ -178,6 +313,7 @@ function HistoryDetails(props) {
|
||||
HistoryDetails.propTypes = {
|
||||
indexer: PropTypes.object.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
|
||||
@@ -29,6 +29,7 @@ function HistoryDetailsModal(props) {
|
||||
isOpen,
|
||||
eventType,
|
||||
indexer,
|
||||
date,
|
||||
data,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
@@ -49,6 +50,7 @@ function HistoryDetailsModal(props) {
|
||||
<HistoryDetails
|
||||
eventType={eventType}
|
||||
indexer={indexer}
|
||||
date={date}
|
||||
data={data}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
@@ -71,6 +73,7 @@ HistoryDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.object.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -15,6 +15,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryFilterModal from './HistoryFilterModal';
|
||||
import HistoryOptionsConnector from './HistoryOptionsConnector';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
@@ -63,6 +64,7 @@ class History extends Component {
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
totalRecords,
|
||||
onFilterSelect,
|
||||
onFirstPagePress,
|
||||
@@ -108,7 +110,8 @@ class History extends Component {
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={HistoryFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
@@ -193,8 +196,9 @@ History.propTypes = {
|
||||
indexersError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onFirstPagePress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import History from './History';
|
||||
@@ -14,13 +15,15 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.history,
|
||||
(state) => state.indexers,
|
||||
createCustomFiltersSelector('history'),
|
||||
createCommandExecutingSelector(commandNames.CLEAR_HISTORY),
|
||||
(history, indexers, isHistoryClearing) => {
|
||||
(history, indexers, customFilters, isHistoryClearing) => {
|
||||
return {
|
||||
isIndexersFetching: indexers.isFetching,
|
||||
isIndexersPopulated: indexers.isPopulated,
|
||||
indexersError: indexers.error,
|
||||
isHistoryClearing,
|
||||
customFilters,
|
||||
...history
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,21 +20,22 @@ function getIconName(eventType) {
|
||||
}
|
||||
}
|
||||
|
||||
function getIconKind(successful) {
|
||||
switch (successful) {
|
||||
case false:
|
||||
return kinds.DANGER;
|
||||
default:
|
||||
return kinds.DEFAULT;
|
||||
function getIconKind(successful, redirect) {
|
||||
if (redirect) {
|
||||
return kinds.INFO;
|
||||
} else if (!successful) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getTooltip(eventType, data, indexer) {
|
||||
function getTooltip(eventType, data, indexer, redirect) {
|
||||
switch (eventType) {
|
||||
case 'indexerQuery':
|
||||
return `Query "${data.query}" sent to ${indexer.name}`;
|
||||
case 'releaseGrabbed':
|
||||
return `Release grabbed from ${indexer.name}`;
|
||||
return redirect ? `Release grabbed via redirect from ${indexer.name}` : `Release grabbed from ${indexer.name}`;
|
||||
case 'indexerAuth':
|
||||
return `Auth attempted for ${indexer.name}`;
|
||||
case 'indexerRss':
|
||||
@@ -45,9 +46,12 @@ function getTooltip(eventType, data, indexer) {
|
||||
}
|
||||
|
||||
function HistoryEventTypeCell({ eventType, successful, data, indexer }) {
|
||||
const { grabMethod } = data;
|
||||
const redirect = grabMethod && grabMethod.toLowerCase() === 'redirect';
|
||||
|
||||
const iconName = getIconName(eventType);
|
||||
const iconKind = getIconKind(successful);
|
||||
const tooltip = getTooltip(eventType, data, indexer);
|
||||
const iconKind = getIconKind(successful, redirect);
|
||||
const tooltip = getTooltip(eventType, data, indexer, redirect);
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
54
frontend/src/History/HistoryFilterModal.tsx
Normal file
54
frontend/src/History/HistoryFilterModal.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
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 { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||
|
||||
function createHistorySelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface HistoryFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||
const sectionItems = useSelector(createHistorySelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'history';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setHistoryFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -331,6 +331,21 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'host') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.indexer}
|
||||
>
|
||||
{
|
||||
data.host ?
|
||||
data.host :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'elapsedTime') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -355,8 +370,9 @@ class HistoryRow extends Component {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={date}
|
||||
className={styles.date}
|
||||
date={date}
|
||||
includeSeconds={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -393,6 +409,7 @@ class HistoryRow extends Component {
|
||||
<HistoryDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
eventType={eventType}
|
||||
date={date}
|
||||
data={data}
|
||||
indexer={indexer}
|
||||
isMarkingAsFailed={isMarkingAsFailed}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
|
||||
import styles from './AddIndexerModal.css';
|
||||
|
||||
function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
onModalClose={onModalClose}
|
||||
className={styles.modal}
|
||||
>
|
||||
<AddIndexerModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
onSelectIndexer={onSelectIndexer}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddIndexerModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSelectIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerModal;
|
||||
44
frontend/src/Indexer/Add/AddIndexerModal.tsx
Normal file
44
frontend/src/Indexer/Add/AddIndexerModal.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { clearIndexerSchema } from 'Store/Actions/indexerActions';
|
||||
import AddIndexerModalContent from './AddIndexerModalContent';
|
||||
import styles from './AddIndexerModal.css';
|
||||
|
||||
interface AddIndexerModalProps {
|
||||
isOpen: boolean;
|
||||
onSelectIndexer(): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function AddIndexerModal({
|
||||
isOpen,
|
||||
onSelectIndexer,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: AddIndexerModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onModalClosePress = useCallback(() => {
|
||||
dispatch(clearIndexerSchema());
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
className={styles.modal}
|
||||
onModalClose={onModalClosePress}
|
||||
>
|
||||
<AddIndexerModalContent
|
||||
{...otherProps}
|
||||
onSelectIndexer={onSelectIndexer}
|
||||
onModalClose={onModalClosePress}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddIndexerModal;
|
||||
@@ -19,12 +19,18 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
.notice {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ interface CssExports {
|
||||
'indexers': string;
|
||||
'modalBody': string;
|
||||
'modalFooter': string;
|
||||
'notice': string;
|
||||
'scroller': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
|
||||
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectIndexerRow from './SelectIndexerRow';
|
||||
import styles from './AddIndexerModalContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Protocol'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sortName',
|
||||
label: () => translate('Name'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
label: () => translate('Language'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: () => translate('Description'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'privacy',
|
||||
label: () => translate('Privacy'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
label: () => translate('Categories'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
const protocols = [
|
||||
{
|
||||
key: 'torrent',
|
||||
value: 'torrent'
|
||||
},
|
||||
{
|
||||
key: 'usenet',
|
||||
value: 'nzb'
|
||||
}
|
||||
];
|
||||
|
||||
const privacyLevels = [
|
||||
{
|
||||
key: 'private',
|
||||
get value() {
|
||||
return translate('Private');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'semiPrivate',
|
||||
get value() {
|
||||
return translate('SemiPrivate');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'public',
|
||||
get value() {
|
||||
return translate('Public');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class AddIndexerModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
filter: '',
|
||||
filterProtocols: [],
|
||||
filterLanguages: [],
|
||||
filterPrivacyLevels: [],
|
||||
filterCategories: []
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFilterChange = ({ value }) => {
|
||||
this.setState({ filter: value });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
indexers,
|
||||
onIndexerSelect,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
onSortPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const languages = Array.from(new Set(indexers.map(({ language }) => language)))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((language) => ({ key: language, value: language }));
|
||||
|
||||
const filteredIndexers = indexers.filter((indexer) => {
|
||||
const {
|
||||
filter,
|
||||
filterProtocols,
|
||||
filterLanguages,
|
||||
filterPrivacyLevels,
|
||||
filterCategories
|
||||
} = this.state;
|
||||
|
||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterProtocols.length && !filterProtocols.includes(indexer.protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterLanguages.length && !filterLanguages.includes(indexer.language)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterPrivacyLevels.length && !filterPrivacyLevels.includes(indexer.privacy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterCategories.length) {
|
||||
const { categories = [] } = indexer.capabilities || {};
|
||||
const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)];
|
||||
const flatCategories = categories
|
||||
.filter((item) => item.id < 100000)
|
||||
.flatMap(flat);
|
||||
|
||||
if (!filterCategories.every((item) => flatCategories.includes(item))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers'));
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AddIndexer')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder={translate('FilterPlaceHolder')}
|
||||
name="filter"
|
||||
value={this.state.filter}
|
||||
autoFocus={true}
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Protocol')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerProtocols"
|
||||
value={this.state.filterProtocols}
|
||||
values={protocols}
|
||||
onChange={({ value }) => this.setState({ filterProtocols: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Language')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerLanguages"
|
||||
value={this.state.filterLanguages}
|
||||
values={languages}
|
||||
onChange={({ value }) => this.setState({ filterLanguages: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Privacy')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerPrivacyLevels"
|
||||
value={this.state.filterPrivacyLevels}
|
||||
values={privacyLevels}
|
||||
onChange={({ value }) => this.setState({ filterPrivacyLevels: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Categories')}</label>
|
||||
<NewznabCategorySelectInputConnector
|
||||
name="indexerCategories"
|
||||
value={this.state.filterCategories}
|
||||
onChange={({ value }) => this.setState({ filterCategories: value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
kind={kinds.INFO}
|
||||
className={styles.alert}
|
||||
>
|
||||
<div>
|
||||
{translate('ProwlarrSupportsAnyIndexer')}
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
autoFocus={false}
|
||||
>
|
||||
{
|
||||
isFetching ? <LoadingIndicator /> : null
|
||||
}
|
||||
{
|
||||
error ? <Alert kind={kinds.DANGER}>{errorMessage}</Alert> : null
|
||||
}
|
||||
{
|
||||
isPopulated && !!indexers.length ?
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
filteredIndexers.map((indexer) => (
|
||||
<SelectIndexerRow
|
||||
key={`${indexer.implementation}-${indexer.name}`}
|
||||
implementation={indexer.implementation}
|
||||
implementationName={indexer.implementationName}
|
||||
{...indexer}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
{
|
||||
isPopulated && !!indexers.length && !filteredIndexers.length ?
|
||||
<Alert
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('NoIndexersFound')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
</Scroller>
|
||||
</ModalBody>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onIndexerSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerModalContent;
|
||||
434
frontend/src/Indexer/Add/AddIndexerModalContent.tsx
Normal file
434
frontend/src/Indexer/Add/AddIndexerModalContent.tsx
Normal file
@@ -0,0 +1,434 @@
|
||||
import { some } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import IndexerAppState from 'App/State/IndexerAppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
|
||||
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||
import Indexer, { IndexerCategory } from 'Indexer/Indexer';
|
||||
import {
|
||||
fetchIndexerSchema,
|
||||
selectIndexerSchema,
|
||||
setIndexerSchemaSort,
|
||||
} from 'Store/Actions/indexerActions';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { SortCallback } from 'typings/callbacks';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectIndexerRow from './SelectIndexerRow';
|
||||
import styles from './AddIndexerModalContent.css';
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Protocol'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'sortName',
|
||||
label: () => translate('Name'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
label: () => translate('Language'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: () => translate('Description'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'privacy',
|
||||
label: () => translate('Privacy'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
label: () => translate('Categories'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
const PROTOCOLS = [
|
||||
{
|
||||
key: 'torrent',
|
||||
value: 'torrent',
|
||||
},
|
||||
{
|
||||
key: 'usenet',
|
||||
value: 'nzb',
|
||||
},
|
||||
];
|
||||
|
||||
const PRIVACY_LEVELS = [
|
||||
{
|
||||
key: 'private',
|
||||
get value() {
|
||||
return translate('Private');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'semiPrivate',
|
||||
get value() {
|
||||
return translate('SemiPrivate');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'public',
|
||||
get value() {
|
||||
return translate('Public');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
interface IndexerSchema extends Indexer {
|
||||
isExistingIndexer: boolean;
|
||||
}
|
||||
|
||||
function createAddIndexersSelector() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('indexers.schema'),
|
||||
createAllIndexersSelector(),
|
||||
(indexers: IndexerAppState, allIndexers) => {
|
||||
const { isFetching, isPopulated, error, items, sortDirection, sortKey } =
|
||||
indexers;
|
||||
|
||||
const indexerList: IndexerSchema[] = items.map((item) => {
|
||||
const { definitionName } = item;
|
||||
return {
|
||||
...item,
|
||||
isExistingIndexer: some(allIndexers, { definitionName }),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
indexers: indexerList,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface AddIndexerModalContentProps {
|
||||
onSelectIndexer(): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function AddIndexerModalContent(props: AddIndexerModalContentProps) {
|
||||
const { onSelectIndexer, onModalClose } = props;
|
||||
|
||||
const { isFetching, isPopulated, error, indexers, sortKey, sortDirection } =
|
||||
useSelector(createAddIndexersSelector());
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
const [filterProtocols, setFilterProtocols] = useState<string[]>([]);
|
||||
const [filterLanguages, setFilterLanguages] = useState<string[]>([]);
|
||||
const [filterPrivacyLevels, setFilterPrivacyLevels] = useState<string[]>([]);
|
||||
const [filterCategories, setFilterCategories] = useState<number[]>([]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
dispatch(fetchIndexerSchema());
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
setFilter(value);
|
||||
},
|
||||
[setFilter]
|
||||
);
|
||||
|
||||
const onFilterProtocolsChange = useCallback(
|
||||
({ value }: { value: string[] }) => {
|
||||
setFilterProtocols(value);
|
||||
},
|
||||
[setFilterProtocols]
|
||||
);
|
||||
|
||||
const onFilterLanguagesChange = useCallback(
|
||||
({ value }: { value: string[] }) => {
|
||||
setFilterLanguages(value);
|
||||
},
|
||||
[setFilterLanguages]
|
||||
);
|
||||
|
||||
const onFilterPrivacyLevelsChange = useCallback(
|
||||
({ value }: { value: string[] }) => {
|
||||
setFilterPrivacyLevels(value);
|
||||
},
|
||||
[setFilterPrivacyLevels]
|
||||
);
|
||||
|
||||
const onFilterCategoriesChange = useCallback(
|
||||
({ value }: { value: number[] }) => {
|
||||
setFilterCategories(value);
|
||||
},
|
||||
[setFilterCategories]
|
||||
);
|
||||
|
||||
const onIndexerSelect = useCallback(
|
||||
({
|
||||
implementation,
|
||||
implementationName,
|
||||
name,
|
||||
}: {
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
name: string;
|
||||
}) => {
|
||||
dispatch(
|
||||
selectIndexerSchema({
|
||||
implementation,
|
||||
implementationName,
|
||||
name,
|
||||
})
|
||||
);
|
||||
|
||||
onSelectIndexer();
|
||||
},
|
||||
[dispatch, onSelectIndexer]
|
||||
);
|
||||
|
||||
const onSortPress = useCallback<SortCallback>(
|
||||
(sortKey, sortDirection) => {
|
||||
dispatch(setIndexerSchemaSort({ sortKey, sortDirection }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const languages = useMemo(
|
||||
() =>
|
||||
Array.from(new Set(indexers.map(({ language }) => language)))
|
||||
.map((language) => ({ key: language, value: language }))
|
||||
.sort(sortByProp('value')),
|
||||
[indexers]
|
||||
);
|
||||
|
||||
const filteredIndexers = useMemo(() => {
|
||||
const flat = ({
|
||||
id,
|
||||
subCategories = [],
|
||||
}: {
|
||||
id: number;
|
||||
subCategories: IndexerCategory[];
|
||||
}): number[] => [id, ...subCategories.flatMap(flat)];
|
||||
|
||||
return indexers.filter((indexer) => {
|
||||
if (
|
||||
filter.length &&
|
||||
!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) &&
|
||||
!indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filterProtocols.length &&
|
||||
!filterProtocols.includes(indexer.protocol)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filterLanguages.length &&
|
||||
!filterLanguages.includes(indexer.language)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filterPrivacyLevels.length &&
|
||||
!filterPrivacyLevels.includes(indexer.privacy)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterCategories.length) {
|
||||
const { categories = [] } = indexer.capabilities || {};
|
||||
|
||||
const flatCategories = categories
|
||||
.filter((item) => item.id < 100000)
|
||||
.flatMap(flat);
|
||||
|
||||
if (
|
||||
!filterCategories.every((categoryId) =>
|
||||
flatCategories.includes(categoryId)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [
|
||||
indexers,
|
||||
filter,
|
||||
filterProtocols,
|
||||
filterLanguages,
|
||||
filterPrivacyLevels,
|
||||
filterCategories,
|
||||
]);
|
||||
|
||||
const errorMessage = getErrorMessage(
|
||||
error,
|
||||
translate('UnableToLoadIndexers')
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('AddIndexer')}</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder={translate('FilterPlaceHolder')}
|
||||
name="filter"
|
||||
value={filter}
|
||||
autoFocus={true}
|
||||
onChange={onFilterChange}
|
||||
/>
|
||||
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>
|
||||
{translate('Protocol')}
|
||||
</label>
|
||||
|
||||
<EnhancedSelectInput
|
||||
name="indexerProtocols"
|
||||
value={filterProtocols}
|
||||
values={PROTOCOLS}
|
||||
onChange={onFilterProtocolsChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>
|
||||
{translate('Language')}
|
||||
</label>
|
||||
|
||||
<EnhancedSelectInput
|
||||
name="indexerLanguages"
|
||||
value={filterLanguages}
|
||||
values={languages}
|
||||
onChange={onFilterLanguagesChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Privacy')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerPrivacyLevels"
|
||||
value={filterPrivacyLevels}
|
||||
values={PRIVACY_LEVELS}
|
||||
onChange={onFilterPrivacyLevelsChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>
|
||||
{translate('Categories')}
|
||||
</label>
|
||||
|
||||
<NewznabCategorySelectInputConnector
|
||||
name="indexerCategories"
|
||||
value={filterCategories}
|
||||
onChange={onFilterCategoriesChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert kind={kinds.INFO} className={styles.notice}>
|
||||
<div>{translate('ProwlarrSupportsAnyIndexer')}</div>
|
||||
</Alert>
|
||||
|
||||
<Scroller className={styles.scroller} autoFocus={false}>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{error ? (
|
||||
<Alert kind={kinds.DANGER} className={styles.alert}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!indexers.length ? (
|
||||
<Table
|
||||
columns={COLUMNS}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{filteredIndexers.map((indexer) => (
|
||||
<SelectIndexerRow
|
||||
{...indexer}
|
||||
key={`${indexer.implementation}-${indexer.name}`}
|
||||
implementation={indexer.implementation}
|
||||
implementationName={indexer.implementationName}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!indexers.length && !filteredIndexers.length ? (
|
||||
<Alert kind={kinds.WARNING} className={styles.alert}>
|
||||
{translate('NoIndexersFound')}
|
||||
</Alert>
|
||||
) : null}
|
||||
</Scroller>
|
||||
</ModalBody>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddIndexerModalContent;
|
||||
@@ -1,94 +0,0 @@
|
||||
import { some } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import AddIndexerModalContent from './AddIndexerModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('indexers.schema'),
|
||||
createAllIndexersSelector(),
|
||||
(indexers, allIndexers) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
sortDirection,
|
||||
sortKey
|
||||
} = indexers;
|
||||
|
||||
const indexerList = items.map((item) => {
|
||||
const { definitionName } = item;
|
||||
return {
|
||||
...item,
|
||||
isExistingIndexer: some(allIndexers, { definitionName })
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
indexers: indexerList,
|
||||
sortKey,
|
||||
sortDirection
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchIndexerSchema,
|
||||
selectIndexerSchema,
|
||||
setIndexerSchemaSort
|
||||
};
|
||||
|
||||
class AddIndexerModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchIndexerSchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerSelect = ({ implementation, implementationName, name }) => {
|
||||
this.props.selectIndexerSchema({ implementation, implementationName, name });
|
||||
this.props.onSelectIndexer();
|
||||
};
|
||||
|
||||
onSortPress = (sortKey, sortDirection) => {
|
||||
this.props.setIndexerSchemaSort({ sortKey, sortDirection });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddIndexerModalContent
|
||||
{...this.props}
|
||||
onSortPress={this.onSortPress}
|
||||
onIndexerSelect={this.onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerModalContentConnector.propTypes = {
|
||||
fetchIndexerSchema: PropTypes.func.isRequired,
|
||||
selectIndexerSchema: PropTypes.func.isRequired,
|
||||
setIndexerSchemaSort: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSelectIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);
|
||||
@@ -2,18 +2,19 @@ import React, { useCallback } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
||||
import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import { IndexerCapabilities } from 'Indexer/Indexer';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import { IndexerCapabilities, IndexerPrivacy } from 'Indexer/Indexer';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectIndexerRow.css';
|
||||
|
||||
interface SelectIndexerRowProps {
|
||||
name: string;
|
||||
protocol: string;
|
||||
privacy: string;
|
||||
protocol: DownloadProtocol;
|
||||
privacy: IndexerPrivacy;
|
||||
language: string;
|
||||
description: string;
|
||||
capabilities: IndexerCapabilities;
|
||||
@@ -63,7 +64,9 @@ function SelectIndexerRow(props: SelectIndexerRowProps) {
|
||||
|
||||
<TableRowCell>{description}</TableRowCell>
|
||||
|
||||
<TableRowCell>{translate(firstCharToUpper(privacy))}</TableRowCell>
|
||||
<TableRowCell>
|
||||
<PrivacyLabel privacy={privacy} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<CapabilitiesLabel capabilities={capabilities} />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user