Compare commits

..

192 Commits

Author SHA1 Message Date
Bogdan
74a9fa784a Fixed: (Nebulance) Detect TV UHD releases 2023-08-26 20:21:41 +03:00
ilike2burnthing
1b0c9adf24 Remove Reddit from issues config 2023-08-22 00:30:49 +03:00
Bogdan
0eaa538e8a Allow deselecting values when using search through url query params 2023-08-21 21:32:26 +03:00
Bogdan
39a54eb8f6 Prevent health checks warnings for disabled notifications 2023-08-21 21:04:30 +03:00
Bogdan
5ad6237785 Improve music and console search results for AnimeBytes
Also prevent duplicate categories showing in the indexer info modal
2023-08-21 18:08:43 +03:00
Bogdan
9fee4f914f Bump version to 1.8.4 2023-08-20 12:23:24 +03:00
Weblate
ba2aab6bb3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Robert A. Viana <robert.abreu@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: w2861 <hfagfc@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-08-20 03:52:43 +03:00
Bogdan
5c8ae82f11 Prevent useless builds 2023-08-19 16:23:16 +03:00
Servarr
bcbeac1e83 Automated API Docs update [skip ci] 2023-08-19 13:55:35 +03:00
Mark McDowall
b36d793d85 Allow decimals in number inputs
(cherry picked from commit 7f5ddff568ce9f87bd45420cbd36690b190bd633)
2023-08-19 13:49:18 +03:00
Mark McDowall
b0162ccc5b New: Success check mark on blue buttons is now white instead of green
(cherry picked from commit 566fae9d5857a10bd69c718368e7847e5a733faa)
2023-08-19 13:47:56 +03:00
Robin Dadswell
f0892eb4b8 Adds Pipeline testing for Postgres15 Databases 2023-08-18 21:12:09 +03:00
Robin Dadswell
e456979467 bump Npgsql to 7.0.4 2023-08-18 21:12:09 +03:00
Bogdan
66ca47b615 Fix flaky automation tests 2023-08-18 20:36:44 +03:00
Weblate
2b7771bfe0 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Robert A. Viana <robert.abreu@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: w2861 <hfagfc@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-08-18 11:40:45 +03:00
Mark McDowall
955bc472a1 Fixed: Hidden files being ignored
(cherry picked from commit d493f8762fcb1684b44e182753c21d7a493db787)
2023-08-18 11:33:21 +03:00
Bogdan
e024bba6b6 Replace docker detection for cgroup v2
(cherry picked from commit 78d4dee4610c5f3f90cc69469004008aa64900b8)
2023-08-18 11:32:39 +03:00
Bogdan
aeb3b7d8b5 Add retry to flaky automation tests 2023-08-17 14:38:48 +03:00
bakerboy448
a7b25b8b93 Remove reddit from readme 2023-08-16 17:25:12 +03:00
Qstick
130257fdd4 New: Notifications (Connect) Status
(cherry picked from commit e3545801721e00d4e5cac3fa534e66dcbe9d2d05)
2023-08-14 16:07:23 +03:00
Qstick
b618f23bc0 Cleanup other provider status code
(cherry picked from commit c281a7818adce8db728d2a104f4444cb9c0baf2c)
2023-08-14 16:07:23 +03:00
Bogdan
a758161e31 New: Default name when adding providers 2023-08-14 16:07:23 +03:00
Weblate
27928103c5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translation: Servarr/Prowlarr
2023-08-14 14:06:06 +03:00
Weblate
d5b3961e8a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: David Molero <contact@dolvem.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: deepserket <deepserket@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-08-14 13:48:57 +03:00
Stevie Robinson
307adf053e Translate Updated and Connection Lost Modals in frontend
(cherry picked from commit 074aa6f4457bf83173e6ba7209c452a6e0659a35)
2023-08-14 01:22:09 +03:00
Bogdan
31261f66ad Use named tokens in frontend translate function 2023-08-13 22:59:56 +03:00
Bogdan
5dbb59dfaa Bump version to 1.8.3 2023-08-13 13:04:41 +03:00
Weblate
25c1803d0e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Nir Israel Hen <nirisraelh@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: wilfriedarma <wilfriedarma.collet@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-08-13 12:51:24 +03:00
Bogdan
9f4c9d3344 Show successful grabs in Search with green icon 2023-08-12 12:12:51 +03:00
Bogdan
dfb00d9bb1 Fixed: Ensure grab notifications are sent according to tags requirements 2023-08-12 12:07:17 +03:00
Bogdan
f7727855b5 Rework adding one minute back-off level for all providers
(cherry picked from commit d8f314ff0ef64e8d90b21b7865e46be74db5e570)
2023-08-12 09:32:07 +03:00
Stepan Goremykin
1e4c67dcdb Update FluentAssertions
(cherry picked from commit 951a9ade00d7c9105f03608cb598450d706b826f)
2023-08-12 09:28:42 +03:00
Robin Dadswell
26afcb0071 Fixed: PostgreSQL timezone issues
(cherry picked from commit d55864f86914199aa0c4ee37df1e42e6ad71ef4f)
2023-08-10 23:03:27 +01:00
Bogdan
7a937e85a4 Fixed: Retain user settings not-affiliated with Prowlarr 2023-08-10 18:05:00 +03:00
Bogdan
7cd82321b4 Bump Npgsql version to 6.0.9
Fixes #1819
2023-08-09 23:41:09 +03:00
Bogdan
8c9adba516 Fixed color for links 2023-08-09 19:30:25 +03:00
Bogdan
03fa9254e3 Prevent NullRef in IsPathValid for null paths 2023-08-09 13:35:13 +03:00
Weblate
e66ecf5c95 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: byakurau <byakurau1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-08-08 18:49:41 +03:00
Bogdan
e0dddfa215 Remove Order and Help columns from Apps Fields 2023-08-08 18:30:25 +03:00
Bogdan
bcb8afadf8 New: Add Content Summary for requests to apps 2023-08-08 18:26:09 +03:00
Bogdan
fc4a0979c3 Fixed: Detect Docker when using control group v2 2023-08-07 19:15:21 +03:00
Bogdan
5f643b2ced Fixed: (Indexers) Don't fetch releases when using unsupported capabilities 2023-08-06 20:30:59 +03:00
Bogdan
6f09b0f4f5 Bump version to 1.8.2 2023-08-06 08:42:24 +03:00
Qstick
95c2531107 Filter user issues from Sentry
(cherry picked from commit 03d361f5537bfc0caba1b86085f974570942fdbc)
2023-08-05 21:38:44 +03:00
Bogdan
f83828cc22 Fixed border for actions in health status 2023-08-05 17:56:13 +03:00
Bogdan
cdea548ce2 New: Add internal links for apps and download clients health checks 2023-08-05 14:26:53 +03:00
Bogdan
cae1da0ce2 Fixed: (Apps) Lower the severity for testing messages 2023-08-05 14:12:06 +03:00
Bogdan
765f354c51 New: Add test all action for apps and download clients to status health 2023-08-05 14:11:50 +03:00
Bogdan
5cbbffb018 Fix translation typo in sync level options 2023-08-05 14:11:50 +03:00
Bogdan
b2c5448cbf Fixed: Run health checks for applications and download clients on bulk events 2023-08-05 11:40:15 +03:00
Bogdan
3dae84705c Fixed: Ensure failing providers are marked as failed when testing all 2023-08-05 10:02:55 +03:00
Weblate
2321d278d6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Albert <zuozl1992@foxmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ivan Mazzoli <dreadtank27@gmail.com>
Co-authored-by: Magnus <magnus.fladvad@gmail.com>
Co-authored-by: Stjepan <stjepstjepanovic@gmail.com>
Co-authored-by: Thirrian <matthiaslantermann@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: stormaac <yxc.frank@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-08-04 21:07:31 +03:00
TwentyNine78
ea73466f6a Fixed: Compatibility with the new Download Station API
(cherry picked from commit 49e90463e57929e7b9885f1b7b0eb05bd7cc3ebe)
2023-08-04 21:06:45 +03:00
Bogdan
6961c5a1c6 Fixed: (AlphaRatio) Use FL tokens only if canUseToken is true
Fixes #1811
2023-08-04 17:20:28 +03:00
Mark McDowall
141f1597dc New: Ignore inaccessible files with getting files
(cherry picked from commit e5aa8584100d96a2077c57f74ae5b2ceab63de19)
2023-08-04 13:17:37 +03:00
Bogdan
1100f350ae Fix translations for option values 2023-08-04 07:05:31 +03:00
Bogdan
3c5eefc349 New: Health check for indexers with invalid download client
(cherry picked from commit 377fce6fe15c0875c4bd33f1371a31af79c9310c)
2023-08-04 06:50:48 +03:00
Mark McDowall
0bfb557470 Prevent NullRef in ContainsInvalidPathChars
(cherry picked from commit 5f7217844533907d7fc6287a48efb31987736c4c)
2023-08-04 06:43:40 +03:00
Servarr
c93d6cff63 Automated API Docs update [skip ci] 2023-08-03 17:40:31 +03:00
Bogdan
7e4980b855 New: Add translations for columns 2023-08-03 17:20:36 +03:00
Bogdan
419ef4b3bf New: More translations for columns
(cherry picked from commit aee8579d1823b7dfb94c0055fe33b5fb5a7fbf17)
2023-08-03 16:10:13 +03:00
Mark McDowall
c56d49ab60 Fixed: Translations for columns
(cherry picked from commit 6d53d2a153a98070c42d0619c15902b6bd5dfab4)
2023-08-03 16:07:00 +03:00
Mark McDowall
1a40924db3 Fixed: Improve translation loading
(cherry picked from commit 73c5ec1da4dd00301e1b0dddbcea37590a99b045)
2023-08-03 16:05:39 +03:00
Mark McDowall
d55906d49a UI loading improvements
Fixed: Caching for dynamically loaded JS files
Fixed: Incorrect caching of initialize.js
(cherry picked from commit f0cb5b81f140c67fa84162e094cc4e0f3476f5da)
2023-08-03 15:57:52 +03:00
Bogdan
bc53fab966 Fixed: Don't fetch capabilities for disabled Newznab/Torznab indexers on create
Also prevent NullRef in GetProxy since definition is null when using FetchCapabilities on add
2023-08-02 14:54:01 +03:00
Bogdan
d897b50f80 New: (UI) Show Magnet Link in search results if any 2023-08-01 13:29:12 +03:00
Bogdan
cc66cee71c Fixed: (Apps) Avoid force saving remote indexers when it's not necessary 2023-07-31 10:33:57 +03:00
Bogdan
f5e96f3f51 Ensure yarn packages are installed when running only LintUI 2023-07-31 08:18:45 +03:00
Mark McDowall
d52e1259a1 Re-order frontend build steps
(cherry picked from commit 97ad6682f7d54af8886144bc5a179fa7242f1f1f)
2023-07-31 07:56:20 +03:00
Bogdan
72e6d66269 New: (Apps) Add force sync indexers for applications 2023-07-31 07:22:08 +03:00
Bogdan
e51b85449d Convert store selectors to Typescript 2023-07-30 21:06:44 +03:00
Bogdan
efd5e92ca5 Support categories with Transmission 2023-07-30 12:38:33 +03:00
Bogdan
d153746a98 Bump version to 1.8.1 2023-07-30 10:45:08 +03:00
Bogdan
a1927e1e0f Sort indexers by name in search footer dropdown 2023-07-29 13:48:10 +03:00
Bogdan
630a4ce800 Fixed: Ensure failing indexers are marked as failed when testing all
(cherry picked from commit b407eba61284d5fb855df6a2868805853aa6f448)
2023-07-29 12:14:58 +03:00
Bogdan
8b1dd78300 Fixed: (Apps) Ensure populated capabilities for Torznab/Newznab definitions 2023-07-29 12:08:48 +03:00
Bogdan
cab50b35aa Convert some selectors to Typescript 2023-07-29 03:14:47 +03:00
Weblate
eee1be784b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-07-28 12:55:09 +03:00
Bogdan
269dc5688b New: (IPTorrents) Add new base url 2023-07-27 12:18:49 +03:00
Weblate
9bed795c89 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-07-27 12:14:26 +03:00
Bogdan
3b5f151252 New: Set default names for providers in Add Modals 2023-07-27 02:58:07 +03:00
Weblate
b3a541c9ff Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dortlix <jeremy.boely@hotmail.fr>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: aguillement <adrien.guillement@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-07-26 07:18:25 +03:00
Bogdan
bc90fa2d3f Add unit to history cleanup days option 2023-07-26 07:17:06 +03:00
Bogdan
4b0a896434 Fixed: (Cardigann) Improvements to automatic logins with captcha 2023-07-26 05:35:42 +03:00
Bogdan
6be0e08635 Convert Delete Indexer to Typescript 2023-07-25 05:50:45 +03:00
Bogdan
f618901048 Convert Indexer Stats to Typescript 2023-07-25 05:50:45 +03:00
Weblate
809ed940e6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: SHUAI.W <x@ousui.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-07-25 01:05:11 +03:00
Qstick
7b14c2ee66 Bump version to 1.8.0 2023-07-23 23:25:30 -05:00
Qstick
4528d03931 Update Sentry DSN 2023-07-23 21:49:21 -05:00
Bogdan
e0b30d34b1 New: (Apps) Add Go To Application in UI 2023-07-23 22:03:49 +03:00
Bogdan
8edf483e69 Bump version to 1.7.4 2023-07-23 07:09:59 +03:00
Bogdan
cea6aae9e1 Add support for deprecated values in field select options
(cherry picked from commit d9786887f3fe30ef60ad9c50b3272bf60dfef309)
2023-07-23 05:05:41 +03:00
Bogdan
1697cee680 Add hover background color in Indexer Table Index 2023-07-22 05:34:39 +03:00
Bogdan
ce8c90a125 Added magnetUrl prop in Search Index Row 2023-07-22 05:34:39 +03:00
Weblate
c8ad3d6edd Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-07-21 13:25:57 +03:00
Bogdan
ebe01913c2 Fix selecting guids from search results 2023-07-21 10:35:04 +03:00
Mark McDowall
07cb19f9f3 Sort available filters options in custom filters
(cherry picked from commit 9e694c7b06c6d54fd652792fa1d81cc27ec1f311)
2023-07-21 07:40:36 +03:00
Bogdan
7f51c44829 Fixed: (UI) Ensure proper parsing for size values in custom filters 2023-07-21 07:26:43 +03:00
Bogdan
07f816f9fd Fixed: (BeyondHD) Add search types option 2023-07-21 01:24:29 +03:00
Bogdan
a4a50b880c Add GetAttribute for enums 2023-07-21 01:24:29 +03:00
Bogdan
79361d92cb Ensure No search results found isn't shown without a search 2023-07-20 23:05:48 +03:00
Bogdan
ecda75152e Cache busting for CSS files 2023-07-20 19:57:45 +03:00
Bogdan
37a4e7c228 Rename decisions to releases in Search Controller 2023-07-20 19:25:04 +03:00
Bogdan
1a66d23bfe Fixed: (UI) Improved mobile search form and show indexer flags 2023-07-20 19:24:00 +03:00
Bogdan
a26aa4bd1e New: (UI) Show indexer id as hint in IndexerSelect 2023-07-20 18:46:12 +03:00
Bogdan
a5d83459e9 New: (BeyondHD) Add pagination support 2023-07-20 15:54:05 +03:00
Mark McDowall
4bfaab4b21 Typings cleanup and improvements
(cherry picked from commit b2c43fb2a67965d68d3d35b72302b0cddb5aca23)
2023-07-20 14:40:21 +03:00
Bogdan
5764950b10 Show implementation name in Application Modal's header 2023-07-20 14:40:21 +03:00
Mark McDowall
470b57316a Add type number to value prop in HintedSelectInputSelectedValue
(cherry picked from commit fea66cb7bccc7e94523614db38b157fa38f55ea5)
2023-07-20 14:40:21 +03:00
Bogdan
f546b9a3b0 Fixed: (SubsPlease) Update indexer urls 2023-07-20 02:45:07 +03:00
Bogdan
cc28c90e39 Combine cleanse rules for passkey and rsskey 2023-07-20 02:20:55 +03:00
Mark McDowall
6e21e892bc Fix chunk IDs and source map file names
(cherry picked from commit bb8fed94eb2c44040031643e8c20ff72de759535)
2023-07-20 01:03:16 +03:00
Weblate
62d868f0e9 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-07-19 15:08:48 +03:00
Bogdan
27b36fe501 Tidy up input fields on mobile in Add Indexer Modal 2023-07-19 05:35:26 +03:00
Mark McDowall
fc80efd15f Fixed: List jump bar click issues
(cherry picked from commit 9c7378625112088d022239fdbdb90c0dc941d61d)
2023-07-18 23:37:26 +03:00
Bogdan
9b75ba6ca0 New: (BeyondHD) Add internal indexer flag 2023-07-18 04:47:05 +03:00
Bogdan
d42649c4df New: (BeyondHD) Add limited, refund and rewind search options 2023-07-18 04:47:05 +03:00
Bogdan
53adfb750c New: (Shazbat) Add scene indexer flag to all releases 2023-07-18 04:19:41 +03:00
Bogdan
ac487f9b40 Fixed: (BeyondHD) Add search by freeleech only 2023-07-17 20:25:20 +03:00
Bogdan
6dd354bf1a Fixed: (BeyondHD) Searching ImdbId has priority over TmdbId. 2023-07-17 19:40:14 +03:00
Weblate
b747d0a321 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Godwhitelight <godwhitelight1@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Qstick <qstick@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2023-07-17 06:57:53 +03:00
Weblate
0e6cec6f54 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Godwhitelight <godwhitelight1@gmail.com>
Co-authored-by: Guy Porat <guyporatmail@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translation: Servarr/Prowlarr
2023-07-16 22:54:31 +03:00
Weblate
65cf7c1009 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translation: Servarr/Prowlarr
2023-07-16 19:42:54 +03:00
Weblate
5f9c3585f4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translation: Servarr/Prowlarr
2023-07-16 19:42:19 +03:00
Weblate
a9d1d4be90 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translation: Servarr/Prowlarr
2023-07-16 19:41:26 +03:00
Weblate
a94ed11b21 Translations update from Servarr Weblate
Co-authored-by: MoowGlax <matthieu.derouet.pro@gmail.com>
Co-authored-by: Thodoris Kalatzis <teo.kal@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: emacsdias <emacs.dias@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: splifter <a.strahlke@gmail.com>
Co-authored-by: victor22265 <843427709@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-07-16 11:32:06 -05:00
Bogdan
3fab8fb0db Fixed: (Search) Ensure TvMazeId is parsed correctly on a repeat search 2023-07-16 17:04:08 +03:00
Mark McDowall
5e52627799 Fixed: Ensure translations are fetched before loading app
(cherry picked from commit ad2721dc55f3233e4c299babe5744418bc530418)
2023-07-16 08:29:30 +03:00
Bogdan
b9a28f243e Bump version to 1.7.3 2023-07-16 08:14:24 +03:00
Bogdan
146e7ca7b6 Use HelpTexts for sync levels in applications 2023-07-16 07:03:58 +03:00
Bogdan
1488fb7570 Revert "Fixed: Ensure translations are fetched before loading app"
This reverts commit 0fc52ae16f.
2023-07-16 04:07:54 +03:00
Mark McDowall
0fc52ae16f Fixed: Ensure translations are fetched before loading app
(cherry picked from commit ad2721dc55f3233e4c299babe5744418bc530418)
2023-07-16 02:21:39 +03:00
Mark McDowall
5218bea705 Use named keys for apply tags help text
(cherry picked from commit c1f8c7b17ba5775a0f6f76cebc3173e03124d000)
2023-07-16 02:11:34 +03:00
Bogdan
ac33330c7c Fix selection input in QueryParameterModal 2023-07-15 18:20:59 +03:00
Bogdan
041a7c571f Translate url type for indexer description 2023-07-15 18:00:52 +03:00
Bogdan
5d73c6aa91 Update webpack, eslint and core-js 2023-07-15 17:34:54 +03:00
Bogdan
ef9a3a4f2a Fixed: (AvistaZ) Allow search by episode 2023-07-15 17:34:28 +03:00
Bogdan
3ce3f8acdd Fixed: (Apps) Lower the severity for common messages 2023-07-13 06:21:05 +03:00
Bogdan
9bac2992b5 Fixed: (UI) Show available indexers count in Add Indexer 2023-07-13 00:37:57 +03:00
Bogdan
4a88b70f40 Show the correct total of releases when selecting 2023-07-13 00:30:12 +03:00
Bogdan
c9b1d0d958 Fixed: (API) Prevent search failed exception when using non-interactive search 2023-07-12 04:40:17 +03:00
Bogdan
a5b5e7a3a5 Fixed: (UI) Prevent passing NaN values to search API when using invalid ids 2023-07-12 04:34:30 +03:00
Bogdan
376202e2af Fixed: (BTN) Prevent NullRef when Result.Torrents is null 2023-07-11 04:53:43 +03:00
Servarr
6b698b33be Automated API Docs update [skip ci] 2023-07-10 19:23:30 +03:00
Bogdan
1706728230 New: Bulk Manage Applications, Download Clients
Co-authored-by: Qstick <qstick@gmail.com>
2023-07-10 19:17:46 +03:00
Bogdan
cb520b2264 Bump version to 1.7.2 2023-07-09 14:59:01 +03:00
ricci2511
193335e2a8 New: Add support for search through url query params 2023-07-09 01:19:05 +03:00
Servarr
1c98727cf3 Automated API Docs update [skip ci] 2023-07-08 19:19:02 +03:00
Bogdan
ab5b321385 New: (UI) Add priority to Indexer Editor 2023-07-08 19:12:54 +03:00
Bogdan
96340909f1 Add translations to SearchFooter 2023-07-08 18:16:02 +03:00
Bogdan
bd6a37dc8c Fixed: (UI) Regain jump to character functionality for search releases 2023-07-08 17:02:01 +03:00
Bogdan
a663cebada Check indexer health checks on bulk updates 2023-07-08 03:52:21 +03:00
Bogdan
2ce5618499 Improve indexer multiple select functionality 2023-07-08 03:13:41 +03:00
Bogdan
94c91d4c3f Fix recursive call in translate() 2023-07-08 03:10:51 +03:00
Bogdan
79fbb2d0d7 New: (UI) Show advanced settings toggle in application modal content 2023-07-07 17:51:12 +03:00
Bogdan
e2e52746bb Fix repeat search when limits are empty 2023-07-07 17:26:56 +03:00
Bogdan
21cc96d683 Fixed: (History) Save limit and offset in history data 2023-07-07 16:21:20 +03:00
Bogdan
e68b45636e Minor refactoring in TorrentsCSV 2023-07-07 13:25:53 +03:00
Servarr
ce68fe4105 Automated API Docs update [skip ci] 2023-07-06 01:29:07 +03:00
Bogdan
712404ddca Show download client field only when download clients are set 2023-07-06 01:07:32 +03:00
ricci2511
826828e8ec New: Add download client per indexer setting 2023-07-06 01:07:32 +03:00
Bogdan
252740519f Remove unused prop in Stats 2023-07-06 00:39:33 +03:00
Bogdan
062fd77e1b Fixed: (UI) Prevent search results clearing when using header search with enter key 2023-07-06 00:17:16 +03:00
Bogdan
6769055b6b Fixed: (TorrentPotato) Allow use of custom APIs 2023-07-06 00:07:50 +03:00
Taloth Saldono
90e92c0b66 Ensure mousetrap instance exists in unbindShortcut
(cherry picked from commit 930742ae2c69a530afe60f76a5824f2722540df8)
2023-07-05 23:02:22 +03:00
Bogdan
7eac11f57a Fixed: (UI) Change default search results sorting to age 2023-07-05 16:52:39 +03:00
Bogdan
02a3c1b224 Align ProwlarrErrorPipeline with upstream 2023-07-04 23:51:10 +03:00
Bogdan
57efa6d0b1 Add Find() to BasicRepository 2023-07-04 22:38:52 +03:00
Qstick
cee52147bc Add package to Sentry release to ensure apps don't mix 2023-07-04 12:21:00 -05:00
Bogdan
a1abcd6c93 Fixed: (History) Reduce History Cleanup Days to 30 2023-07-04 06:56:13 +03:00
Bogdan
18e2757d37 Allow templating in JSON rows selector in Cardigann 2023-07-03 22:45:04 +03:00
Bogdan
8790a6f06a New: (HttpClient) Add HTTP/2 support 2023-07-03 18:55:13 +03:00
Bogdan
4fafdb2cd2 Add x265 categories for Movies and TV in Newznab 2023-07-03 18:54:34 +03:00
Bogdan
bfc06fc8bc Bump version to 1.7.1 2023-07-02 12:01:07 +03:00
Bogdan
9f4f6a5726 Add missing translation for query type 2023-06-29 17:42:59 +03:00
Bogdan
d9ace9a862 Fixed: (Stats) Exclude cached queries from average elapsed time 2023-06-29 16:55:46 +03:00
Bogdan
95691c7476 New: Show query type in history 2023-06-29 16:25:49 +03:00
Bogdan
90f2020e59 Fixed: Misaligned table border in history 2023-06-29 16:08:23 +03:00
Bogdan
6afa1dc8ba Fixed: (Cardigann) Don't check for captcha when captcha answer is empty 2023-06-29 14:43:11 +03:00
Bogdan
e8139f2a5b Fixed: (PornoLab) Moved to YML for Cardigann 2023-06-28 17:56:01 +03:00
Bogdan
45328db2c7 Add close reason to label actions 2023-06-28 15:25:36 +03:00
Bogdan
e55d6b827a Add ContentSummary to HDBits requests 2023-06-27 13:19:57 +03:00
Bogdan
34cd68fa07 Add ContentSummary to BeyondHD requests 2023-06-27 13:19:57 +03:00
Bogdan
aed3f9f887 Create overload for ToJson for Formatting 2023-06-27 13:19:57 +03:00
Bogdan
6880e67507 Fixed: (Apps) Ensure validation for test connection 2023-06-27 06:52:59 +03:00
Bogdan
e0e1b1494e Exclude RSS history events in migration 2023-06-27 05:20:44 +03:00
Bogdan
20df31919d Check for event type to prevent multiple runs on the same row 2023-06-26 20:49:15 +03:00
Bogdan
8785fe02e8 Execute update queries only for certain rows in migration 34 2023-06-26 18:29:14 +03:00
Bogdan
b2b877a8c3 Fix: (UI) Maintain search type and parameters on repeat search 2023-06-26 15:08:31 +03:00
Bogdan
0de302ad48 Don't save empty data in history service 2023-06-26 15:08:31 +03:00
Bogdan
06391489cf Fixed: (Apps) Use forceSave=true to avoid validation warnings 2023-06-26 10:58:14 +03:00
Qstick
8fcceb0702 Bump version to 1.7.0 2023-06-25 20:35:32 -05:00
502 changed files with 11597 additions and 5546 deletions

View File

@@ -6,6 +6,3 @@ contact_links:
- name: Support via Discord
url: https://prowlarr.com/discord
about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/prowlarr
about: Discuss and search thru support topics.

View File

@@ -7,6 +7,7 @@
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord)
or [Subreddit](https://reddit.com/r/prowlarr)
close: true
close-reason: 'not planned'
'Type: Indexer Request':
comment: >
@@ -14,6 +15,7 @@
for bug reports and feature requests. However, this issue appears
to be a indexer request. Please use our Indexer request [site](https://requests.prowlarr.com/)
close: true
close-reason: 'not planned'
'Status: Logs Needed':
comment: >

View File

@@ -29,7 +29,6 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr)
Note: GitHub Issues are for Bugs and Feature Requests Only

View File

@@ -9,15 +9,15 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.6.3'
majorVersion: '1.8.4'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.408'
nodeVersion: '16.X'
innoVersion: '6.2.0'
nodeVersion: '16.x'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11'
@@ -27,6 +27,10 @@ trigger:
include:
- develop
- master
paths:
exclude:
- .github
- src/Prowlarr.Api.*/openapi.json
pr:
branches:
@@ -34,8 +38,9 @@ pr:
- develop
paths:
exclude:
- .github
- src/NzbDrone.Core/Localization/Core
- src/Prowlarr.API.*/openapi.json
- src/Prowlarr.Api.*/openapi.json
stages:
- stage: Setup
@@ -349,7 +354,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create FreeBSD Core Core tar
displayName: Create freebsd-x64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).freebsd-core-x64.tar.gz'
archiveType: 'tar'
@@ -362,7 +367,7 @@ stages:
- bash: |
echo "Uploading source maps to sentry"
curl -sL https://sentry.io/get-cli/ | bash
RELEASENAME="${PROWLARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
RELEASENAME="Prowlarr@${PROWLARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
sentry-cli releases new --finalize -p prowlarr -p prowlarr-ui -p prowlarr-update "${RELEASENAME}"
sentry-cli releases -p prowlarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
sentry-cli releases set-commits --auto "${RELEASENAME}"
@@ -528,8 +533,8 @@ stages:
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
- job: Unit_LinuxCore_Postgres14
displayName: Unit Native LinuxCore with Postgres14 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
@@ -565,6 +570,7 @@ stages:
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
@@ -577,7 +583,60 @@ stages:
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres15
displayName: Unit Native LinuxCore with Postgres15 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests
Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Prowlarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:15
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
failTaskOnFailedTests: true
- stage: Integration
@@ -663,8 +722,8 @@ stages:
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
- job: Integration_LinuxCore_Postgres14
displayName: Integration Native LinuxCore with Postgres14 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
@@ -710,6 +769,7 @@ stages:
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
@@ -720,7 +780,70 @@ stages:
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres15
displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr'
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Prowlarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:15
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
@@ -1003,7 +1126,7 @@ stages:
git add .
if git status | grep -q modified
then
git commit -am 'Automated API Docs update [skip ci]'
git commit -am 'Automated API Docs update'
git push -f --set-upstream origin api-docs
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/prowlarr/prowlarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
else

View File

@@ -392,22 +392,21 @@ then
fi
fi
if [ "$FRONTEND" = "YES" ];
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
then
YarnInstall
RunWebpack
fi
if [ "$LINT" = "YES" ];
then
if [ -z "$FRONTEND" ];
then
YarnInstall
fi
LintUI
fi
if [ "$FRONTEND" = "YES" ];
then
RunWebpack
fi
if [ "$PACKAGES" = "YES" ];
then
UpdateVersionNumber

View File

@@ -26,7 +26,8 @@ module.exports = {
globals: {
expect: false,
chai: false,
sinon: false
sinon: false,
JSX: true
},
parserOptions: {

View File

@@ -35,7 +35,7 @@ module.exports = (env) => {
},
entry: {
index: 'index.js'
index: 'index.ts'
},
resolve: {
@@ -65,23 +65,23 @@ module.exports = (env) => {
output: {
path: distFolder,
publicPath: '/',
filename: '[name].js',
filename: '[name]-[contenthash].js',
sourceMapFilename: '[file].map'
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'named',
splitChunks: {
chunks: 'initial',
name: 'vendors'
}
chunkIds: isProduction ? 'deterministic' : 'named'
},
performance: {
hints: false
},
experiments: {
topLevelAwait: true
},
plugins: [
new webpack.DefinePlugin({
__DEV__: !isProduction,
@@ -89,13 +89,15 @@ module.exports = (env) => {
}),
new MiniCssExtractPlugin({
filename: 'Content/styles.css'
filename: 'Content/styles.css',
chunkFilename: 'Content/[id]-[chunkhash].css'
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs',
filename: 'index.html',
publicPath: '/'
publicPath: '/',
inject: false
}),
new FileManagerPlugin({

View File

@@ -5,9 +5,9 @@ import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import HistoryConnector from 'History/HistoryConnector';
import IndexerIndex from 'Indexer/Index/IndexerIndex';
import StatsConnector from 'Indexer/Stats/StatsConnector';
import IndexerStats from 'Indexer/Stats/IndexerStats';
import SearchIndexConnector from 'Search/SearchIndexConnector';
import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSettingsConnector';
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
@@ -60,7 +60,7 @@ function AppRoutes(props) {
<Route
path="/indexers/stats"
component={StatsConnector}
component={IndexerStats}
/>
{/*
@@ -98,7 +98,7 @@ function AppRoutes(props) {
<Route
path="/settings/applications"
component={ApplicationSettingsConnector}
component={ApplicationSettings}
/>
<Route

View File

@@ -1,6 +1,7 @@
.version {
margin: 0 3px;
font-weight: bold;
font-family: var(--defaultFontFamily);
}
.maintenance {

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
@@ -64,12 +65,12 @@ function AppUpdatedModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Prowlarr Updated
{translate('AppUpdated', { appName: 'Prowlarr' })}
</ModalHeader>
<ModalBody>
<div>
Version <span className={styles.version}>{version}</span> of Prowlarr has been installed, in order to get the latest changes you'll need to reload Prowlarr.
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Prowlarr', version })} blockClassName={styles.version} />
</div>
{
@@ -77,16 +78,14 @@ function AppUpdatedModalContent(props) {
<div>
{
!update.changes &&
<div className={styles.maintenance}>
{translate('MaintenanceRelease')}
</div>
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
}
{
!!update.changes &&
<div>
<div className={styles.changes}>
What's new?
{translate('WhatsNew')}
</div>
<UpdateChanges
@@ -113,14 +112,14 @@ function AppUpdatedModalContent(props) {
<Button
onPress={onSeeChangesPress}
>
Recent Changes
{translate('RecentChanges')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={onModalClose}
>
Reload
{translate('Reload')}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody>
<div>
{translate('ConnectionLostMessage')}
{translate('ConnectionLostToBackend', { appName: 'Prowlarr' })}
</div>
<div className={styles.automatic}>
{translate('ConnectionLostAutomaticMessage')}
{translate('ConnectionLostReconnect', { appName: 'Prowlarr' })}
</div>
</ModalBody>
<ModalFooter>

View File

@@ -0,0 +1,48 @@
import SortDirection from 'Helpers/Props/SortDirection';
export interface Error {
responseJSON: {
message: string;
};
}
export interface AppSectionDeleteState {
isDeleting: boolean;
deleteError: Error;
}
export interface AppSectionSaveState {
isSaving: boolean;
saveError: Error;
}
export interface PagedAppSectionState {
pageSize: number;
}
export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: {
items: T[];
};
}
export interface AppSectionItemState<T> {
isFetching: boolean;
isPopulated: boolean;
error: Error;
item: T;
}
interface AppSectionState<T> {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: T[];
sortKey: string;
sortDirection: SortDirection;
}
export default AppSectionState;

View File

@@ -0,0 +1,54 @@
import CommandAppState from './CommandAppState';
import IndexerAppState, {
IndexerIndexAppState,
IndexerStatusAppState,
} from './IndexerAppState';
import IndexerStatsAppState from './IndexerStatsAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState';
interface FilterBuilderPropOption {
id: string;
name: string;
}
export interface FilterBuilderProp<T> {
name: string;
label: string;
type: string;
valueType?: string;
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
}
export interface PropertyFilter {
key: string;
value: boolean | string | number | string[] | number[];
type: string;
}
export interface Filter {
key: string;
label: string;
filers: PropertyFilter[];
}
export interface CustomFilter {
id: number;
type: string;
label: string;
filers: PropertyFilter[];
}
interface AppState {
commands: CommandAppState;
indexerIndex: IndexerIndexAppState;
indexerStats: IndexerStatsAppState;
indexerStatus: IndexerStatusAppState;
indexers: IndexerAppState;
settings: SettingsAppState;
system: SystemAppState;
tags: TagsAppState;
}
export default AppState;

View File

@@ -0,0 +1,8 @@
import { CustomFilter } from './AppState';
interface ClientSideCollectionAppState {
totalItems: number;
customFilters: CustomFilter[];
}
export default ClientSideCollectionAppState;

View File

@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;

View File

@@ -0,0 +1,37 @@
import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection';
import Indexer, { IndexerStatus } from 'Indexer/Indexer';
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from './AppSectionState';
import { Filter, FilterBuilderProp } from './AppState';
export interface IndexerIndexAppState {
isTestingAll: boolean;
sortKey: string;
sortDirection: SortDirection;
secondarySortKey: string;
secondarySortDirection: SortDirection;
view: string;
tableOptions: {
showSearchAction: boolean;
};
selectedFilterKey: string;
filterBuilderProps: FilterBuilderProp<Indexer>[];
filters: Filter[];
columns: Column[];
}
interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {
itemMap: Record<number, number>;
}
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
export default IndexerAppState;

View File

@@ -0,0 +1,11 @@
import { AppSectionItemState } from 'App/State/AppSectionState';
import { Filter } from 'App/State/AppState';
import { IndexerStats } from 'typings/IndexerStats';
export interface IndexerStatsAppState
extends AppSectionItemState<IndexerStats> {
selectedFilterKey: string;
filters: Filter[];
}
export default IndexerStatsAppState;

View File

@@ -0,0 +1,10 @@
import AppSectionState, {
AppSectionDeleteState,
} from 'App/State/AppSectionState';
import Release from 'typings/Release';
interface ReleaseAppState
extends AppSectionState<Release>,
AppSectionDeleteState {}
export default ReleaseAppState;

View File

@@ -0,0 +1,42 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionItemState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import Application from 'typings/Application';
import DownloadClient from 'typings/DownloadClient';
import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings';
export interface AppProfileAppState
extends AppSectionState<Application>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface ApplicationAppState
extends AppSectionState<Application>,
AppSectionDeleteState,
AppSectionSaveState {
isTestingAll: boolean;
}
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface NotificationAppState
extends AppSectionState<Notification>,
AppSectionDeleteState {}
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
appProfiles: AppProfileAppState;
applications: ApplicationAppState;
downloadClients: DownloadClientAppState;
notifications: NotificationAppState;
ui: UiSettingsAppState;
}
export default SettingsAppState;

View File

@@ -0,0 +1,10 @@
import SystemStatus from 'typings/SystemStatus';
import { AppSectionItemState } from './AppSectionState';
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
interface SystemAppState {
status: SystemStatusAppState;
}
export default SystemAppState;

View File

@@ -0,0 +1,28 @@
import ModelBase from 'App/ModelBase';
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
export interface Tag extends ModelBase {
label: string;
}
export interface TagDetail extends ModelBase {
label: string;
applicationIds: number[];
indexerIds: number[];
indexerProxyIds: number[];
notificationIds: number[];
}
export interface TagDetailAppState
extends AppSectionState<TagDetail>,
AppSectionDeleteState,
AppSectionSaveState {}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
details: TagDetailAppState;
}
export default TagsAppState;

View File

@@ -0,0 +1,37 @@
import ModelBase from 'App/ModelBase';
export interface CommandBody {
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
completionMessage: string;
requiresDiskAccess: boolean;
isExclusive: boolean;
isLongRunning: boolean;
name: string;
lastExecutionTime: string;
lastStartTime: string;
trigger: string;
suppressMessages: boolean;
seriesId?: number;
}
interface Command extends ModelBase {
name: string;
commandName: string;
message: string;
body: CommandBody;
priority: string;
status: string;
result: string;
queued: string;
started: string;
ended: string;
duration: string;
trigger: string;
stateChangeTime: string;
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
lastExecutionTime: string;
}
export default Command;

View File

@@ -23,7 +23,9 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
info,
} = props;
const [detailedError, setDetailedError] = useState(null);
const [detailedError, setDetailedError] = useState<
StackTrace.StackFrame[] | null
>(null);
useEffect(() => {
if (error) {

View File

@@ -20,12 +20,12 @@ import styles from './FileBrowserModalContent.css';
const columns = [
{
name: 'type',
label: translate('Type'),
label: () => translate('Type'),
isVisible: true
},
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isVisible: true
}
];

View File

@@ -198,11 +198,13 @@ class FilterBuilderRow extends Component {
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
const { name, label } = availablePropFilter;
return {
key: availablePropFilter.name,
value: availablePropFilter.label
key: name,
value: typeof label === 'function' ? label() : label
};
});
}).sort((a, b) => a.value.localeCompare(b.value));
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);

View File

@@ -9,13 +9,13 @@ import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() {
return createSelector(
(state) => state.indexers,
(qualityProfiles) => {
(indexers) => {
const {
isFetching,
isPopulated,
error,
items
} = qualityProfiles;
} = indexers;
const tagList = items.map((item) => {
return {

View File

@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput';
function createMapStateToProps() {
@@ -23,7 +24,7 @@ function createMapStateToProps() {
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
value: translate('NoChange'),
disabled: true
});
}

View File

@@ -0,0 +1,98 @@
import PropTypes from 'prop-types';
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 EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.downloadClients,
(state, { includeAny }) => includeAny,
(state, { protocol }) => protocol,
(downloadClients, includeAny, protocolFilter) => {
const {
isFetching,
isPopulated,
error,
items
} = downloadClients;
const values = items
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
.sort(sortByName)
.map((downloadClient) => ({
key: downloadClient.id,
value: downloadClient.name
}));
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
});
}
return {
isFetching,
isPopulated,
error,
values
};
}
);
}
const mapDispatchToProps = {
dispatchFetchDownloadClients: fetchDownloadClients
};
class DownloadClientSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchDownloadClients();
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
};
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
DownloadClientSelectInputConnector.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeAny: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchDownloadClients: PropTypes.func.isRequired
};
DownloadClientSelectInputConnector.defaultProps = {
includeAny: false,
protocol: 'torrent'
};
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector);

View File

@@ -10,6 +10,7 @@ import CaptchaInputConnector from './CaptchaInputConnector';
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
@@ -72,6 +73,9 @@ function getComponent(type) {
case inputTypes.CATEGORY_SELECT:
return NewznabCategorySelectInputConnector;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector;
@@ -258,12 +262,15 @@ FormInputGroup.propTypes = {
values: PropTypes.arrayOf(PropTypes.any),
type: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all),
min: PropTypes.number,
max: PropTypes.number,
unit: PropTypes.string,
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
helpText: PropTypes.string,
helpTexts: PropTypes.arrayOf(PropTypes.string),
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string,
autoFocus: PropTypes.bool,
includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object,

View File

@@ -33,11 +33,11 @@ function HintedSelectInputOption(props) {
isMobile && styles.isMobile
)}
>
<div>{value}</div>
<div>{typeof value === 'function' ? value() : value}</div>
{
hint != null &&
<div className={styles.hintText}>
<div className={styles.hintText} title={hint}>
{hint}
</div>
}
@@ -48,7 +48,7 @@ function HintedSelectInputOption(props) {
HintedSelectInputOption.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
hint: PropTypes.node,
depth: PropTypes.number,
isSelected: PropTypes.bool.isRequired,

View File

@@ -24,7 +24,7 @@ function HintedSelectInputSelectedValue(props) {
>
<div className={styles.valueText}>
{
isMultiSelect &&
isMultiSelect ?
value.map((key, index) => {
const v = valuesMap[key];
return (
@@ -32,26 +32,28 @@ function HintedSelectInputSelectedValue(props) {
{v ? v.value : key}
</Label>
);
})
}) :
null
}
{
!isMultiSelect && value
isMultiSelect ? null : value
}
</div>
{
hint != null && includeHint &&
hint != null && includeHint ?
<div className={styles.hintText}>
{hint}
</div>
</div> :
null
}
</EnhancedSelectInputSelectedValue>
);
}
HintedSelectInputSelectedValue.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
hint: PropTypes.string,
isMultiSelect: PropTypes.bool.isRequired,

View File

@@ -1,18 +1,20 @@
import _ from 'lodash';
import { groupBy, map } from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import titleCase from 'Utilities/String/titleCase';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state) => state.indexers,
createSortedSectionSelector('indexers', sortByName),
(value, indexers) => {
const values = [];
const groupedIndexers = _.map(_.groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
groupedIndexers.forEach((element) => {
values.push({
@@ -25,6 +27,7 @@ function createMapStateToProps() {
values.push({
key: indexer.id,
value: indexer.name,
hint: `(${indexer.id})`,
isDisabled: !indexer.enable,
parentKey: element.protocol === 'usenet' ? -1 : -2
});
@@ -50,7 +53,6 @@ class IndexersSelectInputConnector extends Component {
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}

View File

@@ -10,7 +10,7 @@ function parseValue(props, value) {
} = props;
if (value == null || value === '') {
return min;
return null;
}
let newValue = isFloat ? parseFloat(value) : parseInt(value);
@@ -41,7 +41,7 @@ class NumberInput extends Component {
componentDidUpdate(prevProps, prevState) {
const { value } = this.props;
if (value !== prevProps.value && !this.state.isFocused) {
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
this.setState({
value: value == null ? '' : value.toString()
});

View File

@@ -68,6 +68,7 @@ class PathInputConnector extends Component {
}
PathInputConnector.propTypes = {
...PathInput.props,
includeFiles: PropTypes.bool.isRequired,
dispatchFetchPaths: PropTypes.func.isRequired,
dispatchClearPaths: PropTypes.func.isRequired

View File

@@ -61,7 +61,7 @@ class SelectInput extends Component {
value={key}
{...otherOptionProps}
>
{optionValue}
{typeof optionValue === 'function' ? optionValue() : optionValue}
</option>
);
})
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
className: PropTypes.string,
disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool,
hasError: PropTypes.bool,

View File

@@ -41,7 +41,7 @@ class Icon extends PureComponent {
return (
<span
className={containerClassName}
title={title}
title={typeof title === 'function' ? title() : title}
>
{icon}
</span>
@@ -58,7 +58,7 @@ Icon.propTypes = {
name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};

View File

@@ -1,98 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styles from './Link.css';
class Link extends Component {
//
// Listeners
onClick = (event) => {
const {
isDisabled,
onPress
} = this.props;
if (!isDisabled && onPress) {
onPress(event);
}
};
//
// Render
render() {
const {
className,
component,
to,
target,
isDisabled,
noRouter,
onPress,
...otherProps
} = this.props;
const linkProps = { target };
let el = component;
if (to) {
if ((/\w+?:\/\//).test(to)) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_blank';
linkProps.rel = 'noreferrer';
} else if (noRouter) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_self';
} else {
el = RouterLink;
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
linkProps.target = target;
}
}
if (el === 'button' || el === 'input') {
linkProps.type = otherProps.type || 'button';
linkProps.disabled = isDisabled;
}
linkProps.className = classNames(
className,
styles.link,
to && styles.to,
isDisabled && 'isDisabled'
);
const props = {
...otherProps,
...linkProps
};
props.onClick = this.onClick;
return (
React.createElement(el, props)
);
}
}
Link.propTypes = {
className: PropTypes.string,
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
to: PropTypes.string,
target: PropTypes.string,
isDisabled: PropTypes.bool,
noRouter: PropTypes.bool,
onPress: PropTypes.func
};
Link.defaultProps = {
component: 'button',
noRouter: false
};
export default Link;

View File

@@ -0,0 +1,96 @@
import classNames from 'classnames';
import React, {
ComponentClass,
FunctionComponent,
SyntheticEvent,
useCallback,
} from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styles from './Link.css';
interface ReactRouterLinkProps {
to?: string;
}
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
className?: string;
component?:
| string
| FunctionComponent<LinkProps>
| ComponentClass<LinkProps, unknown>;
to?: string;
target?: string;
isDisabled?: boolean;
noRouter?: boolean;
onPress?(event: SyntheticEvent): void;
}
function Link(props: LinkProps) {
const {
className,
component = 'button',
to,
target,
type,
isDisabled,
noRouter = false,
onPress,
...otherProps
} = props;
const onClick = useCallback(
(event: SyntheticEvent) => {
if (!isDisabled && onPress) {
onPress(event);
}
},
[isDisabled, onPress]
);
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
target,
};
let el = component;
if (to) {
if (/\w+?:\/\//.test(to)) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_blank';
linkProps.rel = 'noreferrer';
} else if (noRouter) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_self';
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
el = RouterLink;
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
linkProps.target = target;
}
}
if (el === 'button' || el === 'input') {
linkProps.type = type || 'button';
linkProps.disabled = isDisabled;
}
linkProps.className = classNames(
className,
styles.link,
to && styles.to,
isDisabled && 'isDisabled'
);
const elementProps = {
...otherProps,
type,
...linkProps,
};
elementProps.onClick = onClick;
return React.createElement(el, elementProps);
}
export default Link;

View File

@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
render() {
const {
kind,
isSpinning,
error,
children,
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK;
let iconKind = kinds.SUCCESS;
let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
if (hasWarning) {
iconName = icons.WARNING;
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
return (
<SpinnerButton
kind={kind}
isSpinning={isSpinning}
{...otherProps}
>
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
}
SpinnerErrorButton.propTypes = {
kind: PropTypes.oneOf(kinds.all),
isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object,
children: PropTypes.node.isRequired

View File

@@ -10,27 +10,55 @@ class InlineMarkdown extends Component {
render() {
const {
className,
data
data,
blockClassName
} = this.props;
// For now only replace links
// For now only replace links or code blocks (not both)
const markdownBlocks = [];
if (data) {
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0;
let match = null;
while ((match = regex.exec(data)) !== null) {
while ((match = linkRegex.exec(data)) !== null) {
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
endIndex = match.index + match[0].length;
}
if (endIndex !== data.length) {
if (endIndex !== data.length && markdownBlocks.length > 0) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
endIndex = 0;
match = null;
let matchedCode = false;
while ((match = codeRegex.exec(data)) !== null) {
matchedCode = true;
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<code key={`code-${match.index}`} className={blockClassName ?? null}>{match[0].substring(1, match[0].length - 1)}</code>);
endIndex = match.index + match[0].length;
}
if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
if (markdownBlocks.length === 0) {
markdownBlocks.push(data);
}
}
return <span className={className}>{markdownBlocks}</span>;
@@ -39,7 +67,8 @@ class InlineMarkdown extends Component {
InlineMarkdown.propTypes = {
className: PropTypes.string,
data: PropTypes.string
data: PropTypes.string,
blockClassName: PropTypes.string
};
export default InlineMarkdown;

View File

@@ -33,7 +33,7 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
{typeof filter.label === 'function' ? filter.label() : filter.label}
</FilterMenuItem>
);
})

View File

@@ -7,6 +7,7 @@ function ErrorPage(props) {
const {
version,
isLocalStorageSupported,
translationsError,
indexersError,
indexerStatusError,
indexerCategoriesError,
@@ -21,6 +22,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (translationsError) {
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
} else if (indexersError) {
errorMessage = getErrorMessage(indexersError, 'Failed to load indexers from API');
} else if (indexerStatusError) {
@@ -55,6 +58,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = {
version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired,
translationsError: PropTypes.object,
indexersError: PropTypes.object,
indexerStatusError: PropTypes.object,
indexerCategoriesError: PropTypes.object,

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchIndexers } from 'Store/Actions/indexerActions';
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
@@ -54,6 +54,7 @@ const selectIsPopulated = createSelector(
(state) => state.indexerStatus.isPopulated,
(state) => state.settings.indexerCategories.isPopulated,
(state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
(
customFiltersIsPopulated,
tagsIsPopulated,
@@ -63,7 +64,8 @@ const selectIsPopulated = createSelector(
indexersIsPopulated,
indexerStatusIsPopulated,
indexerCategoriesIsPopulated,
systemStatusIsPopulated
systemStatusIsPopulated,
translationsIsPopulated
) => {
return (
customFiltersIsPopulated &&
@@ -74,7 +76,8 @@ const selectIsPopulated = createSelector(
indexersIsPopulated &&
indexerStatusIsPopulated &&
indexerCategoriesIsPopulated &&
systemStatusIsPopulated
systemStatusIsPopulated &&
translationsIsPopulated
);
}
);
@@ -89,6 +92,7 @@ const selectErrors = createSelector(
(state) => state.indexerStatus.error,
(state) => state.settings.indexerCategories.error,
(state) => state.system.status.error,
(state) => state.app.translations.error,
(
customFiltersError,
tagsError,
@@ -98,7 +102,8 @@ const selectErrors = createSelector(
indexersError,
indexerStatusError,
indexerCategoriesError,
systemStatusError
systemStatusError,
translationsError
) => {
const hasError = !!(
customFiltersError ||
@@ -109,7 +114,8 @@ const selectErrors = createSelector(
indexersError ||
indexerStatusError ||
indexerCategoriesError ||
systemStatusError
systemStatusError ||
translationsError
);
return {
@@ -122,7 +128,8 @@ const selectErrors = createSelector(
indexersError,
indexerStatusError,
indexerCategoriesError,
systemStatusError
systemStatusError,
translationsError
};
}
);
@@ -184,6 +191,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() {
dispatch(fetchStatus());
},
dispatchFetchTranslations() {
dispatch(fetchTranslations());
},
onResize(dimensions) {
dispatch(saveDimensions(dimensions));
},
@@ -217,6 +227,7 @@ class PageConnector extends Component {
this.props.dispatchFetchUISettings();
this.props.dispatchFetchGeneralSettings();
this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
}
}
@@ -242,6 +253,7 @@ class PageConnector extends Component {
dispatchFetchUISettings,
dispatchFetchGeneralSettings,
dispatchFetchStatus,
dispatchFetchTranslations,
...otherProps
} = this.props;
@@ -282,6 +294,7 @@ PageConnector.propTypes = {
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired
};

View File

@@ -1,22 +1,19 @@
import React, { forwardRef, ReactNode, useCallback } from 'react';
import Scroller from 'Components/Scroller/Scroller';
import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react';
import Scroller, { OnScroll } from 'Components/Scroller/Scroller';
import ScrollDirection from 'Helpers/Props/ScrollDirection';
import { isLocked } from 'Utilities/scrollLock';
import styles from './PageContentBody.css';
interface PageContentBodyProps {
className: string;
innerClassName: string;
className?: string;
innerClassName?: string;
children: ReactNode;
initialScrollTop?: number;
onScroll?: (payload) => void;
onScroll?: (payload: OnScroll) => void;
}
const PageContentBody = forwardRef(
(
props: PageContentBodyProps,
ref: React.MutableRefObject<HTMLDivElement>
) => {
(props: PageContentBodyProps, ref: ForwardedRef<HTMLDivElement>) => {
const {
className = styles.contentBody,
innerClassName = styles.innerContentBody,
@@ -26,7 +23,7 @@ const PageContentBody = forwardRef(
} = props;
const onScrollWrapper = useCallback(
(payload) => {
(payload: OnScroll) => {
if (onScroll && !isLocked()) {
onScroll(payload);
}

View File

@@ -1,4 +1,5 @@
.jumpBar {
z-index: $pageJumpBarZIndex;
display: flex;
align-content: stretch;
align-items: stretch;

View File

@@ -20,12 +20,12 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [
{
iconName: icons.MOVIE_CONTINUING,
title: translate('Indexers'),
title: () => translate('Indexers'),
to: '/',
alias: '/indexers',
children: [
{
title: translate('Stats'),
title: () => translate('Stats'),
to: '/indexers/stats'
}
]
@@ -33,47 +33,47 @@ const links = [
{
iconName: icons.SEARCH,
title: translate('Search'),
title: () => translate('Search'),
to: '/search'
},
{
iconName: icons.ACTIVITY,
title: translate('History'),
title: () => translate('History'),
to: '/history'
},
{
iconName: icons.SETTINGS,
title: translate('Settings'),
title: () => translate('Settings'),
to: '/settings',
children: [
{
title: translate('Indexers'),
title: () => translate('Indexers'),
to: '/settings/indexers'
},
{
title: translate('Apps'),
title: () => translate('Apps'),
to: '/settings/applications'
},
{
title: translate('DownloadClients'),
title: () => translate('DownloadClients'),
to: '/settings/downloadclients'
},
{
title: translate('Connect'),
title: () => translate('Connect'),
to: '/settings/connect'
},
{
title: translate('Tags'),
title: () => translate('Tags'),
to: '/settings/tags'
},
{
title: translate('General'),
title: () => translate('General'),
to: '/settings/general'
},
{
title: translate('UI'),
title: () => translate('UI'),
to: '/settings/ui'
}
]
@@ -81,32 +81,32 @@ const links = [
{
iconName: icons.SYSTEM,
title: translate('System'),
title: () => translate('System'),
to: '/system/status',
children: [
{
title: translate('Status'),
title: () => translate('Status'),
to: '/system/status',
statusComponent: HealthStatusConnector
},
{
title: translate('Tasks'),
title: () => translate('Tasks'),
to: '/system/tasks'
},
{
title: translate('Backup'),
title: () => translate('Backup'),
to: '/system/backup'
},
{
title: translate('Updates'),
title: () => translate('Updates'),
to: '/system/updates'
},
{
title: translate('Events'),
title: () => translate('Events'),
to: '/system/events'
},
{
title: translate('LogFiles'),
title: () => translate('LogFiles'),
to: '/system/logs/files'
}
]

View File

@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
}
<span className={isChildItem ? styles.noIcon : null}>
{title}
{typeof title === 'function' ? title() : title}
</span>
{
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
PageSidebarItem.propTypes = {
iconName: PropTypes.object,
title: PropTypes.string.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
to: PropTypes.string.isRequired,
isActive: PropTypes.bool,
isActiveParent: PropTypes.bool,

View File

@@ -1,9 +1,21 @@
import classNames from 'classnames';
import { throttle } from 'lodash';
import React, { forwardRef, ReactNode, useEffect, useRef } from 'react';
import React, {
ForwardedRef,
forwardRef,
MutableRefObject,
ReactNode,
useEffect,
useRef,
} from 'react';
import ScrollDirection from 'Helpers/Props/ScrollDirection';
import styles from './Scroller.css';
export interface OnScroll {
scrollLeft: number;
scrollTop: number;
}
interface ScrollerProps {
className?: string;
scrollDirection?: ScrollDirection;
@@ -12,11 +24,11 @@ interface ScrollerProps {
scrollTop?: number;
initialScrollTop?: number;
children?: ReactNode;
onScroll?: (payload) => void;
onScroll?: (payload: OnScroll) => void;
}
const Scroller = forwardRef(
(props: ScrollerProps, ref: React.MutableRefObject<HTMLDivElement>) => {
(props: ScrollerProps, ref: ForwardedRef<HTMLDivElement>) => {
const {
className,
autoFocus = false,
@@ -30,7 +42,7 @@ const Scroller = forwardRef(
} = props;
const internalRef = useRef();
const currentRef = ref ?? internalRef;
const currentRef = (ref as MutableRefObject<HTMLDivElement>) ?? internalRef;
useEffect(
() => {

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'cell': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
import TableRowCell from './TableRowCell';
import styles from './TableRowCellButton.css';
function TableRowCellButton({ className, ...otherProps }) {
return (
<Link
className={className}
component={TableRowCell}
{...otherProps}
/>
);
}
TableRowCellButton.propTypes = {
className: PropTypes.string.isRequired
};
TableRowCellButton.defaultProps = {
className: styles.cell
};
export default TableRowCellButton;

View File

@@ -0,0 +1,19 @@
import React, { ReactNode } from 'react';
import Link, { LinkProps } from 'Components/Link/Link';
import TableRowCell from './TableRowCell';
import styles from './TableRowCellButton.css';
interface TableRowCellButtonProps extends LinkProps {
className?: string;
children: ReactNode;
}
function TableRowCellButton(props: TableRowCellButtonProps) {
const { className = styles.cell, ...otherProps } = props;
return (
<Link className={className} component={TableRowCell} {...otherProps} />
);
}
export default TableRowCellButton;

View File

@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'input': string;
'selectCell': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,8 +1,12 @@
import React from 'react';
type PropertyFunction<T> = () => T;
interface Column {
name: string;
label: string;
columnLabel: string;
isSortable: boolean;
label: string | PropertyFunction<string> | React.ReactNode;
columnLabel?: string;
isSortable?: boolean;
isVisible: boolean;
isModifiable?: boolean;
}

View File

@@ -107,7 +107,7 @@ function Table(props) {
{...getTableHeaderCellProps(otherProps)}
{...column}
>
{column.label}
{typeof column.label === 'function' ? column.label() : column.label}
</TableHeaderCell>
);
})
@@ -121,6 +121,7 @@ function Table(props) {
}
Table.propTypes = {
...TableHeaderCell.props,
className: PropTypes.string,
horizontalScroll: PropTypes.bool.isRequired,
selectAll: PropTypes.bool.isRequired,

View File

@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
const {
className,
name,
label,
columnLabel,
isSortable,
isVisible,
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
{...otherProps}
component="th"
className={className}
title={columnLabel}
label={typeof label === 'function' ? label() : label}
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
onPress={this.onPress}
>
{children}
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
TableHeaderCell.propTypes = {
className: PropTypes.string,
name: PropTypes.string.isRequired,
columnLabel: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSortable: PropTypes.bool,
isVisible: PropTypes.bool,
isModifiable: PropTypes.bool,

View File

@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
isDisabled={isModifiable === false}
onChange={onVisibleChange}
/>
{label}
{typeof label === 'function' ? label() : label}
</label>
{
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,

View File

@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
<TableOptionsColumn
name={name}
label={label}
label={typeof label === 'function' ? label() : label}
isVisible={isVisible}
isModifiable={isModifiable}
index={index}
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
TableOptionsColumnDragSource.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,

View File

@@ -39,7 +39,8 @@ class VirtualTable extends Component {
super(props, context);
this.state = {
width: 0
width: 0,
scrollRestored: false
};
this._grid = null;
@@ -48,20 +49,25 @@ class VirtualTable extends Component {
componentDidUpdate(prevProps, prevState) {
const {
items,
scrollIndex
scrollIndex,
scrollTop
} = this.props;
const {
width
width,
scrollRestored
} = this.state;
if (this._grid &&
(prevState.width !== width ||
hasDifferentItemsOrOrder(prevProps.items, items))) {
if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
if (this._grid && scrollTop !== undefined && scrollTop !== 0 && !scrollRestored) {
this.setState({ scrollRestored: true });
this._grid.scrollToPosition({ scrollTop });
}
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
this._grid.scrollToCell({
rowIndex: scrollIndex,
@@ -98,6 +104,7 @@ class VirtualTable extends Component {
focusScroller,
header,
headerHeight,
rowHeight,
rowRenderer,
...otherProps
} = this.props;
@@ -141,6 +148,7 @@ class VirtualTable extends Component {
{header}
<div ref={registerChild}>
<Grid
{...otherProps}
ref={this.setGridRef}
autoContainerWidth={true}
autoHeight={true}
@@ -148,7 +156,7 @@ class VirtualTable extends Component {
width={width}
height={height}
headerHeight={height - headerHeight}
rowHeight={ROW_HEIGHT}
rowHeight={rowHeight}
rowCount={items.length}
columnCount={1}
columnWidth={width}
@@ -162,7 +170,6 @@ class VirtualTable extends Component {
className={styles.tableBodyContainer}
style={gridStyle}
containerStyle={containerStyle}
{...otherProps}
/>
</div>
</Scroller>
@@ -180,16 +187,19 @@ VirtualTable.propTypes = {
className: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
scrollIndex: PropTypes.number,
scrollTop: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired,
focusScroller: PropTypes.bool.isRequired,
header: PropTypes.node.isRequired,
headerHeight: PropTypes.number.isRequired,
rowRenderer: PropTypes.func.isRequired
rowRenderer: PropTypes.func.isRequired,
rowHeight: PropTypes.number.isRequired
};
VirtualTable.defaultProps = {
className: styles.tableContainer,
headerHeight: 38,
rowHeight: ROW_HEIGHT,
focusScroller: true
};

View File

@@ -67,8 +67,10 @@ function keyboardShortcuts(WrappedComponent) {
};
unbindShortcut = (key) => {
delete this._mousetrapBindings[key];
this._mousetrap.unbind(key);
if (this._mousetrap != null) {
delete this._mousetrapBindings[key];
this._mousetrap.unbind(key);
}
};
unbindAllShortcuts = () => {

View File

@@ -1,24 +1,30 @@
import PropTypes from 'prop-types';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import scrollPositions from 'Store/scrollPositions';
function withScrollPosition(WrappedComponent, scrollPositionKey) {
function ScrollPosition(props) {
interface WrappedComponentProps {
initialScrollTop: number;
}
interface ScrollPositionProps {
history: RouteComponentProps['history'];
location: RouteComponentProps['location'];
match: RouteComponentProps['match'];
}
function withScrollPosition(
WrappedComponent: React.FC<WrappedComponentProps>,
scrollPositionKey: string
) {
function ScrollPosition(props: ScrollPositionProps) {
const { history } = props;
const initialScrollTop =
history.action === 'POP' ||
(history.location.state && history.location.state.restoreScrollPosition)
? scrollPositions[scrollPositionKey]
: 0;
history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0;
return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
}
ScrollPosition.propTypes = {
history: PropTypes.object.isRequired,
};
return ScrollPosition;
}

View File

@@ -0,0 +1,11 @@
import { useEffect, useRef } from 'react';
export default function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}

View File

@@ -72,8 +72,10 @@ import {
faLanguage as fasLanguage,
faLaptop as fasLaptop,
faLevelUpAlt as fasLevelUpAlt,
faListCheck as fasListCheck,
faLocationArrow as fasLocationArrow,
faLock as fasLock,
faMagnet as fasMagnet,
faMedkit as fasMedkit,
faMinus as fasMinus,
faMusic as fasMusic,
@@ -180,6 +182,8 @@ export const INTERACTIVE = fasUser;
export const KEYBOARD = farKeyboard;
export const LOCK = fasLock;
export const LOGOUT = fasSignOutAlt;
export const MAGNET = fasMagnet;
export const MANAGE = fasListCheck;
export const MEDIA_INFO = farFileInvoice;
export const MISSING = fasExclamationTriangle;
export const MONITORED = fasBookmark;

View File

@@ -9,6 +9,8 @@ export const KEY_VALUE_LIST = 'keyValueList';
export const INFO = 'info';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
export const CATEGORY_SELECT = 'newznabCategorySelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const FLOAT = 'float';
export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';
@@ -34,6 +36,7 @@ export const all = [
INFO,
MOVIE_MONITORED_SELECT,
CATEGORY_SELECT,
FLOAT,
NUMBER,
OAUTH,
PASSWORD,

View File

@@ -18,6 +18,8 @@ function HistoryDetails(props) {
query,
queryResults,
categories,
limit,
offset,
source,
url
} = data;
@@ -31,43 +33,66 @@ function HistoryDetails(props) {
/>
{
!!indexer &&
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer.name}
/>
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('QueryResults')}
data={queryResults ? queryResults : '-'}
/>
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('Categories')}
data={categories ? categories : '-'}
/>
/> :
null
}
{
!!data &&
limit ?
<DescriptionListItem
title={translate('Limit')}
data={limit}
/> :
null
}
{
offset ?
<DescriptionListItem
title={translate('Offset')}
data={offset}
/> :
null
}
{
data ?
<DescriptionListItem
title={translate('Source')}
data={source}
/>
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('Url')}
data={url ? <Link to={url}>{translate('Link')}</Link> : '-'}
/>
/> :
null
}
</DescriptionList>
);
@@ -76,42 +101,46 @@ function HistoryDetails(props) {
if (eventType === 'releaseGrabbed') {
const {
source,
title,
grabTitle,
url
} = data;
return (
<DescriptionList>
{
!!indexer &&
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer.name}
/>
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('Source')}
data={source ? source : '-'}
/>
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('Title')}
data={title ? title : '-'}
/>
title={translate('GrabTitle')}
data={grabTitle ? grabTitle : '-'}
/> :
null
}
{
!!data &&
data ?
<DescriptionListItem
title={translate('Url')}
data={url ? <Link to={url}>{translate('Link')}</Link> : '-'}
/>
/> :
null
}
</DescriptionList>
);
@@ -124,11 +153,12 @@ function HistoryDetails(props) {
title={translate('Auth')}
>
{
!!indexer &&
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer.name}
/>
/> :
null
}
</DescriptionList>
);

View File

@@ -62,6 +62,7 @@ class HistoryOptions extends Component {
<FormInputGroup
type={inputTypes.NUMBER}
name="historyCleanupDays"
unit={translate('days')}
value={historyCleanupDays}
helpText={translate('HistoryCleanupDaysHelpText')}
helpTextWarning={translate('HistoryCleanupDaysHelpTextWarning')}

View File

@@ -26,9 +26,7 @@
width: 70px;
}
.parameters {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
.parametersContent {
display: flex;
flex-wrap: wrap;
}

View File

@@ -6,7 +6,7 @@ interface CssExports {
'details': string;
'elapsedTime': string;
'indexer': string;
'parameters': string;
'parametersContent': string;
'query': string;
'releaseGroup': string;
'source': string;

View File

@@ -1,17 +1,39 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
import translate from 'Utilities/String/translate';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import * as historyDataTypes from './historyDataTypes';
import HistoryEventTypeCell from './HistoryEventTypeCell';
import HistoryRowParameter from './HistoryRowParameter';
import styles from './HistoryRow.css';
const historyParameters = [
{ key: historyDataTypes.IMDB_ID, title: 'IMDb' },
{ key: historyDataTypes.TMDB_ID, title: 'TMDb' },
{ key: historyDataTypes.TVDB_ID, title: 'TVDb' },
{ key: historyDataTypes.TRAKT_ID, title: 'Trakt' },
{ key: historyDataTypes.R_ID, title: 'TvRage' },
{ key: historyDataTypes.TVMAZE_ID, title: 'TvMaze' },
{ key: historyDataTypes.SEASON, title: translate('Season') },
{ key: historyDataTypes.EPISODE, title: translate('Episode') },
{ key: historyDataTypes.ARTIST, title: translate('Artist') },
{ key: historyDataTypes.ALBUM, title: translate('Album') },
{ key: historyDataTypes.LABEL, title: translate('Label') },
{ key: historyDataTypes.TRACK, title: translate('Track') },
{ key: historyDataTypes.YEAR, title: translate('Year') },
{ key: historyDataTypes.GENRE, title: translate('Genre') },
{ key: historyDataTypes.AUTHOR, title: translate('Author') },
{ key: historyDataTypes.TITLE, title: translate('Title') },
{ key: historyDataTypes.PUBLISHER, title: translate('Publisher') }
];
class HistoryRow extends Component {
//
@@ -44,15 +66,52 @@ class HistoryRow extends Component {
data
} = this.props;
const { query, queryType, limit, offset } = data;
let searchQuery = query;
let categories = [];
if (data.categories) {
categories = data.categories.split(',').map((item) => {
return parseInt(item);
});
categories = data.categories.split(',').map((item) => parseInt(item));
}
this.props.onSearchPress(data.query, indexer.id, categories);
const searchParams = [
historyDataTypes.IMDB_ID,
historyDataTypes.TMDB_ID,
historyDataTypes.TVDB_ID,
historyDataTypes.TRAKT_ID,
historyDataTypes.R_ID,
historyDataTypes.TVMAZE_ID,
historyDataTypes.SEASON,
historyDataTypes.EPISODE,
historyDataTypes.ARTIST,
historyDataTypes.ALBUM,
historyDataTypes.LABEL,
historyDataTypes.TRACK,
historyDataTypes.YEAR,
historyDataTypes.GENRE,
historyDataTypes.AUTHOR,
historyDataTypes.TITLE,
historyDataTypes.PUBLISHER
]
.reduce((acc, key) => {
if (key in data && data[key].length > 0) {
const value = data[key];
acc.push({ key, value });
}
return acc;
}, [])
.map((item) => `{${item.key}:${item.value}}`)
.join('')
;
if (searchParams.length > 0) {
searchQuery += `${searchParams}`;
}
this.props.onSearchPress(searchQuery, indexer.id, categories, queryType, parseInt(limit), parseInt(offset));
};
onDetailsPress = () => {
@@ -84,6 +143,8 @@ class HistoryRow extends Component {
return null;
}
const parameters = historyParameters.filter((parameter) => parameter.key in data && data[parameter.key]);
return (
<TableRow>
{
@@ -133,162 +194,19 @@ class HistoryRow extends Component {
if (name === 'parameters') {
return (
<TableRowCell
key={name}
className={styles.parameters}
>
{
data.imdbId ?
<HistoryRowParameter
title='IMDb'
value={data.imdbId}
/> :
null
}
{
data.tmdbId ?
<HistoryRowParameter
title='TMDb'
value={data.tmdbId}
/> :
null
}
{
data.tvdbId ?
<HistoryRowParameter
title='TVDb'
value={data.tvdbId}
/> :
null
}
{
data.traktId ?
<HistoryRowParameter
title='Trakt'
value={data.traktId}
/> :
null
}
{
data.rId ?
<HistoryRowParameter
title='TvRage'
value={data.rId}
/> :
null
}
{
data.tvMazeId ?
<HistoryRowParameter
title='TvMaze'
value={data.tvMazeId}
/> :
null
}
{
data.season ?
<HistoryRowParameter
title={translate('Season')}
value={data.season}
/> :
null
}
{
data.episode ?
<HistoryRowParameter
title={translate('Episode')}
value={data.episode}
/> :
null
}
{
data.artist ?
<HistoryRowParameter
title={translate('Artist')}
value={data.artist}
/> :
null
}
{
data.album ?
<HistoryRowParameter
title={translate('Album')}
value={data.album}
/> :
null
}
{
data.label ?
<HistoryRowParameter
title={translate('Label')}
value={data.label}
/> :
null
}
{
data.track ?
<HistoryRowParameter
title={translate('Track')}
value={data.track}
/> :
null
}
{
data.year ?
<HistoryRowParameter
title={translate('Year')}
value={data.year}
/> :
null
}
{
data.genre ?
<HistoryRowParameter
title={translate('Genre')}
value={data.genre}
/> :
null
}
{
data.author ?
<HistoryRowParameter
title={translate('Author')}
value={data.author}
/> :
null
}
{
data.bookTitle ?
<HistoryRowParameter
title={translate('Book')}
value={data.bookTitle}
/> :
null
}
{
data.publisher ?
<HistoryRowParameter
title={translate('Publisher')}
value={data.publisher}
/> :
null
}
<TableRowCell key={name}>
<div className={styles.parametersContent}>
{parameters.map((parameter) => {
return (
<HistoryRowParameter
key={parameter.key}
title={parameter.title}
value={data[parameter.key]}
/>
);
}
)}
</div>
</TableRowCell>
);
}
@@ -300,8 +218,25 @@ class HistoryRow extends Component {
className={styles.indexer}
>
{
data.title ?
data.title :
data.grabTitle ?
data.grabTitle :
null
}
</TableRowCell>
);
}
if (name === 'queryType') {
return (
<TableRowCell
key={name}
className={styles.query}
>
{
data.queryType ?
<Label kind={kinds.INFO}>
{data.queryType}
</Label> :
null
}
</TableRowCell>
@@ -377,6 +312,12 @@ class HistoryRow extends Component {
key={name}
className={styles.details}
>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
title={translate('HistoryDetails')}
/>
{
eventType === 'indexerQuery' ?
<IconButton
@@ -386,11 +327,6 @@ class HistoryRow extends Component {
/> :
null
}
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
title={translate('HistoryDetails')}
/>
</TableRowCell>
);
}

View File

@@ -1,4 +1,5 @@
import { push } from 'connected-react-router';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -48,8 +49,15 @@ class HistoryRowConnector extends Component {
//
// Listeners
onSearchPress = (term, indexerId, categories) => {
this.props.setSearchDefault({ searchQuery: term, searchIndexerIds: [indexerId], searchCategories: categories });
onSearchPress = (query, indexerId, categories, type, limit, offset) => {
this.props.setSearchDefault(_.pickBy({
searchQuery: query,
searchIndexerIds: [indexerId],
searchCategories: categories,
searchType: type,
searchLimit: limit,
searchOffset: offset
}));
this.props.push(`${window.Prowlarr.urlBase}/search`);
};

View File

@@ -0,0 +1,17 @@
export const IMDB_ID = 'imdbId';
export const TMDB_ID = 'tmdbId';
export const TVDB_ID = 'tvdbId';
export const TRAKT_ID = 'traktId';
export const R_ID = 'rId';
export const TVMAZE_ID = 'tvMazeId';
export const SEASON = 'season';
export const EPISODE = 'episode';
export const ARTIST = 'artist';
export const ALBUM = 'album';
export const LABEL = 'label';
export const TRACK = 'track';
export const YEAR = 'year';
export const GENRE = 'genre';
export const AUTHOR = 'author';
export const TITLE = 'title';
export const PUBLISHER = 'publisher';

View File

@@ -40,7 +40,6 @@
flex: 1;
flex-direction: column;
margin-right: 12px;
max-width: 50%;
}
.filterContainer:last-child {
@@ -53,17 +52,22 @@
}
@media only screen and (max-width: $breakpointSmall) {
.filterInput {
margin-bottom: 5px;
}
.alert {
display: none;
}
.filterRow {
flex-direction: column;
display: block;
margin-bottom: 10px;
}
.filterContainer {
margin-right: 0;
margin-bottom: 12px;
margin-bottom: 5px;
}
.scroller {
@@ -72,3 +76,30 @@
margin-left: -30px;
}
}
@media only screen and (min-width: $breakpointSmall) {
.filterContainer {
max-width: 50%;
}
}
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.available {
font-weight: bold;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
.available {
display: none;
}
}

View File

@@ -2,12 +2,14 @@
// Please do not change this file!
interface CssExports {
'alert': string;
'available': string;
'filterContainer': string;
'filterInput': string;
'filterLabel': string;
'filterRow': string;
'indexers': string;
'modalBody': string;
'modalFooter': string;
'scroller': string;
}
export const cssExports: CssExports;

View File

@@ -22,31 +22,31 @@ import styles from './AddIndexerModalContent.css';
const columns = [
{
name: 'protocol',
label: translate('Protocol'),
label: () => translate('Protocol'),
isSortable: true,
isVisible: true
},
{
name: 'sortName',
label: translate('Name'),
label: () => translate('Name'),
isSortable: true,
isVisible: true
},
{
name: 'language',
label: translate('Language'),
label: () => translate('Language'),
isSortable: true,
isVisible: true
},
{
name: 'description',
label: translate('Description'),
label: () => translate('Description'),
isSortable: false,
isVisible: true
},
{
name: 'privacy',
label: translate('Privacy'),
label: () => translate('Privacy'),
isSortable: true,
isVisible: true
}
@@ -66,15 +66,21 @@ const protocols = [
const privacyLevels = [
{
key: 'private',
value: translate('Private')
get value() {
return translate('Private');
}
},
{
key: 'semiPrivate',
value: translate('SemiPrivate')
get value() {
return translate('SemiPrivate');
}
},
{
key: 'public',
value: translate('Public')
get value() {
return translate('Public');
}
}
];
@@ -257,6 +263,7 @@ class AddIndexerModalContent extends Component {
<SelectIndexerRowConnector
key={`${indexer.implementation}-${indexer.name}`}
implementation={indexer.implementation}
implementationName={indexer.implementationName}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
@@ -278,12 +285,18 @@ class AddIndexerModalContent extends Component {
</Scroller>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
<ModalFooter className={styles.modalFooter}>
<div className={styles.available}>
{
isPopulated ?
translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
null
}
</div>
<div>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</div>
</ModalFooter>
</ModalContent>
);

View File

@@ -49,8 +49,8 @@ class AddIndexerModalContentConnector extends Component {
//
// Listeners
onIndexerSelect = ({ implementation, name }) => {
this.props.selectIndexerSchema({ implementation, name });
onIndexerSelect = ({ implementation, implementationName, name }) => {
this.props.selectIndexerSchema({ implementation, implementationName, name });
this.props.onSelectIndexer();
};

View File

@@ -10,12 +10,14 @@ class AddIndexerPresetMenuItem extends Component {
onPress = () => {
const {
name,
implementation
implementation,
implementationName
} = this.props;
this.props.onPress({
name,
implementation
implementation,
implementationName
});
};
@@ -26,6 +28,7 @@ class AddIndexerPresetMenuItem extends Component {
const {
name,
implementation,
implementationName,
...otherProps
} = this.props;
@@ -43,6 +46,7 @@ class AddIndexerPresetMenuItem extends Component {
AddIndexerPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};

View File

@@ -17,10 +17,11 @@ class SelectIndexerRow extends Component {
onPress = () => {
const {
implementation,
implementationName,
name
} = this.props;
this.props.onIndexerSelect({ implementation, name });
this.props.onIndexerSelect({ implementation, implementationName, name });
};
//
@@ -81,6 +82,7 @@ SelectIndexerRow.propTypes = {
language: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onIndexerSelect: PropTypes.func.isRequired,
isExistingIndexer: PropTypes.bool.isRequired
};

View File

@@ -1,34 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteIndexerModalContentConnector from './DeleteIndexerModalContentConnector';
function DeleteIndexerModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={onModalClose}
>
<DeleteIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
DeleteIndexerModal.propTypes = {
...DeleteIndexerModalContentConnector.propTypes,
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteIndexerModal;

View File

@@ -0,0 +1,25 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
interface DeleteIndexerModalProps {
isOpen: boolean;
indexerId: number;
onModalClose(): void;
}
function DeleteIndexerModal(props: DeleteIndexerModalProps) {
const { isOpen, indexerId, onModalClose } = props;
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<DeleteIndexerModalContent
indexerId={indexerId}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default DeleteIndexerModal;

View File

@@ -1,88 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class DeleteIndexerModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false,
addImportExclusion: false
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
};
onAddImportExclusionChange = ({ value }) => {
this.setState({ addImportExclusion: value });
};
onDeleteMovieConfirmed = () => {
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = this.state.addImportExclusion;
this.setState({ deleteFiles: false, addImportExclusion: false });
this.props.onDeletePress(deleteFiles, addImportExclusion);
};
//
// Render
render() {
const {
name,
onModalClose
} = this.props;
return (
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
Delete - {name}
</ModalHeader>
<ModalBody>
{`Are you sure you want to delete ${name} from Prowlarr`}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteMovieConfirmed}
>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteIndexerModalContent.propTypes = {
name: PropTypes.string.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteIndexerModalContent;

View File

@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import Indexer from 'Indexer/Indexer';
import { deleteIndexer } from 'Store/Actions/indexerActions';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import translate from 'Utilities/String/translate';
interface DeleteIndexerModalContentProps {
indexerId: number;
onModalClose(): void;
}
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
const { indexerId, onModalClose } = props;
const { name } = useSelector(
createIndexerSelectorForHook(indexerId)
) as Indexer;
const dispatch = useDispatch();
const onConfirmDelete = useCallback(() => {
dispatch(deleteIndexer({ id: indexerId }));
onModalClose();
}, [indexerId, dispatch, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('Delete')} - {name}
</ModalHeader>
<ModalBody>
{translate('AreYouSureYouWantToDeleteIndexer', { name })}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
<Button kind={kinds.DANGER} onPress={onConfirmDelete}>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default DeleteIndexerModalContent;

View File

@@ -1,57 +0,0 @@
import { push } from 'connected-react-router';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteIndexer } from 'Store/Actions/indexerActions';
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
function createMapStateToProps() {
return createSelector(
createIndexerSelector(),
(indexer) => {
return indexer;
}
);
}
const mapDispatchToProps = {
deleteIndexer,
push
};
class DeleteIndexerModalContentConnector extends Component {
//
// Listeners
onDeletePress = () => {
this.props.deleteIndexer({
id: this.props.indexerId
});
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<DeleteIndexerModalContent
{...this.props}
onDeletePress={this.onDeletePress}
/>
);
}
}
DeleteIndexerModalContentConnector.propTypes = {
indexerId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired,
deleteIndexer: PropTypes.func.isRequired,
push: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(DeleteIndexerModalContentConnector);

View File

@@ -26,6 +26,8 @@ function EditIndexerModalContent(props) {
isTesting,
saveError,
item,
hasUsenetDownloadClients,
hasTorrentDownloadClients,
onInputChange,
onFieldChange,
onModalClose,
@@ -48,15 +50,18 @@ function EditIndexerModalContent(props) {
appProfileId,
tags,
fields,
priority
priority,
protocol,
downloadClientId
} = item;
const indexerDisplayName = implementationName === definitionName ? implementationName : `${implementationName} (${definitionName})`;
const showDownloadClientInput = downloadClientId.value > 0 || protocol.value === 'usenet' && hasUsenetDownloadClients || protocol.value === 'torrent' && hasTorrentDownloadClients;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? translate('EditIndexer') : translate('AddIndexer')} - ${indexerDisplayName}`}
{id ? translate('EditIndexerImplementation', { implementationName: indexerDisplayName }) : translate('AddIndexerImplementation', { implementationName: indexerDisplayName })}
</ModalHeader>
<ModalBody>
@@ -156,6 +161,25 @@ function EditIndexerModalContent(props) {
/>
</FormGroup>
{showDownloadClientInput ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={onInputChange}
/>
</FormGroup> : null
}
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
@@ -222,6 +246,8 @@ EditIndexerModalContent.propTypes = {
isTesting: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
hasUsenetDownloadClients: PropTypes.bool.isRequired,
hasTorrentDownloadClients: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,

View File

@@ -3,17 +3,23 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions';
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import { fetchDownloadClients, toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector';
import EditIndexerModalContent from './EditIndexerModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
(state) => state.settings.downloadClients,
createIndexerSchemaSelector(),
(advancedSettings, indexer) => {
(advancedSettings, downloadClients, indexer) => {
const usenetDownloadClients = downloadClients.items.filter((downloadClient) => downloadClient.protocol === 'usenet');
const torrentDownloadClients = downloadClients.items.filter((downloadClient) => downloadClient.protocol === 'torrent');
return {
advancedSettings,
hasUsenetDownloadClients: usenetDownloadClients.length > 0,
hasTorrentDownloadClients: torrentDownloadClients.length > 0,
...indexer
};
}
@@ -25,7 +31,8 @@ const mapDispatchToProps = {
setIndexerFieldValue,
saveIndexer,
testIndexer,
toggleAdvancedSettings
toggleAdvancedSettings,
dispatchFetchDownloadClients: fetchDownloadClients
};
class EditIndexerModalContentConnector extends Component {
@@ -33,6 +40,10 @@ class EditIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchDownloadClients();
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
@@ -90,7 +101,8 @@ EditIndexerModalContentConnector.propTypes = {
toggleAdvancedSettings: PropTypes.func.isRequired,
saveIndexer: PropTypes.func.isRequired,
testIndexer: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
onModalClose: PropTypes.func.isRequired,
dispatchFetchDownloadClients: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector);

View File

@@ -1,6 +1,10 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import IndexerAppState, {
IndexerIndexAppState,
} from 'App/State/IndexerAppState';
import { APP_INDEXER_SYNC } from 'Commands/commandNames';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
@@ -41,9 +45,7 @@ import IndexerIndexTable from './Table/IndexerIndexTable';
import IndexerIndexTableOptions from './Table/IndexerIndexTableOptions';
import styles from './IndexerIndex.css';
function getViewComponent() {
return IndexerIndexTable;
}
const getViewComponent = () => IndexerIndexTable;
interface IndexerIndexProps {
initialScrollTop?: number;
@@ -64,29 +66,22 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
sortKey,
sortDirection,
view,
} = useSelector(
createIndexerClientSideCollectionItemsSelector('indexerIndex')
);
}: IndexerAppState & IndexerIndexAppState & ClientSideCollectionAppState =
useSelector(createIndexerClientSideCollectionItemsSelector('indexerIndex'));
const isSyncingIndexers = useSelector(
createCommandExecutingSelector(APP_INDEXER_SYNC)
);
const { isSmallScreen } = useSelector(createDimensionsSelector());
const dispatch = useDispatch();
const scrollerRef = useRef<HTMLDivElement>();
const scrollerRef = useRef<HTMLDivElement>(null);
const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
undefined
);
const [isSelectMode, setIsSelectMode] = useState(false);
const onAppIndexerSyncPress = useCallback(() => {
dispatch(
executeCommand({
name: APP_INDEXER_SYNC,
})
);
}, [dispatch]);
const onAddIndexerPress = useCallback(() => {
setIsAddIndexerModalOpen(true);
}, [setIsAddIndexerModalOpen]);
@@ -103,6 +98,15 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
setIsEditIndexerModalOpen(false);
}, [setIsEditIndexerModalOpen]);
const onAppIndexerSyncPress = useCallback(() => {
dispatch(
executeCommand({
name: APP_INDEXER_SYNC,
forceSync: true,
})
);
}, [dispatch]);
const onTestAllPress = useCallback(() => {
dispatch(testAllIndexers());
}, [dispatch]);
@@ -112,53 +116,53 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
}, [isSelectMode, setIsSelectMode]);
const onTableOptionChange = useCallback(
(payload) => {
(payload: unknown) => {
dispatch(setIndexerTableOption(payload));
},
[dispatch]
);
const onSortSelect = useCallback(
(value) => {
(value: string) => {
dispatch(setIndexerSort({ sortKey: value }));
},
[dispatch]
);
const onFilterSelect = useCallback(
(value) => {
(value: string) => {
dispatch(setIndexerFilter({ selectedFilterKey: value }));
},
[dispatch]
);
const onJumpBarItemPress = useCallback(
(character) => {
(character: string) => {
setJumpToCharacter(character);
},
[setJumpToCharacter]
);
const onScroll = useCallback(
({ scrollTop }) => {
setJumpToCharacter(null);
scrollPositions.seriesIndex = scrollTop;
({ scrollTop }: { scrollTop: number }) => {
setJumpToCharacter(undefined);
scrollPositions.indexerIndex = scrollTop;
},
[setJumpToCharacter]
);
const jumpBarItems = useMemo(() => {
// Reset if not sorting by sortTitle
if (sortKey !== 'sortTitle') {
// Reset if not sorting by sortName
if (sortKey !== 'sortName') {
return {
order: [],
};
}
const characters = items.reduce((acc, item) => {
let char = item.sortTitle.charAt(0);
const characters = items.reduce((acc: Record<string, number>, item) => {
let char = item.sortName.charAt(0);
if (!isNaN(char)) {
if (!isNaN(Number(char))) {
char = '#';
}
@@ -190,7 +194,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
return (
<SelectProvider items={items}>
<PageContent>
<PageContent title={translate('Indexers')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
@@ -225,7 +229,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
label={
isSelectMode
? translate('StopSelecting')
: translate('SelectIndexer')
: translate('SelectIndexers')
}
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
isSelectMode={isSelectMode}
@@ -277,6 +281,8 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
<PageContentBody
ref={scrollerRef}
className={styles.contentBody}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
innerClassName={styles[`${view}InnerContentBody`]}
initialScrollTop={props.initialScrollTop}
onScroll={onScroll}

View File

@@ -1,12 +1,13 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setIndexerFilter } from 'Store/Actions/indexerIndexActions';
function createIndexerSelector() {
return createSelector(
(state) => state.indexers.items,
(state: AppState) => state.indexers.items,
(indexers) => {
return indexers;
}
@@ -15,14 +16,20 @@ function createIndexerSelector() {
function createFilterBuilderPropsSelector() {
return createSelector(
(state) => state.indexerIndex.filterBuilderProps,
(state: AppState) => state.indexerIndex.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
export default function IndexerIndexFilterModal(props) {
interface IndexerIndexFilterModalProps {
isOpen: boolean;
}
export default function IndexerIndexFilterModal(
props: IndexerIndexFilterModalProps
) {
const sectionItems = useSelector(createIndexerSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'indexerIndex';
@@ -30,7 +37,7 @@ export default function IndexerIndexFilterModal(props) {
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload) => {
(payload: unknown) => {
dispatch(setIndexerFilter(payload));
},
[dispatch]
@@ -38,6 +45,7 @@ export default function IndexerIndexFilterModal(props) {
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}

View File

@@ -3,6 +3,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
import IndexerAppState from 'App/State/IndexerAppState';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
@@ -13,7 +14,7 @@ import styles from './IndexerIndexFooter.css';
function createUnoptimizedSelector() {
return createSelector(
createClientSideCollectionSelector('indexers', 'indexerIndex'),
(indexers) => {
(indexers: IndexerAppState) => {
return indexers.items.map((s) => {
const { protocol, privacy, enable } = s;

View File

@@ -1,10 +1,18 @@
import PropTypes from 'prop-types';
import React from 'react';
import { CustomFilter } from 'App/State/AppState';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
import IndexerIndexFilterModal from 'Indexer/Index/IndexerIndexFilterModal';
function IndexerIndexFilterMenu(props) {
interface IndexerIndexFilterMenuProps {
selectedFilterKey: string | number;
filters: object[];
customFilters: CustomFilter[];
isDisabled: boolean;
onFilterSelect(filterName: string): unknown;
}
function IndexerIndexFilterMenu(props: IndexerIndexFilterMenuProps) {
const {
selectedFilterKey,
filters,
@@ -26,15 +34,6 @@ function IndexerIndexFilterMenu(props) {
);
}
IndexerIndexFilterMenu.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired,
};
IndexerIndexFilterMenu.defaultProps = {
showCustomFilters: false,
};

View File

@@ -1,12 +1,19 @@
import PropTypes from 'prop-types';
import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import SortMenu from 'Components/Menu/SortMenu';
import SortMenuItem from 'Components/Menu/SortMenuItem';
import { align, sortDirections } from 'Helpers/Props';
import { align } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import translate from 'Utilities/String/translate';
function IndexerIndexSortMenu(props) {
interface IndexerIndexSortMenuProps {
sortKey?: string;
sortDirection?: SortDirection;
isDisabled: boolean;
onSortSelect(sortKey: string): unknown;
}
function IndexerIndexSortMenu(props: IndexerIndexSortMenuProps) {
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
return (
@@ -79,11 +86,4 @@ function IndexerIndexSortMenu(props) {
);
}
IndexerIndexSortMenu.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
isDisabled: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired,
};
export default IndexerIndexSortMenu;

View File

@@ -7,8 +7,10 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import { bulkDeleteIndexers } from 'Store/Actions/indexerIndexActions';
import Indexer from 'Indexer/Indexer';
import { bulkDeleteIndexers } from 'Store/Actions/indexerActions';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import translate from 'Utilities/String/translate';
import styles from './DeleteIndexerModalContent.css';
interface DeleteIndexerModalContentProps {
@@ -19,21 +21,21 @@ interface DeleteIndexerModalContentProps {
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
const { indexerIds, onModalClose } = props;
const allIndexer = useSelector(createAllIndexersSelector());
const allIndexers: Indexer[] = useSelector(createAllIndexersSelector());
const dispatch = useDispatch();
const indexers = useMemo(() => {
const indexers = indexerIds.map((id) => {
return allIndexer.find((s) => s.id === id);
});
const indexers = useMemo((): Indexer[] => {
const indexerList = indexerIds.map((id) => {
return allIndexers.find((s) => s.id === id);
}) as Indexer[];
return orderBy(indexers, ['sortTitle']);
}, [indexerIds, allIndexer]);
return orderBy(indexerList, ['sortName']);
}, [indexerIds, allIndexers]);
const onDeleteIndexerConfirmed = useCallback(() => {
dispatch(
bulkDeleteIndexers({
indexerIds,
ids: indexerIds,
})
);
@@ -42,17 +44,19 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Delete Selected Indexer</ModalHeader>
<ModalHeader>{translate('DeleteSelectedIndexers')}</ModalHeader>
<ModalBody>
<div className={styles.message}>
{`Are you sure you want to delete ${indexers.length} selected indexers?`}
{translate('DeleteSelectedIndexersMessageText', {
count: indexers.length,
})}
</div>
<ul>
{indexers.map((s) => {
return (
<li key={s.name}>
<li key={s.id}>
<span>{s.name}</span>
</li>
);
@@ -61,10 +65,10 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.DANGER} onPress={onDeleteIndexerConfirmed}>
Delete
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -7,13 +7,18 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import { inputTypes, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
interface SavePayload {
enable?: boolean;
appProfileId?: number;
priority?: number;
minimumSeeders?: number;
seedRatio?: number;
seedTime?: number;
packSeedTime?: number;
}
interface EditIndexerModalContentProps {
@@ -25,9 +30,25 @@ interface EditIndexerModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'true', value: translate('Enabled') },
{ key: 'false', value: translate('Disabled') },
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'true',
get value() {
return translate('Enabled');
},
},
{
key: 'false',
get value() {
return translate('Disabled');
},
},
];
function EditIndexerModalContent(props: EditIndexerModalContentProps) {
@@ -35,6 +56,15 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
const [enable, setEnable] = useState(NO_CHANGE);
const [appProfileId, setAppProfileId] = useState<string | number>(NO_CHANGE);
const [priority, setPriority] = useState<null | string | number>(null);
const [minimumSeeders, setMinimumSeeders] = useState<null | string | number>(
null
);
const [seedRatio, setSeedRatio] = useState<null | string | number>(null);
const [seedTime, setSeedTime] = useState<null | string | number>(null);
const [packSeedTime, setPackSeedTime] = useState<null | string | number>(
null
);
const save = useCallback(() => {
let hasChanges = false;
@@ -50,15 +80,50 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
payload.appProfileId = appProfileId as number;
}
if (priority !== null) {
hasChanges = true;
payload.priority = priority as number;
}
if (minimumSeeders !== null) {
hasChanges = true;
payload.minimumSeeders = minimumSeeders as number;
}
if (seedRatio !== null) {
hasChanges = true;
payload.seedRatio = seedRatio as number;
}
if (seedTime !== null) {
hasChanges = true;
payload.seedTime = seedTime as number;
}
if (packSeedTime !== null) {
hasChanges = true;
payload.packSeedTime = packSeedTime as number;
}
if (hasChanges) {
onSavePress(payload);
}
onModalClose();
}, [enable, appProfileId, onSavePress, onModalClose]);
}, [
enable,
appProfileId,
priority,
minimumSeeders,
seedRatio,
seedTime,
packSeedTime,
onSavePress,
onModalClose,
]);
const onInputChange = useCallback(
({ name, value }) => {
({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'enable':
setEnable(value);
@@ -66,8 +131,23 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
case 'appProfileId':
setAppProfileId(value);
break;
case 'priority':
setPriority(value);
break;
case 'minimumSeeders':
setMinimumSeeders(value);
break;
case 'seedRatio':
setSeedRatio(value);
break;
case 'seedTime':
setSeedTime(value);
break;
case 'packSeedTime':
setPackSeedTime(value);
break;
default:
console.warn('EditIndexerModalContent Unknown Input');
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
}
},
[setEnable]
@@ -81,10 +161,10 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('Edit Selected Indexer')}</ModalHeader>
<ModalHeader>{translate('EditSelectedIndexers')}</ModalHeader>
<ModalBody>
<FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
@@ -96,30 +176,95 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
/>
</FormGroup>
<FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('SyncProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.APP_PROFILE_SELECT}
name="appProfileId"
value={appProfileId}
helpText={translate('AppProfileSelectHelpText')}
includeNoChange={true}
includeNoChangeDisabled={false}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('IndexerPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
value={priority}
min={1}
max={50}
helpText={translate('IndexerPriorityHelpText')}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('AppsMinimumSeeders')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="minimumSeeders"
value={minimumSeeders}
helpText={translate('AppsMinimumSeedersHelpText')}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('SeedRatio')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="seedRatio"
value={seedRatio}
helpText={translate('SeedRatioHelpText')}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('SeedTime')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="seedTime"
value={seedTime}
unit={translate('minutes')}
helpText={translate('SeedTimeHelpText')}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('PackSeedTime')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="packSeedTime"
value={packSeedTime}
unit={translate('minutes')}
helpText={translate('PackSeedTimeHelpText')}
onChange={onInputChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('{0} indexers selected', selectedCount.toString())}
{translate('CountIndexersSelected', { count: selectedCount })}
</div>
<div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button onPress={onSavePressWrapper}>
{translate('Apply Changes')}
{translate('ApplyChanges')}
</Button>
</div>
</ModalFooter>

View File

@@ -7,7 +7,7 @@ import translate from 'Utilities/String/translate';
interface IndexerIndexSelectAllButtonProps {
label: string;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent;
overflowComponent: React.FunctionComponent<never>;
}
function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) {

View File

@@ -2,10 +2,12 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useSelect } from 'App/SelectContext';
import AppState from 'App/State/AppState';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { kinds } from 'Helpers/Props';
import { saveIndexerEditor } from 'Store/Actions/indexerIndexActions';
import { bulkEditIndexers } from 'Store/Actions/indexerActions';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import DeleteIndexerModal from './Delete/DeleteIndexerModal';
@@ -13,8 +15,18 @@ import EditIndexerModal from './Edit/EditIndexerModal';
import TagsModal from './Tags/TagsModal';
import styles from './IndexerIndexSelectFooter.css';
const seriesEditorSelector = createSelector(
(state) => state.indexers,
interface SavePayload {
enable?: boolean;
appProfileId?: number;
priority?: number;
minimumSeeders?: number;
seedRatio?: number;
seedTime?: number;
packSeedTime?: number;
}
const indexersEditorSelector = createSelector(
(state: AppState) => state.indexers,
(indexers) => {
const { isSaving, isDeleting, deleteError } = indexers;
@@ -27,8 +39,9 @@ const seriesEditorSelector = createSelector(
);
function IndexerIndexSelectFooter() {
const { isSaving, isDeleting, deleteError } =
useSelector(seriesEditorSelector);
const { isSaving, isDeleting, deleteError } = useSelector(
indexersEditorSelector
);
const dispatch = useDispatch();
@@ -37,6 +50,7 @@ function IndexerIndexSelectFooter() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isSavingIndexer, setIsSavingIndexer] = useState(false);
const [isSavingTags, setIsSavingTags] = useState(false);
const previousIsDeleting = usePrevious(isDeleting);
const [selectState, selectDispatch] = useSelect();
const { selectedState } = selectState;
@@ -56,14 +70,14 @@ function IndexerIndexSelectFooter() {
}, [setIsEditModalOpen]);
const onSavePress = useCallback(
(payload) => {
(payload: SavePayload) => {
setIsSavingIndexer(true);
setIsEditModalOpen(false);
dispatch(
saveIndexerEditor({
bulkEditIndexers({
...payload,
indexerIds,
ids: indexerIds,
})
);
},
@@ -79,13 +93,13 @@ function IndexerIndexSelectFooter() {
}, [setIsTagsModalOpen]);
const onApplyTagsPress = useCallback(
(tags, applyTags) => {
(tags: number[], applyTags: string) => {
setIsSavingTags(true);
setIsTagsModalOpen(false);
dispatch(
saveIndexerEditor({
indexerIds,
bulkEditIndexers({
ids: indexerIds,
tags,
applyTags,
})
@@ -110,10 +124,10 @@ function IndexerIndexSelectFooter() {
}, [isSaving]);
useEffect(() => {
if (!isDeleting && !deleteError) {
if (previousIsDeleting && !isDeleting && !deleteError) {
selectDispatch({ type: 'unselectAll' });
}
}, [isDeleting, deleteError, selectDispatch]);
}, [previousIsDeleting, isDeleting, deleteError, selectDispatch]);
const anySelected = selectedCount > 0;
@@ -134,7 +148,7 @@ function IndexerIndexSelectFooter() {
isDisabled={!anySelected}
onPress={onTagsPress}
>
{translate('Set Tags')}
{translate('SetTags')}
</SpinnerButton>
</div>
@@ -151,7 +165,7 @@ function IndexerIndexSelectFooter() {
</div>
<div className={styles.selected}>
{translate('{0} indexers selected', selectedCount.toString())}
{translate('CountIndexersSelected', { count: selectedCount })}
</div>
<EditIndexerModal

View File

@@ -7,7 +7,7 @@ interface IndexerIndexSelectModeButtonProps {
label: string;
iconName: IconDefinition;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent;
overflowComponent: React.FunctionComponent<never>;
onPress: () => void;
}

View File

@@ -1,6 +1,7 @@
import { concat, uniq } from 'lodash';
import { uniq } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Tag } from 'App/State/TagsAppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -12,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import Indexer from 'Indexer/Indexer';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import translate from 'Utilities/String/translate';
@@ -26,29 +28,35 @@ interface TagsModalContentProps {
function TagsModalContent(props: TagsModalContentProps) {
const { indexerIds, onModalClose, onApplyTagsPress } = props;
const allIndexers = useSelector(createAllIndexersSelector());
const tagList = useSelector(createTagsSelector());
const allIndexers: Indexer[] = useSelector(createAllIndexersSelector());
const tagList: Tag[] = useSelector(createTagsSelector());
const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add');
const indexerTags = useMemo(() => {
const indexers = indexerIds.map((id) => {
return allIndexers.find((s) => s.id === id);
});
const tags = indexerIds.reduce((acc: number[], id) => {
const s = allIndexers.find((s) => s.id === id);
return uniq(concat(...indexers.map((s) => s.tags)));
if (s) {
acc.push(...s.tags);
}
return acc;
}, []);
return uniq(tags);
}, [indexerIds, allIndexers]);
const onTagsChange = useCallback(
({ value }) => {
({ value }: { value: number[] }) => {
setTags(value);
},
[setTags]
);
const onApplyTagsChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setApplyTags(value);
},
[setApplyTags]
@@ -59,14 +67,14 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: 'Add' },
{ key: 'remove', value: 'Remove' },
{ key: 'replace', value: 'Replace' },
{ key: 'add', value: translate('Add') },
{ key: 'remove', value: translate('Remove') },
{ key: 'replace', value: translate('Replace') },
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Tags</ModalHeader>
<ModalHeader>{translate('Tags')}</ModalHeader>
<ModalBody>
<Form>
@@ -90,10 +98,10 @@ function TagsModalContent(props: TagsModalContentProps) {
value={applyTags}
values={applyTagsOptions}
helpTexts={[
translate('ApplyTagsHelpTexts1'),
translate('ApplyTagsHelpTexts2'),
translate('ApplyTagsHelpTexts3'),
translate('ApplyTagsHelpTexts4'),
translate('ApplyTagsHelpTextHowToApplyIndexers'),
translate('ApplyTagsHelpTextAdd'),
translate('ApplyTagsHelpTextRemove'),
translate('ApplyTagsHelpTextReplace'),
]}
onChange={onApplyTagsChange}
/>
@@ -119,8 +127,8 @@ function TagsModalContent(props: TagsModalContentProps) {
key={tag.id}
title={
removeTag
? translate('RemoveTagRemovingTag')
: translate('RemoveTagExistingTag')
? translate('RemovingTag')
: translate('ExistingTag')
}
kind={removeTag ? kinds.INVERSE : kinds.INFO}
size={sizes.LARGE}
@@ -159,10 +167,10 @@ function TagsModalContent(props: TagsModalContentProps) {
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
Apply
{translate('Apply')}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -19,7 +19,11 @@
.priority,
.protocol,
.privacy {
.privacy,
.minimumSeeders,
.seedRatio,
.seedTime,
.packSeedTime {
composes: cell;
flex: 0 0 90px;

View File

@@ -8,9 +8,13 @@ interface CssExports {
'cell': string;
'checkInput': string;
'externalLink': string;
'minimumSeeders': string;
'packSeedTime': string;
'priority': string;
'privacy': string;
'protocol': string;
'seedRatio': string;
'seedTime': string;
'sortName': string;
'status': string;
'tags': string;

View File

@@ -12,7 +12,9 @@ import { icons } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItemSelector';
import Indexer from 'Indexer/Indexer';
import IndexerTitleLink from 'Indexer/IndexerTitleLink';
import { SelectStateInputProps } from 'typings/props';
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
import translate from 'Utilities/String/translate';
import CapabilitiesLabel from './CapabilitiesLabel';
@@ -46,7 +48,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
fields,
added,
capabilities,
} = indexer;
} = indexer as Indexer;
const baseUrl =
fields.find((field) => field.name === 'baseUrl')?.value ??
@@ -55,6 +57,23 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
const vipExpiration =
fields.find((field) => field.name === 'vipExpiration')?.value ?? '';
const minimumSeeders =
fields.find(
(field) => field.name === 'torrentBaseSettings.appMinimumSeeders'
)?.value ?? undefined;
const seedRatio =
fields.find((field) => field.name === 'torrentBaseSettings.seedRatio')
?.value ?? undefined;
const seedTime =
fields.find((field) => field.name === 'torrentBaseSettings.seedTime')
?.value ?? undefined;
const packSeedTime =
fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')
?.value ?? undefined;
const rssUrl = `${window.location.origin}${
window.Prowlarr.urlBase
}/${id}/api?apikey=${encodeURIComponent(
@@ -83,12 +102,8 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
setIsDeleteIndexerModalOpen(false);
}, [setIsDeleteIndexerModalOpen]);
const checkInputCallback = useCallback(() => {
// Mock handler to satisfy `onChange` being required for `CheckInput`.
}, []);
const onSelectedChange = useCallback(
({ id, value, shiftKey }) => {
({ id, value, shiftKey }: SelectStateInputProps) => {
selectDispatch({
type: 'toggleSelected',
id,
@@ -185,6 +200,8 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
if (name === 'added') {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
key={name}
className={styles[name]}
@@ -196,6 +213,8 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
if (name === 'vipExpiration') {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
key={name}
className={styles[name]}
@@ -213,10 +232,44 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
);
}
if (name === 'minimumSeeders') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{minimumSeeders}
</VirtualTableRowCell>
);
}
if (name === 'seedRatio') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{seedRatio}
</VirtualTableRowCell>
);
}
if (name === 'seedTime') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{seedTime}
</VirtualTableRowCell>
);
}
if (name === 'packSeedTime') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{packSeedTime}
</VirtualTableRowCell>
);
}
if (name === 'actions') {
return (
<VirtualTableRowCell
key={column.name}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
className={styles[column.name]}
>
<IconButton

View File

@@ -1,3 +1,11 @@
.tableScroller {
position: relative;
}
.row {
transition: background-color 500ms;
&:hover {
background-color: var(--tableRowHoverBackgroundColor);
}
}

View File

@@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'row': string;
'tableScroller': string;
}
export const cssExports: CssExports;

View File

@@ -1,8 +1,9 @@
import { throttle } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { RefObject, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Scroller from 'Components/Scroller/Scroller';
import Column from 'Components/Table/Column';
import useMeasure from 'Helpers/Hooks/useMeasure';
@@ -13,7 +14,6 @@ import dimensions from 'Styles/Variables/dimensions';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import IndexerIndexRow from './IndexerIndexRow';
import IndexerIndexTableHeader from './IndexerIndexTableHeader';
import selectTableOptions from './selectTableOptions';
import styles from './IndexerIndexTable.css';
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
@@ -30,17 +30,17 @@ interface RowItemData {
interface IndexerIndexTableProps {
items: Indexer[];
sortKey?: string;
sortKey: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const columnsSelector = createSelector(
(state) => state.indexerIndex.columns,
(state: AppState) => state.indexerIndex.columns,
(columns) => columns
);
@@ -64,6 +64,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
justifyContent: 'space-between',
...style,
}}
className={styles.row}
>
<IndexerIndexRow
indexerId={indexer.id}
@@ -91,19 +92,16 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
} = props;
const columns = useSelector(columnsSelector);
const { showBanners } = useSelector(selectTableOptions);
const listRef: React.MutableRefObject<List> = useRef();
const listRef = useRef<List<RowItemData>>(null);
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const rowHeight = useMemo(() => {
return showBanners ? 70 : 38;
}, [showBanners]);
const rowHeight = 38;
useEffect(() => {
const current = scrollerRef.current as HTMLElement;
const current = scrollerRef?.current as HTMLElement;
if (isSmallScreen) {
setSize({
@@ -127,8 +125,8 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@@ -137,7 +135,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
listRef.current.scrollTo(scrollTop);
listRef.current?.scrollTo(scrollTop);
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@@ -166,8 +164,8 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
scrollTop += offset;
}
listRef.current.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop);
listRef.current?.scrollTo(scrollTop);
scrollerRef?.current?.scrollTo(0, scrollTop);
}
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
@@ -179,7 +177,6 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
scrollDirection={ScrollDirection.Horizontal}
>
<IndexerIndexTableHeader
showBanners={showBanners}
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}

Some files were not shown because too many files have changed in this diff Show More