1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-28 18:05:41 -04:00

Compare commits

...

99 Commits

Author SHA1 Message Date
Servarr
8fd267580a Automated API Docs update 2023-06-21 06:45:42 +03:00
Bogdan
8974aa823b Remove not implemented endpoints from API docs
Closes #8724
2023-06-21 06:34:07 +03:00
Bogdan
41492efd2e Convert to 'using' declaration in Housekeeping Tasks
Closes #8723
2023-06-21 06:25:44 +03:00
Bogdan
d008768fff Prevent NullRef when deleting missing backups
(cherry picked from commit 0ff0fe2e68f3abf7b8e4d6bf0c1e9dee4eb68227)

Closes #8721
2023-06-21 06:19:30 +03:00
Bogdan
cb21fe535d Fix translation for Unreleased 2023-06-20 19:39:16 +03:00
Bogdan
4cce2727e2 Update translations
(cherry picked from commit 26031389757f6b5270bbe5591101b08e58debb73)
2023-06-20 02:38:55 +03:00
Stevie Robinson
b1ff82da37 Fixed: Parsing Vyndros as release group
(cherry picked from commit f2ddd4757c897c522b553de8bafb5340746253c9)

Closes #8569
2023-06-19 07:09:04 +03:00
Mark McDowall
c5266152c5 Fixed: Strip additional domains from release names
(cherry picked from commit e273f16c3905e0c2451f43cf98b9b7ad1cbdc777)

Closes #8603
2023-06-19 07:00:22 +03:00
Bogdan
783878be1e Minor improvements in health checks
(cherry picked from commit a22f598b0c129110f2a3b663e9b40c84f3a1f02b)

Closes #8615
2023-06-19 06:38:51 +03:00
Bogdan
0cbfb4ca65 New: (UI) Search library by imdbId and tmdbId 2023-06-19 04:19:12 +03:00
bakerboy448
c22c9400c2 New: Indexer Messaging and Error Improvements
(cherry picked from commit 3b505d8734dcbe3fa53acba7f94f1361151e6a44)
2023-06-18 12:06:55 +03:00
Bogdan
0288c4b704 Bump version to 4.6.3 2023-06-18 12:05:11 +03:00
Márki-Zay Ferenc
e4429d2919 Fixed: Close database connection in housekeeping tasks 2023-06-17 03:52:12 +03:00
Bogdan
7052a7a5ec New: Improved page loading errors
Closes #8706
2023-06-16 23:45:26 +03:00
Weblate
b38912851b Translated using Weblate (Portuguese (Brazil)) [skip ci]
Currently translated at 100.0% (1184 of 1184 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-06-16 02:21:40 +03:00
Bogdan
1354c2c337 Fix sorting history items by movie titles 2023-06-15 23:21:16 +03:00
Servarr
e259235df6 Automated API Docs update 2023-06-14 09:17:16 +03:00
Bogdan
0cc1fe8308 Add HelpTextWarning support in FieldDefinition
(cherry picked from commit 0e07d54ee77d5f83716e17b6757e23f38ff73694)

Closes #8687
2023-06-14 07:55:44 +03:00
Bogdan
f4fe18a440 Require ApiKey for all actions in RadarrImport
(cherry picked from commit 19b8fbe13bf584b915a05fe9fc87622adbaee0b7)

Closes #8692
2023-06-14 07:50:53 +03:00
Bogdan
eeed935e3a Update cleansing rules for RSS TL feed and homedir for Mac
(cherry picked from commit e5ff4aafa3f0b855fec332788e9fc490a03dfce3)

Closes #8684
2023-06-14 07:48:26 +03:00
Bogdan
1b3701371a Fixed: Treat redirects as errors in Radarr Import List
(cherry picked from commit 059a156f4a34c6b9cbe139fa1973b814e8a534ae)
2023-06-14 07:40:06 +03:00
Qstick
d56f3ec2e7 Fixed: Correctly handle 302 and 303 redirects in HttpClient
(cherry picked from commit ed7c5a937f4b50fcdf819e8fe347c8c0bc6bd2e7)

(cherry picked from commit 11bd764a75d3b97117098738d3489c4b3329738f)
2023-06-14 07:37:37 +03:00
Weblate
e7e3aac971 Update translation files [skip ci]
Updated by "Cleanup translation files" hook in Weblate.

Update translation files  [skip ci]

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Ukrainian) [skip ci]

Currently translated at 97.5% (1155 of 1184 strings)

Translated using Weblate (Norwegian Bokmål) [skip ci]

Currently translated at 22.1% (262 of 1184 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 97.8% (1158 of 1184 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1184 of 1184 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 99.5% (1179 of 1184 strings)

Update translation files  [skip ci]

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Indonesian) [skip ci]

Currently translated at 7.5% (89 of 1180 strings)

Translated using Weblate (Croatian) [skip ci]

Currently translated at 21.8% (258 of 1180 strings)

Translated using Weblate (Ukrainian) [skip ci]

Currently translated at 97.9% (1156 of 1180 strings)

Translated using Weblate (Slovak) [skip ci]

Currently translated at 21.1% (249 of 1180 strings)

Translated using Weblate (Norwegian Bokmål) [skip ci]

Currently translated at 22.2% (263 of 1180 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 98.2% (1159 of 1180 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 88.8% (1048 of 1180 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 98.8% (1166 of 1180 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.9% (1179 of 1180 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 88.2% (1041 of 1180 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 86.2% (1018 of 1180 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 88.2% (1041 of 1180 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 85.9% (1014 of 1180 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 88.2% (1041 of 1180 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 88.1% (1040 of 1180 strings)

Translated using Weblate (Swedish) [skip ci]

Currently translated at 89.0% (1051 of 1180 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 98.2% (1159 of 1180 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 98.3% (1160 of 1180 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 97.1% (1146 of 1180 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 98.4% (1162 of 1180 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 21.3% (252 of 1180 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 88.2% (1041 of 1180 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 88.3% (1042 of 1180 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 98.1% (1158 of 1180 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 91.2% (1077 of 1180 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 97.9% (1156 of 1180 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 99.6% (1176 of 1180 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 88.3% (1042 of 1180 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 89.1% (1052 of 1180 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 93.8% (1107 of 1180 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 98.2% (1159 of 1180 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 99.4% (1173 of 1180 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 99.9% (1179 of 1180 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-06-14 07:37:13 +03:00
Bogdan
d2cb36c88a Check only clients not in failure status in DownloadClientSortingCheck
(cherry picked from commit 0b872803358bac1297cc7618ea3c13a32a92b5a4)
2023-06-14 07:34:21 +03:00
bakerboy448
2fe28cb1dc Fixed: Handle checkingResumeData state form qBittorrent
(cherry picked from commit 8d8a16225ff7772ccb57784f272ca31e28bb8455)
2023-06-14 07:33:55 +03:00
Bogdan
5d65b4cae4 Fix sorting queue items by size 2023-06-14 04:46:19 +03:00
Bogdan
b0f56e2840 Update translations 2023-06-13 02:03:51 +03:00
Weblate
5593837482 Update translation files [skip ci]
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translation: Servarr/Radarr
2023-06-13 01:40:09 +03:00
Qstick
8231290c7b Update Remote Path Mapping delete modal title
(cherry picked from commit 18716a00516a971f7f2eb369b920266bea24fe08)

Closes #8675
2023-06-12 22:52:50 +03:00
Weblate
0c1b88c60a Translated using Weblate (Indonesian) [skip ci]
Currently translated at 7.3% (87 of 1179 strings)

Co-authored-by: liimee <git.taaa@fedora.email>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translation: Servarr/Radarr
2023-06-12 11:49:17 +03:00
Qstick
0b8478e4a1 Bump version to 4.6.2 2023-06-11 09:39:17 -05:00
Weblate
69e09c8687 Translated using Weblate (Russian) [skip ci]
Currently translated at 98.3% (1159 of 1179 strings)

Co-authored-by: Андрей <andryfly7@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2023-06-10 22:43:53 +03:00
Bogdan
3f2ea49023 Add style for default kind in ProgressBar 2023-06-10 03:13:47 +03:00
Bogdan
32f09633e9 Use more specific styling for kinds in ProgressBar
(cherry picked from commit dd31c913d2a974d95f3be251714ce749cfd99a72)

Fixes #8669
2023-06-10 01:45:40 +03:00
Qstick
3542b263c7 Fixed: Don't die on movie refresh when collection has been deleted from TMDB
Fixes #8664
2023-06-07 20:12:59 -05:00
Weblate
d5cc84d8c8 Translated using Weblate (Greek) [skip ci]
Currently translated at 99.5% (1174 of 1179 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 99.4% (1172 of 1179 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 98.3% (1159 of 1179 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 100.0% (1179 of 1179 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 100.0% (1179 of 1179 strings)

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>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translation: Servarr/Radarr
2023-06-07 17:57:09 +03:00
Bogdan
c0790060fb Fixed: (UI) Sort last movies with no release dates
Towards #8662
2023-06-07 06:52:12 +03:00
Bogdan
5ec7e86488 Add language names as hints in TMDbSettings
Add Romanian in TMDb Language Codes
Move DateTime.TryParse to if condition in TMDbParser
2023-06-05 22:17:32 +03:00
Qstick
b8abafd72f Bump version to 4.6.1 2023-06-04 11:43:43 -05:00
Bogdan
a471f1b44f Bump NLog to 5.2.0 2023-06-02 07:50:34 +03:00
Weblate
7fe34be789 Update translation files [skip ci]
Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 98.8% (1166 of 1179 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1179 of 1179 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 98.5% (1162 of 1179 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 99.3% (1171 of 1179 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 98.6% (1163 of 1179 strings)

Co-authored-by: Cc95459 <954591059@qq.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Jens <jensmahnke@me.com>
Co-authored-by: Thijs Waalen <contact@thijswaalen.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: foXaCe <foxace66@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-06-01 02:00:11 +03:00
Zak Saunders
471a34eabf New: Remove Rarbg Indexer due to site shutdown 2023-05-31 16:20:11 +01:00
Bogdan
4fe5e5974e Fixed: Don't log handled exceptions in API 2023-05-31 06:46:19 +03:00
Bogdan
1ca66d0b29 Revert "Fixed: Don't log handled exceptions in API"
This reverts commit 1bf3302ec2.
2023-05-31 06:45:52 +03:00
Bogdan
4ab1cb393a Add style for default kind in ProgressBar
Fixes #8641
2023-05-31 04:00:52 +03:00
ItsME6969
fa1f07987c Fixed: Parse Multi Disk DVDs & MDVDR (#8639)
* Update QualityParser.cs

Added: Support for Multi Disk DVD releases
Added: Support for MDVDR

* Update QualityParserFixture.cs

Added: Support for Multi Disk DVD releases
Added: Support for MDVDR
2023-05-30 11:52:44 -05:00
Bogdan
b5a5530cb1 Improvements to movie index sorting fields 2023-05-30 02:12:57 +03:00
Bogdan
e0448f7213 Fixed: (UI) Show release dates when enabled 2023-05-30 02:12:57 +03:00
Weblate
8eee5a3b1d Translated using Weblate (Indonesian) [skip ci]
Currently translated at 6.7% (79 of 1175 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translation: Servarr/Radarr
2023-05-29 21:08:39 +03:00
Bogdan
830f1aa10f Fix typo in IUpdateMediaInfo 2023-05-28 23:18:33 +03:00
Servarr
7666c7b1eb Automated API Docs update 2023-05-28 23:12:10 +03:00
JeWe37
0b4c12dd7b New: Option to Import via Script
(cherry picked from commit 9f1e2151206a077334a9c34a12a373b465752d87)
2023-05-28 22:49:23 +03:00
Qstick
53857083f2 Add a few more TMDB list filter languages
Fixes #8248
2023-05-28 13:58:34 -05:00
Servarr
ea9c77cf49 Automated API Docs update 2023-05-28 21:56:48 +03:00
Bogdan
43ed8d0c2b Add minimum length as const in ApiKeyValidationCheck
(cherry picked from commit b06269544cfa11015f3fb938fd8c2ef07d9cac4a)

Closes #8618
2023-05-28 21:39:57 +03:00
Bogdan
9df06b80bf Fixed: Enforce validation warnings
(cherry picked from commit 48ee1158ad4213fd0690842e2672f52d08f7ad26)

Closes #8628
2023-05-28 21:18:53 +03:00
Bogdan
713f984b26 Simplify ShouldHaveApiKey and HasErrors
(cherry picked from commit 7343616a47cd538bba4c9128d2c1094561f9b3a5)
2023-05-28 21:16:42 +03:00
Servarr
683d261a91 Automated API Docs update 2023-05-28 20:51:48 +03:00
Weblate
b33d9a9641 Translated using Weblate (Indonesian) [skip ci]
Currently translated at 6.5% (77 of 1173 strings)

Translated using Weblate (Croatian) [skip ci]

Currently translated at 21.9% (258 of 1173 strings)

Translated using Weblate (Ukrainian) [skip ci]

Currently translated at 98.5% (1156 of 1173 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 98.7% (1158 of 1173 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 89.2% (1047 of 1173 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 99.4% (1166 of 1173 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 88.7% (1041 of 1173 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 86.7% (1017 of 1173 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 88.7% (1041 of 1173 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 86.4% (1014 of 1173 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 88.7% (1041 of 1173 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 88.5% (1039 of 1173 strings)

Translated using Weblate (Swedish) [skip ci]

Currently translated at 89.5% (1051 of 1173 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 98.7% (1158 of 1173 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 98.7% (1158 of 1173 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 97.6% (1145 of 1173 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 94.9% (1114 of 1173 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 88.7% (1041 of 1173 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 88.7% (1041 of 1173 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 98.7% (1158 of 1173 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 91.7% (1076 of 1173 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 98.5% (1156 of 1173 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 98.7% (1158 of 1173 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 88.7% (1041 of 1173 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 89.5% (1051 of 1173 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 94.3% (1107 of 1173 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 98.8% (1159 of 1173 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 99.0% (1162 of 1173 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 97.4% (1143 of 1173 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 94.8% (1113 of 1173 strings)

Update translation files  [skip ci]

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Polish) [skip ci]

Currently translated at 97.3% (1142 of 1173 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Qstick <qstick@gmail.com>
Co-authored-by: Thijs Waalen <contact@thijswaalen.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: wgarbaczow <wladimir.garbaczow@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-05-28 12:41:15 -05:00
Qstick
c69cc20266 Cleanup react warnings from manage modals
(cherry picked from commit efab1b0793a7d990889a03703b9d9755c4741313)
2023-05-28 12:35:27 -05:00
Qstick
4fc1ee0aff Fix priority input for provider manage edit modal
(cherry picked from commit 593652a84a45aea7b542e682c08453f4ee88871c)
2023-05-28 12:35:27 -05:00
Qstick
1d4b6d4cad New: Bulk Manage Import Lists, Indexers, Clients
(cherry picked from commit 73ccab53d5194282de4b983354c9afa5a6d678fb)
2023-05-28 12:35:27 -05:00
ItsME6969
5baeba18cb Fixed: Correctly parse SCENE and P2P BluRay DISKS releases (#8595)
* Update QualityParserFixture.cs

Added Releases following P2P naming convention

* Update QualityParser.cs

Added support for P2P BR-DISK naming convention

* Update src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs

Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>

* Update QualityParserFixture.cs

---------

Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2023-05-28 12:26:48 -05:00
Matt Strapp
854b3045fe Fix: Use relative paths instead of absolute paths for webmanifest 2023-05-28 12:04:39 -05:00
Qstick
6b80c244bf Bump version to 4.6.0 2023-05-28 09:24:13 -05:00
Stepan Goremykin
044de922fa Use Array.Empty and fix a few multiple enumerations
(cherry picked from commit 11d91faaada0e70910c832ce405ddeed52a24172)
2023-05-28 16:43:46 +03:00
Bogdan
c987824174 Use 'var' instead of explicit type
(cherry picked from commit 12374f7f0038e5b25548f5ab3f71122410832393)
2023-05-28 16:41:52 +03:00
Bogdan
8762588ef0 Inline 'out' variable declarations
(cherry picked from commit 281add47de1d3940990156c841362125dea9cc7d)
2023-05-28 16:41:52 +03:00
Bogdan
ed68a944ea Standardize variable declaration
(cherry picked from commit 909f2ded6b75998fa8e1addd0dcf849279e7b120)
2023-05-28 16:41:52 +03:00
Bogdan
52c64080f2 Enforce rule IDE0005 on build
(cherry picked from commit 6b1e4ef81938d264a2ddc8b626b0502f799aa640)
2023-05-28 16:41:52 +03:00
bakerboy448
d878738a62 Fixed: A really important spelling mistake
(cherry picked from commit b510201b43f6bc5e6774119ebbd7b8a0d89ee487)

Closes #8604
2023-05-27 13:49:10 +03:00
Kalyan Chekuri
af496fe701 added new language "Telugu" for filtering releases and testcases. 2023-05-26 20:38:00 -05:00
Mark McDowall
bbcd0b7861 New: Filter SABnzbd queue by category
(cherry picked from commit 49fd9c4462741e756e0cd43c287939f929de11b2)
2023-05-27 03:59:12 +03:00
Mark McDowall
1bf3302ec2 Fixed: Don't log handled exceptions in API
(cherry picked from commit 59f2e5b65dd7352aad92b33adefa6cf5ca79a0de)
2023-05-27 03:58:07 +03:00
Bogdan
e55c3f7ddf Fixed: Sorting search releases by language
(cherry picked from commit af55e322f1a9505a0b63739b499d6af72c3105a0)

Closes #8597
2023-05-26 23:25:12 +03:00
Bogdan
4eb89eb851 Show movie title as tooltip on movie index 2023-05-26 09:55:12 +03:00
Weblate
71dfd897a8 Translated using Weblate (Indonesian) [skip ci]
Currently translated at 4.2% (50 of 1173 strings)

Co-authored-by: liimee <git.taaa@fedora.email>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translation: Servarr/Radarr
2023-05-25 13:47:30 +03:00
Bogdan
a1ccfacfa2 Fixed: (UI) Show studio and release/added dates in movie index 2023-05-25 01:06:06 +03:00
Servarr
29ba6fe556 Automated API Docs update 2023-05-24 10:10:07 +03:00
Mark McDowall
7b7b866777 Rename Clearart to Clearlogo, use png for Clearlogo
(cherry picked from commit 349f7cf4c934fbf53516f7e92026282750180a16)
2023-05-24 09:44:05 +03:00
Mark McDowall
371eb68bf0 Fixed: Don't download unknown artwork
(cherry picked from commit aecc8295c8c3ce39040d03a1f79f775e1c910362)
2023-05-24 09:44:05 +03:00
Bogdan
f8cb8c6bd8 Sort translations 2023-05-23 16:30:15 +03:00
Mark McDowall
488f8c71e8 Fixed: Don't rollback file move if destination already exists
Towards #5610

(cherry picked from commit f05405fe1ce4c78a8c75e27920c863c5b83686bd)
2023-05-23 12:41:27 +03:00
Bogdan
c3cdb867a8 Use project name as relative path in builds
(cherry picked from commit fb908e8e1969e633a50ca000c767a998427363b2)

(cherry picked from commit 365a6e77a6530831eeeec7580406ff736da0e17a)
2023-05-23 12:40:57 +03:00
Weblate
6163307c1c Translated using Weblate (French) [skip ci]
Currently translated at 99.8% (1171 of 1173 strings)

Co-authored-by: foXaCe <foxace66@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translation: Servarr/Radarr
2023-05-23 12:40:37 +03:00
Bogdan
e9dcef34d4 Fix spelling "Use languages from Torznab/Newznab attributes if given"
(cherry picked from commit de3bfb7c5ab03e527dca1be3ef4a664dce266db6)
2023-05-22 02:03:33 +03:00
Qstick
b7be2c1d6e New: Use languages from Torznab/Newznab attributes if given
(cherry picked from commit 9c5a07f62a6e32832c10c80813cd3b98c5859989)
2023-05-22 02:03:33 +03:00
Bogdan
933c23ce57 New: Improve validation messages
(cherry picked from commit a117001de673e80abd90d54a34a7c86292b3a649)
2023-05-22 01:14:49 +03:00
Bogdan
aa794bddab Minor CS improvements in NzbDroneValidation
(cherry picked from commit 6118afa339621509aad55caf27b05e89bd0b8c74)
2023-05-22 01:14:49 +03:00
Mark McDowall
d5605abd91 Add FindByName to IsoLanguage
(cherry picked from commit 723810014573c4bae2b8c611144bbee1a6b56237)
2023-05-22 01:14:49 +03:00
Bogdan
5ead395f9f Fix tests in TorrentDownloadStationFixture 2023-05-22 01:14:49 +03:00
Mark McDowall
0f34948c00 Fixed tests in DownloadServiceFixture
(cherry picked from commit d743a8f7e9eac348b4679919f60af5b27457acfd)
2023-05-22 01:14:49 +03:00
Mark McDowall
99d865ee4a Fixed: Don't move seeding torrents in Synology Download Station
(cherry picked from commit 3cd33d3f44097b4cb4fb291bca70a0aa53c4b844)
2023-05-22 01:14:49 +03:00
Mark McDowall
90096451e0 Fixed: Don't retry grabbing the same release if download client is unavailable
(cherry picked from commit b38c1255dc19d72ee10db4af67e76a4ce95f288f)
2023-05-22 01:14:49 +03:00
Mark McDowall
84570159ae Fixed: Exception when request to SABnzbd times out
(cherry picked from commit f946d78153b85ad726a06a1140143c8beac8766d)
2023-05-22 01:14:49 +03:00
Qstick
8d264020aa Fixed: Don't match if movie year parsed but is mismatch
Fixes #8548
2023-05-21 15:20:57 -05:00
S0me6uy
65850e6a5d New: Signal Notifications
(cherry picked from commit 59dd3b11271a63ea16f0e32a596dba8e9b9d1096)
2023-05-21 20:38:52 +03:00
Bogdan
db154ae9a4 Fix tests in IndexerServiceFixture
(cherry picked from commit 90a9ecbaac1b406ffeb6b6876d619191a317a35f)
2023-05-21 19:49:43 +03:00
Bogdan
df70d85d0a New: Show current tags for Connections
(cherry picked from commit 2016f11b1c322fcd5f9e2f09328ca19d2114629d)
2023-05-20 02:04:38 +03:00
459 changed files with 6071 additions and 2732 deletions

View File

@@ -40,12 +40,18 @@ dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
# Prefer "var" everywhere
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true
csharp_style_var_elsewhere = true
# Prefer "out" variables to be declared inline
csharp_style_inlined_variable_declaration = true
# Using directive is unnecessary.
dotnet_diagnostic.IDE0005.severity = error
# Use var instead of explicit type
dotnet_diagnostic.IDE0007.severity = error
# Inline variable declaration
dotnet_diagnostic.IDE0018.severity = error
# Stylecop Rules
dotnet_diagnostic.SA0001.severity = none

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '4.5.2'
majorVersion: '4.6.3'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
@@ -156,16 +157,16 @@ class Blocklist extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadBlocklist')}
</div>
</Alert>
}
{
isPopulated && !error && !items.length &&
<div>
{translate('NoHistory')}
</div>
<Alert kind={kinds.INFO}>
{translate('NoHistoryBlocklist')}
</Alert>
}
{
@@ -209,7 +210,7 @@ class Blocklist extends Component {
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title={translate('RemoveSelected')}
message={translate('AreYouSureYouWantToRemoveTheSelectedItemsFromBlocklist')}
message={translate('RemoveSelectedItemBlocklistMessageText')}
confirmLabel={translate('RemoveSelected')}
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
@@ -11,7 +12,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import HistoryRowConnector from './HistoryRowConnector';
@@ -83,9 +84,9 @@ class History extends Component {
{
!isFetchingAny && hasError &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadHistory')}
</div>
</Alert>
}
{
@@ -93,9 +94,9 @@ class History extends Component {
// wait for the episodes to populate because they are never coming.
isPopulated && !hasError && !items.length &&
<div>
<Alert kind={kinds.INFO}>
{translate('NoHistory')}
</div>
</Alert>
}
{

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -12,7 +13,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import { align, icons, kinds } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
@@ -231,17 +232,17 @@ class Queue extends Component {
{
!isRefreshing && hasError ?
<div>
<Alert kind={kinds.DANGER}>
{translate('FailedToLoadQueue')}
</div> :
</Alert> :
null
}
{
isAllPopulated && !hasError && !items.length ?
<div>
<Alert kind={kinds.INFO}>
{translate('QueueIsEmpty')}
</div> :
</Alert> :
null
}

View File

@@ -88,7 +88,7 @@ class RemoveQueueItemsModal extends Component {
<ModalBody>
<div className={styles.message}>
{selectedCount > 1 ? translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', selectedCount) : translate('AreYouSureYouWantToRemoveSelectedItemFromQueue')}
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
</div>
{
@@ -133,7 +133,7 @@ class RemoveQueueItemsModal extends Component {
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
Remove
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -1,9 +1,11 @@
import { reduce } from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
@@ -105,9 +107,9 @@ class ImportMovie extends Component {
{
!rootFoldersFetching && !!rootFoldersError ?
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
</div> :
</Alert> :
null
}
@@ -116,9 +118,9 @@ class ImportMovie extends Component {
!rootFoldersFetching &&
rootFoldersPopulated &&
!unmappedFolders.length ?
<div>
<Alert kind={kinds.INFO}>
{translate('AllMoviesInPathHaveBeenImported', [path])}
</div> :
</Alert> :
null
}

View File

@@ -92,9 +92,9 @@ class ImportMovieSelectFolder extends Component {
{
!isFetching && error ?
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
</div> :
</Alert> :
null
}

View File

@@ -1,14 +1,33 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
AppSectionSchemaState,
} from 'App/State/AppSectionState';
import Language from 'Language/Language';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import { UiSettings } from 'typings/UiSettings';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface NotificationAppState
extends AppSectionState<Notification>,
AppSectionDeleteState {}
export interface QualityProfilesAppState
@@ -20,6 +39,9 @@ export type UiSettingsAppState = AppSectionState<UiSettings>;
interface SettingsAppState {
downloadClients: DownloadClientAppState;
importLists: ImportListAppState;
indexers: IndexerAppState;
notifications: NotificationAppState;
language: LanguageSettingsAppState;
uiSettings: UiSettingsAppState;
qualityProfiles: QualityProfilesAppState;

View File

@@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AgendaConnector from './Agenda/AgendaConnector';
import * as calendarViews from './calendarViews';
@@ -31,9 +33,9 @@ class Calendar extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadTheCalendar')}
</div>
</Alert>
}
{

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -9,7 +10,7 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { align, icons, sortDirections } from 'Helpers/Props';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import styles from 'Movie/Index/MovieIndex.css';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate';
@@ -313,9 +314,9 @@ class Collection extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadCollections')}
</div>
</Alert>
}
{

View File

@@ -265,6 +265,8 @@ 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,

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);

View File

@@ -63,6 +63,7 @@ function ProviderFieldFormGroup(props) {
name,
label,
helpText,
helpTextWarning,
helpLink,
placeholder,
value,
@@ -96,6 +97,7 @@ function ProviderFieldFormGroup(props) {
name={name}
label={label}
helpText={helpText}
helpTextWarning={helpTextWarning}
helpLink={helpLink}
placeholder={placeholder}
value={value}
@@ -122,6 +124,7 @@ ProviderFieldFormGroup.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
helpText: PropTypes.string,
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.any,

View File

@@ -13,7 +13,7 @@ const messages = [
'Loading humorous message... Please Wait',
'I could\'ve been faster in Python',
'Don\'t forget to rewind your movies',
'Congratulations! you are the 1000th visitor.',
'Congratulations! You are the 1000th visitor.',
'HELP! I\'m being held hostage and forced to write these stupid lines!',
'RE-calibrating the internet...',
'I\'ll be here all week',

View File

@@ -19,6 +19,8 @@ function createCleanMovieSelector() {
year,
images,
alternateTitles = [],
tmdbId,
imdbId,
tags = []
} = movie;
@@ -29,6 +31,8 @@ function createCleanMovieSelector() {
year,
images,
alternateTitles,
tmdbId,
imdbId,
firstCharacter: title.charAt(0).toLowerCase(),
tags: tags.reduce((acc, id) => {
const matchingTag = allTags.find((tag) => tag.id === id);

View File

@@ -12,6 +12,8 @@ function MovieSearchResult(props) {
year,
images,
alternateTitles,
tmdbId,
imdbId,
tags
} = props;
@@ -47,6 +49,22 @@ function MovieSearchResult(props) {
null
}
{
match.key === 'tmdbId' && tmdbId ?
<div className={styles.alternateTitle}>
TmdbId: {tmdbId}
</div> :
null
}
{
match.key === 'imdbId' && imdbId ?
<div className={styles.alternateTitle}>
ImdbId: {imdbId}
</div> :
null
}
{
tag ?
<div className={styles.tagContainer}>
@@ -69,6 +87,8 @@ MovieSearchResult.propTypes = {
year: PropTypes.number.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
tmdbId: PropTypes.number,
imdbId: PropTypes.string,
tags: PropTypes.arrayOf(PropTypes.object).isRequired,
match: PropTypes.object.isRequired
};

View File

@@ -9,6 +9,8 @@ const fuseOptions = {
keys: [
'title',
'alternateTitles.title',
'tmdbId',
'imdbId',
'tags.label'
]
};

View File

@@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
function PageSectionContent(props) {
const {
@@ -17,7 +19,7 @@ function PageSectionContent(props) {
);
} else if (!isFetching && !!error) {
return (
<div>{errorMessage}</div>
<Alert kind={kinds.DANGER}>{errorMessage}</Alert>
);
} else if (isPopulated && !error) {
return (

View File

@@ -16,6 +16,46 @@
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
color: var(--white);
transition: width 0.6s ease;
&.default {
background-color: var(--darkGray);
}
&.primary {
background-color: var(--primaryColor);
}
&.danger {
background-color: var(--dangerColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color(#f05050 shade(5%)), color(#f05050 shade(5%)) 5px, color(#f05050 shade(15%)) 5px, color(#f05050 shade(15%)) 10px);
}
}
&.success {
background-color: var(--successColor);
}
&.purple {
background-color: var(--purple);
}
&.warning {
background-color: var(--warningColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, #ffa500, #ffa500 5px, color(#ffa500 tint(15%)) 5px, color(#ffa500 tint(15%)) 10px);
}
}
&.info {
background-color: var(--infoColor);
}
&.queue {
background-color: var(--queueColor);
}
}
.frontTextContainer {
@@ -45,42 +85,6 @@
cursor: default;
}
.primary {
background-color: var(--primaryColor);
}
.danger {
background-color: var(--dangerColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color(#f05050 shade(5%)), color(#f05050 shade(5%)) 5px, color(#f05050 shade(15%)) 5px, color(#f05050 shade(15%)) 10px);
}
}
.success {
background-color: var(--successColor);
}
.purple {
background-color: var(--purple);
}
.warning {
background-color: var(--warningColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, #ffa500, #ffa500 5px, color(#ffa500 tint(15%)) 5px, color(#ffa500 tint(15%)) 10px);
}
}
.info {
background-color: var(--infoColor);
}
.queue {
background-color: var(--queueColor);
}
.small {
height: $progressBarSmallHeight;

View File

@@ -5,6 +5,7 @@ interface CssExports {
'backTextContainer': string;
'container': string;
'danger': string;
'default': string;
'frontText': string;
'frontTextContainer': string;
'info': string;

View File

@@ -38,7 +38,7 @@ function ProgressBar(props) {
{
showText && width ?
<div
className={styles.backTextContainer}
className={classNames(styles.backTextContainer, styles[kind])}
style={{ width: actualWidth }}
>
<div className={styles.backText}>
@@ -67,7 +67,7 @@ function ProgressBar(props) {
{
showText ?
<div
className={styles.frontTextContainer}
className={classNames(styles.frontTextContainer, styles[kind])}
style={{ width: progressPercent }}
>
<div

View File

@@ -2,16 +2,17 @@
"name": "Radarr",
"icons": [
{
"src": "/Content/Images/Icons/android-chrome-192x192.png",
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/Content/Images/Icons/android-chrome-512x512.png",
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "../../../../",
"theme_color": "#3a3f51",
"background_color": "#3a3f51",
"display": "standalone"

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -10,7 +11,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import styles from 'Movie/Index/MovieIndex.css';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate';
@@ -369,9 +370,9 @@ class DiscoverMovie extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadMovies')}
</div>
</Alert>
}
{

View File

@@ -71,6 +71,7 @@ import {
faLanguage as fasLanguage,
faLaptop as fasLaptop,
faLevelUpAlt as fasLevelUpAlt,
faListCheck as fasListCheck,
faMedkit as fasMedkit,
faMinus as fasMinus,
faPause as fasPause,
@@ -172,6 +173,7 @@ export const INFO = fasInfoCircle;
export const INTERACTIVE = fasUser;
export const KEYBOARD = farKeyboard;
export const LOGOUT = fasSignOutAlt;
export const MANAGE = fasListCheck;
export const MEDIA_INFO = farFileInvoice;
export const MISSING = fasExclamationTriangle;
export const MONITORED = fasBookmark;

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { LanguageSettingsAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -86,7 +87,9 @@ function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<div>{translate('UnableToLoadLanguages')}</div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadLanguages')}
</Alert>
) : null}
{isPopulated && !error ? (

View File

@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { Error } from 'App/State/AppSectionState';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -130,7 +131,9 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
{isFetching && <LoadingIndicator />}
{!isFetching && error ? (
<div>{translate('UnableToLoadQualities')}</div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadQualities')}
</Alert>
) : null}
{isPopulated && !error ? (

View File

@@ -10,6 +10,7 @@ import { SelectProvider } from 'App/SelectContext';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
import { RSS_SYNC } from 'Commands/commandNames';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -20,7 +21,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import withScrollPosition from 'Components/withScrollPosition';
import { align, icons } from 'Helpers/Props';
import { align, icons, kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import NoMovie from 'Movie/NoMovie';
@@ -337,7 +338,9 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<div>{translate('UnableToLoadMovies')}</div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadMovies')}
</Alert>
) : null}
{isLoaded ? (

View File

@@ -16,6 +16,7 @@ import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import MoviePoster from 'Movie/MoviePoster';
import { executeCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import translate from 'Utilities/String/translate';
import createMovieIndexItemSelector from '../createMovieIndexItemSelector';
import MovieIndexPosterInfo from './MovieIndexPosterInfo';
@@ -41,13 +42,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
showTitle,
showMonitored,
showQualityProfile,
showCinemaRelease,
showReleaseDate,
showSearchAction,
} = useSelector(selectPosterOptions);
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
useSelector(createUISettingsSelector());
const {
title,
@@ -59,12 +60,19 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
youTubeTrailerId,
hasFile,
isAvailable,
studio,
added,
year,
inCinemas,
physicalRelease,
digitalRelease,
path,
movieFile,
ratings,
sizeOnDisk,
certification,
originalTitle,
originalLanguage,
} = movie;
const dispatch = useDispatch();
@@ -122,9 +130,23 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
height: `${posterHeight}px`,
};
let releaseDate = '';
let releaseDateType = '';
if (physicalRelease && digitalRelease) {
releaseDate =
physicalRelease < digitalRelease ? physicalRelease : digitalRelease;
releaseDateType = physicalRelease < digitalRelease ? 'Released' : 'Digital';
} else if (physicalRelease && !digitalRelease) {
releaseDate = physicalRelease;
releaseDateType = 'Released';
} else if (digitalRelease && !physicalRelease) {
releaseDate = digitalRelease;
releaseDateType = 'Digital';
}
return (
<div className={styles.content}>
<div className={styles.posterContainer}>
<div className={styles.posterContainer} title={title}>
{isSelectMode ? <MovieIndexPosterSelect movieId={movieId} /> : null}
<Label className={styles.controls}>
@@ -195,31 +217,68 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
bottomRadius={false}
/>
{showTitle ? <div className={styles.title}>{title}</div> : null}
{showTitle ? (
<div className={styles.title} title={title}>
{title}
</div>
) : null}
{showMonitored ? (
<div className={styles.title}>
{monitored ? translate('monitored') : translate('unmonitored')}
{monitored ? translate('Monitored') : translate('Unmonitored')}
</div>
) : null}
{showQualityProfile ? (
<div className={styles.title}>{qualityProfile.name}</div>
<div className={styles.title} title={translate('QualityProfile')}>
{qualityProfile.name}
</div>
) : null}
{showCinemaRelease && inCinemas ? (
<div className={styles.title} title={translate('InCinemas')}>
<Icon name={icons.IN_CINEMAS} />{' '}
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: false,
})}
</div>
) : null}
{showReleaseDate && releaseDate ? (
<div className={styles.title}>
<Icon
name={releaseDateType === 'Digital' ? icons.MOVIE_FILE : icons.DISC}
/>{' '}
{getRelativeDate(releaseDate, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: false,
})}
</div>
) : null}
<MovieIndexPosterInfo
studio={studio}
qualityProfile={qualityProfile}
added={added}
year={year}
showQualityProfile={showQualityProfile}
showCinemaRelease={showCinemaRelease}
showReleaseDate={showReleaseDate}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
inCinemas={inCinemas}
physicalRelease={physicalRelease}
digitalRelease={digitalRelease}
ratings={ratings}
sizeOnDisk={sizeOnDisk}
sortKey={sortKey}
path={path}
certification={certification}
originalTitle={originalTitle}
originalLanguage={originalLanguage}
/>
<EditMovieModalConnector

View File

@@ -3,3 +3,9 @@
text-align: center;
font-size: $smallFontSize;
}
.title {
@add-mixin truncate;
composes: info;
}

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'info': string;
'title': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,7 +1,11 @@
import React from 'react';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import TmdbRating from 'Components/TmdbRating';
import { icons } from 'Helpers/Props';
import { Language, Ratings } from 'Movie/Movie';
import QualityProfile from 'typings/QualityProfile';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
@@ -12,16 +16,22 @@ interface MovieIndexPosterInfoProps {
showQualityProfile: boolean;
qualityProfile: QualityProfile;
added?: string;
year: number;
inCinemas?: string;
digitalRelease?: string;
physicalRelease?: string;
path: string;
ratings: Ratings;
certification: string;
originalTitle: string;
originalLanguage: Language;
sizeOnDisk?: number;
sortKey: string;
showRelativeDates: boolean;
showCinemaRelease: boolean;
showReleaseDate: boolean;
shortDateFormat: string;
longDateFormat: string;
timeFormat: string;
}
@@ -31,25 +41,39 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
showQualityProfile,
qualityProfile,
added,
year,
inCinemas,
digitalRelease,
physicalRelease,
path,
ratings,
certification,
originalTitle,
originalLanguage,
sizeOnDisk,
sortKey,
showRelativeDates,
showCinemaRelease,
showReleaseDate,
shortDateFormat,
longDateFormat,
timeFormat,
} = props;
if (sortKey === 'studio' && studio) {
return <div className={styles.info}>{studio}</div>;
return (
<div className={styles.info} title={translate('Studio')}>
{studio}
</div>
);
}
if (sortKey === 'qualityProfileId' && !showQualityProfile) {
return <div className={styles.info}>{qualityProfile.name}</div>;
return (
<div className={styles.info} title={translate('QualityProfile')}>
{qualityProfile.name}
</div>
);
}
if (sortKey === 'added' && added) {
@@ -64,13 +88,24 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
);
return (
<div className={styles.info}>
<div
className={styles.info}
title={formatDateTime(added, longDateFormat, timeFormat)}
>
{translate('Added')}: {addedDate}
</div>
);
}
if (sortKey === 'inCinemas' && inCinemas && !showReleaseDate) {
if (sortKey === 'year' && year) {
return (
<div className={styles.info} title={translate('Year')}>
{year}
</div>
);
}
if (sortKey === 'inCinemas' && inCinemas && !showCinemaRelease) {
const inCinemasDate = getRelativeDate(
inCinemas,
shortDateFormat,
@@ -82,7 +117,7 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
);
return (
<div className={styles.info}>
<div className={styles.info} title={translate('InCinemas')}>
<Icon name={icons.IN_CINEMAS} /> {inCinemasDate}
</div>
);
@@ -124,18 +159,58 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
);
}
if (sortKey === 'imdbRating' && !!ratings.imdb) {
return (
<div className={styles.info}>
<ImdbRating ratings={ratings} iconSize={12} />
</div>
);
}
if (sortKey === 'tmdbRating' && !!ratings.tmdb) {
return (
<div className={styles.info}>
<TmdbRating ratings={ratings} iconSize={12} />
</div>
);
}
if (sortKey === 'path') {
return <div className={styles.info}>{path}</div>;
return (
<div className={styles.info} title={translate('Path')}>
{path}
</div>
);
}
if (sortKey === 'sizeOnDisk') {
return <div className={styles.info}>{formatBytes(sizeOnDisk)}</div>;
return (
<div className={styles.info} title={translate('SizeOnDisk')}>
{formatBytes(sizeOnDisk)}
</div>
);
}
if (sortKey === 'certification') {
return <div className={styles.info}>{certification}</div>;
}
if (sortKey === 'originalTitle' && originalTitle) {
return (
<div className={styles.title} title={originalTitle}>
{originalTitle}
</div>
);
}
if (sortKey === 'originalLanguage' && originalLanguage) {
return (
<div className={styles.info} title={translate('OriginalLanguage')}>
{originalLanguage.name}
</div>
);
}
return null;
}

View File

@@ -143,6 +143,7 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
showTitle,
showMonitored,
showQualityProfile,
showCinemaRelease,
showReleaseDate,
} = posterOptions;
@@ -167,6 +168,10 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
heights.push(19);
}
if (showCinemaRelease) {
heights.push(19);
}
if (showReleaseDate) {
heights.push(19);
}
@@ -174,8 +179,13 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
switch (sortKey) {
case 'studio':
case 'added':
case 'year':
case 'imdbRating':
case 'tmdbRating':
case 'path':
case 'sizeOnDisk':
case 'originalTitle':
case 'originalLanguage':
heights.push(19);
break;
case 'qualityProfileId':
@@ -183,6 +193,17 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
heights.push(19);
}
break;
case 'inCinemas':
if (!showCinemaRelease) {
heights.push(19);
}
break;
case 'digitalRelease':
case 'physicalRelease':
if (!showReleaseDate) {
heights.push(19);
}
break;
default:
// No need to add a height of 0
}

View File

@@ -16,6 +16,13 @@ export interface Collection {
title: string;
}
export interface Ratings {
imdb: object;
tmdb: object;
metacritic: object;
rottenTomatoes: object;
}
interface Movie extends ModelBase {
tmdbId: number;
imdbId: string;
@@ -41,7 +48,7 @@ interface Movie extends ModelBase {
path: string;
sizeOnDisk: number;
genres: string[];
ratings: object;
ratings: Ratings;
certification: string;
tags: number[];
images: Image[];

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -109,9 +110,9 @@ class FileEditModalContent extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadQualities')}
</div>
</Alert>
}
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -92,9 +93,9 @@ class SelectQualityModalContent extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadQualities')}
</div>
</Alert>
}
{

View File

@@ -1,8 +1,10 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import RootFolderRowConnector from './RootFolderRowConnector';
@@ -44,9 +46,9 @@ function RootFolders(props) {
if (!isFetching && !!error) {
return (
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
</div>
</Alert>
);
}

View File

@@ -152,13 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER}
title={translate('DeleteCustomFormat')}
message={
<div>
<div>
{translate('AreYouSureYouWantToDeleteFormat', [name])}
</div>
</div>
}
message={translate('DeleteCustomFormatMessageText', [name])}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button';
import ClipboardButton from 'Components/Link/ClipboardButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -41,9 +42,9 @@ class ExportCustomFormatModalContent extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadCustomFormats')}
</div>
</Alert>
}
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -11,7 +12,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, sizes } from 'Helpers/Props';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ImportCustomFormatModalContent.css';
@@ -95,9 +96,9 @@ class ImportCustomFormatModalContent extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadCustomFormats')}
</div>
</Alert>
}
{

View File

@@ -78,7 +78,7 @@ class Specification extends Component {
<IconButton
className={styles.cloneButton}
title={translate('CloneFormatTag')}
title={translate('CloneCondition')}
name={icons.CLONE}
onPress={this.onCloneSpecificationPress}
/>
@@ -114,14 +114,8 @@ class Specification extends Component {
<ConfirmModal
isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER}
title={translate('DeleteCustomFormat')}
message={
<div>
<div>
{translate('AreYouSureYouWantToDeleteFormat', [name])}
</div>
</div>
}
title={translate('DeleteCondition')}
message={translate('DeleteConditionMessageText', [name])}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose}

View File

@@ -8,6 +8,7 @@ import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal';
import DownloadClientOptionsConnector from './Options/DownloadClientOptionsConnector';
import RemotePathMappingsConnector from './RemotePathMappings/RemotePathMappingsConnector';
@@ -23,7 +24,8 @@ class DownloadClientSettings extends Component {
this.state = {
isSaving: false,
hasPendingChanges: false
hasPendingChanges: false,
isManageDownloadClientsOpen: false
};
}
@@ -38,6 +40,14 @@ class DownloadClientSettings extends Component {
this.setState(payload);
};
onManageDownloadClientsPress = () => {
this.setState({ isManageDownloadClientsOpen: true });
};
onManageDownloadClientsModalClose = () => {
this.setState({ isManageDownloadClientsOpen: false });
};
onSavePress = () => {
if (this._saveCallback) {
this._saveCallback();
@@ -55,7 +65,8 @@ class DownloadClientSettings extends Component {
const {
isSaving,
hasPendingChanges
hasPendingChanges,
isManageDownloadClientsOpen
} = this.state;
return (
@@ -73,6 +84,12 @@ class DownloadClientSettings extends Component {
isSpinning={isTestingAll}
onPress={dispatchTestAllDownloadClients}
/>
<PageToolbarButton
label="Manage Clients"
iconName={icons.MANAGE}
onPress={this.onManageDownloadClientsPress}
/>
</Fragment>
}
onSavePress={this.onSavePress}
@@ -87,6 +104,11 @@ class DownloadClientSettings extends Component {
/>
<RemotePathMappingsConnector />
<ManageDownloadClientsModal
isOpen={isManageDownloadClientsOpen}
onModalClose={this.onManageDownloadClientsModalClose}
/>
</PageContentBody>
</PageContent>
);

View File

@@ -0,0 +1,28 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageDownloadClientsEditModalContent from './ManageDownloadClientsEditModalContent';
interface ManageDownloadClientsEditModalProps {
isOpen: boolean;
downloadClientIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
function ManageDownloadClientsEditModal(
props: ManageDownloadClientsEditModalProps
) {
const { isOpen, downloadClientIds, onSavePress, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageDownloadClientsEditModalContent
downloadClientIds={downloadClientIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default ManageDownloadClientsEditModal;

View File

@@ -0,0 +1,16 @@
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.selected {
font-weight: bold;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
}

View File

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

View File

@@ -0,0 +1,180 @@
import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ManageDownloadClientsEditModalContent.css';
interface SavePayload {
enable?: boolean;
removeCompletedDownloads?: boolean;
removeFailedDownloads?: boolean;
priority?: number;
}
interface ManageDownloadClientsEditModalContentProps {
downloadClientIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
const NO_CHANGE = 'noChange';
const enableOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true },
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabled', value: 'Disabled' },
];
function ManageDownloadClientsEditModalContent(
props: ManageDownloadClientsEditModalContentProps
) {
const { downloadClientIds, onSavePress, onModalClose } = props;
const [enable, setEnable] = useState(NO_CHANGE);
const [removeCompletedDownloads, setRemoveCompletedDownloads] =
useState(NO_CHANGE);
const [removeFailedDownloads, setRemoveFailedDownloads] = useState(NO_CHANGE);
const [priority, setPriority] = useState<null | string | number>(null);
const save = useCallback(() => {
let hasChanges = false;
const payload: SavePayload = {};
if (enable !== NO_CHANGE) {
hasChanges = true;
payload.enable = enable === 'enabled';
}
if (removeCompletedDownloads !== NO_CHANGE) {
hasChanges = true;
payload.removeCompletedDownloads = removeCompletedDownloads === 'enabled';
}
if (removeFailedDownloads !== NO_CHANGE) {
hasChanges = true;
payload.removeFailedDownloads = removeFailedDownloads === 'enabled';
}
if (priority !== null) {
hasChanges = true;
payload.priority = priority as number;
}
if (hasChanges) {
onSavePress(payload);
}
onModalClose();
}, [
enable,
priority,
removeCompletedDownloads,
removeFailedDownloads,
onSavePress,
onModalClose,
]);
const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'enable':
setEnable(value);
break;
case 'priority':
setPriority(value);
break;
case 'removeCompletedDownloads':
setRemoveCompletedDownloads(value);
break;
case 'removeFailedDownloads':
setRemoveFailedDownloads(value);
break;
default:
console.warn('EditDownloadClientsModalContent Unknown Input');
}
},
[]
);
const selectedCount = downloadClientIds.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('EditSelectedDownloadClients')}</ModalHeader>
<ModalBody>
<FormGroup>
<FormLabel>{translate('Enabled')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="enable"
value={enable}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Priority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
value={priority}
min={1}
max={50}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RemoveCompletedDownloads')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="removeCompletedDownloads"
value={removeCompletedDownloads}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RemoveFailedDownloads')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="removeFailedDownloads"
value={removeFailedDownloads}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('{count} download clients selected', {
count: selectedCount,
})}
</div>
<div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button onPress={save}>{translate('Apply Changes')}</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
export default ManageDownloadClientsEditModalContent;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageDownloadClientsModalContent from './ManageDownloadClientsModalContent';
interface ManageDownloadClientsModalProps {
isOpen: boolean;
onModalClose(): void;
}
function ManageDownloadClientsModal(props: ManageDownloadClientsModalProps) {
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageDownloadClientsModalContent onModalClose={onModalClose} />
</Modal>
);
}
export default ManageDownloadClientsModal;

View File

@@ -0,0 +1,16 @@
.leftButtons,
.rightButtons {
display: flex;
flex: 1 0 50%;
flex-wrap: wrap;
}
.rightButtons {
justify-content: flex-end;
}
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}

View File

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

View File

@@ -0,0 +1,241 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { DownloadClientAppState } from 'App/State/SettingsAppState';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import {
bulkDeleteDownloadClients,
bulkEditDownloadClients,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import ManageDownloadClientsEditModal from './Edit/ManageDownloadClientsEditModal';
import ManageDownloadClientsModalRow from './ManageDownloadClientsModalRow';
import styles from './ManageDownloadClientsModalContent.css';
// TODO: This feels janky to do, but not sure of a better way currently
type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageDownloadClientsModalRow
>['onSelectedChange'];
const COLUMNS = [
{
name: 'name',
label: 'Name',
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: 'Implementation',
isSortable: true,
isVisible: true,
},
{
name: 'enable',
label: 'Enabled',
isSortable: true,
isVisible: true,
},
{
name: 'priority',
label: 'Priority',
isSortable: true,
isVisible: true,
},
{
name: 'removeCompletedDownloads',
label: 'Remove Completed',
isSortable: true,
isVisible: true,
},
{
name: 'removeFailedDownloads',
label: 'Remove Failed',
isSortable: true,
isVisible: true,
},
];
interface ManageDownloadClientsModalContentProps {
onModalClose(): void;
}
function ManageDownloadClientsModalContent(
props: ManageDownloadClientsModalContentProps
) {
const { onModalClose } = props;
const {
isFetching,
isPopulated,
isDeleting,
isSaving,
error,
items,
}: DownloadClientAppState = useSelector(
createClientSideCollectionSelector('settings.downloadClients')
);
const dispatch = useDispatch();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const selectedCount = selectedIds.length;
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
const onDeleteModalClose = useCallback(() => {
setIsDeleteModalOpen(false);
}, [setIsDeleteModalOpen]);
const onEditPress = useCallback(() => {
setIsEditModalOpen(true);
}, [setIsEditModalOpen]);
const onEditModalClose = useCallback(() => {
setIsEditModalOpen(false);
}, [setIsEditModalOpen]);
const onConfirmDelete = useCallback(() => {
dispatch(bulkDeleteDownloadClients({ ids: selectedIds }));
setIsDeleteModalOpen(false);
}, [selectedIds, dispatch]);
const onSavePress = useCallback(
(payload: object) => {
setIsEditModalOpen(false);
dispatch(
bulkEditDownloadClients({
ids: selectedIds,
...payload,
})
);
},
[selectedIds, dispatch]
);
const onSelectAllChange = useCallback(
({ value }: SelectStateInputProps) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
({ id, value, shiftKey = false }) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const errorMessage = getErrorMessage(error, 'Unable to load import lists.');
const anySelected = selectedCount > 0;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Manage Import Lists</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{error ? <div>{errorMessage}</div> : null}
{isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table
columns={COLUMNS}
horizontalScroll={true}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
>
<TableBody>
{items.map((item) => {
return (
<ManageDownloadClientsModalRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
columns={COLUMNS}
onSelectedChange={onSelectedChange}
/>
);
})}
</TableBody>
</Table>
) : null}
</ModalBody>
<ModalFooter>
<div className={styles.leftButtons}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!anySelected}
onPress={onDeletePress}
>
Delete
</SpinnerButton>
<SpinnerButton
isSpinning={isSaving}
isDisabled={!anySelected}
onPress={onEditPress}
>
Edit
</SpinnerButton>
</div>
<Button onPress={onModalClose}>Close</Button>
</ModalFooter>
<ManageDownloadClientsEditModal
isOpen={isEditModalOpen}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
downloadClientIds={selectedIds}
/>
<ConfirmModal
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title="Delete Download Clients(s)"
message={`Are you sure you want to delete ${selectedIds.length} download clients(s)?`}
confirmLabel="Delete"
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
/>
</ModalContent>
);
}
export default ManageDownloadClientsModalContent;

View File

@@ -0,0 +1,11 @@
.name,
.enable,
.tags,
.priority,
.removeCompletedDownloads,
.removeFailedDownloads,
.implementation {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all;
}

View File

@@ -0,0 +1,13 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'enable': string;
'implementation': string;
'name': string;
'priority': string;
'removeCompletedDownloads': string;
'removeFailedDownloads': string;
'tags': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,77 @@
import React, { useCallback } from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import { SelectStateInputProps } from 'typings/props';
import styles from './ManageDownloadClientsModalRow.css';
interface ManageDownloadClientsModalRowProps {
id: number;
name: string;
enable: boolean;
priority: number;
removeCompletedDownloads: boolean;
removeFailedDownloads: boolean;
implementation: string;
columns: Column[];
isSelected?: boolean;
onSelectedChange(result: SelectStateInputProps): void;
}
function ManageDownloadClientsModalRow(
props: ManageDownloadClientsModalRowProps
) {
const {
id,
isSelected,
name,
enable,
priority,
removeCompletedDownloads,
removeFailedDownloads,
implementation,
onSelectedChange,
} = props;
const onSelectedChangeWrapper = useCallback(
(result: SelectStateInputProps) => {
onSelectedChange({
...result,
});
},
[onSelectedChange]
);
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChangeWrapper}
/>
<TableRowCell className={styles.name}>{name}</TableRowCell>
<TableRowCell className={styles.implementation}>
{implementation}
</TableRowCell>
<TableRowCell className={styles.enable}>
{enable ? 'Yes' : 'No'}
</TableRowCell>
<TableRowCell className={styles.priority}>{priority}</TableRowCell>
<TableRowCell className={styles.removeCompletedDownloads}>
{removeCompletedDownloads ? 'Yes' : 'No'}
</TableRowCell>
<TableRowCell className={styles.removeFailedDownloads}>
{removeFailedDownloads ? 'Yes' : 'No'}
</TableRowCell>
</TableRow>
);
}
export default ManageDownloadClientsModalRow;

View File

@@ -29,9 +29,9 @@ function DownloadClientOptions(props) {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadDownloadClientOptions')}
</div>
</Alert>
}
{

View File

@@ -88,8 +88,8 @@ class RemotePathMapping extends Component {
<ConfirmModal
isOpen={this.state.isDeleteRemotePathMappingModalOpen}
kind={kinds.DANGER}
title={translate('DeleteDelayProfile')}
message={translate('AreYouSureYouWantToDeleteThisRemotePathMapping')}
title={translate('DeleteRemotePathMapping')}
message={translate('DeleteRemotePathMappingMessageText')}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteRemotePathMapping}
onCancel={this.onDeleteRemotePathMappingModalClose}

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -123,9 +124,9 @@ class GeneralSettings extends Component {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadGeneralSettings')}
</div>
</Alert>
}
{

View File

@@ -168,7 +168,7 @@ class SecuritySettings extends Component {
isOpen={this.state.isConfirmApiKeyResetModalOpen}
kind={kinds.DANGER}
title={translate('ResetAPIKey')}
message={translate('AreYouSureYouWantToResetYourAPIKey')}
message={translate('ResetAPIKeyMessageText')}
confirmLabel={translate('Reset')}
onConfirm={this.onConfirmResetApiKey}
onCancel={this.onCloseResetApiKeyModal}

View File

@@ -89,7 +89,7 @@ class ImportListExclusion extends Component {
isOpen={this.state.isDeleteImportExclusionModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportListExclusion')}
message={translate('AreYouSureYouWantToDeleteThisImportListExclusion')}
message={translate('DeleteImportListExclusionMessageText')}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteImportExclusion}
onCancel={this.onDeleteImportExclusionModalClose}

View File

@@ -9,6 +9,7 @@ import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import ImportListExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector';
import ImportListsConnector from './ImportLists/ImportListsConnector';
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
import ImportListOptionsConnector from './Options/ImportListOptionsConnector';
class ImportListSettings extends Component {
@@ -23,7 +24,8 @@ class ImportListSettings extends Component {
this.state = {
isSaving: false,
hasPendingChanges: false
hasPendingChanges: false,
isManageImportListsOpen: false
};
}
@@ -38,6 +40,14 @@ class ImportListSettings extends Component {
this.setState(payload);
};
onManageImportListsPress = () => {
this.setState({ isManageImportListsOpen: true });
};
onManageImportListsModalClose = () => {
this.setState({ isManageImportListsOpen: false });
};
onSavePress = () => {
if (this._saveCallback) {
this._saveCallback();
@@ -55,7 +65,8 @@ class ImportListSettings extends Component {
const {
isSaving,
hasPendingChanges
hasPendingChanges,
isManageImportListsOpen
} = this.state;
return (
@@ -73,6 +84,12 @@ class ImportListSettings extends Component {
isSpinning={isTestingAll}
onPress={dispatchTestAllImportList}
/>
<PageToolbarButton
label="Manage Lists"
iconName={icons.MANAGE}
onPress={this.onManageImportListsPress}
/>
</Fragment>
}
onSavePress={this.onSavePress}
@@ -88,6 +105,11 @@ class ImportListSettings extends Component {
<ImportListExclusionsConnector />
<ManageImportListsModal
isOpen={isManageImportListsOpen}
onModalClose={this.onManageImportListsModalClose}
/>
</PageContentBody>
</PageContent>
);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageImportListsEditModalContent from './ManageImportListsEditModalContent';
interface ManageImportListsEditModalProps {
isOpen: boolean;
importListIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
function ManageImportListsEditModal(props: ManageImportListsEditModalProps) {
const { isOpen, importListIds, onSavePress, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageImportListsEditModalContent
importListIds={importListIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default ManageImportListsEditModal;

View File

@@ -0,0 +1,16 @@
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.selected {
font-weight: bold;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
}

View File

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

View File

@@ -0,0 +1,152 @@
import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ManageImportListsEditModalContent.css';
interface SavePayload {
enableAuto?: boolean;
qualityProfileId?: number;
rootFolderPath?: string;
}
interface ManageImportListsEditModalContentProps {
importListIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
const NO_CHANGE = 'noChange';
const autoAddOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true },
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabled', value: 'Disabled' },
];
function ManageImportListsEditModalContent(
props: ManageImportListsEditModalContentProps
) {
const { importListIds, onSavePress, onModalClose } = props;
const [enableAuto, setenableAuto] = useState(NO_CHANGE);
const [qualityProfileId, setQualityProfileId] = useState<string | number>(
NO_CHANGE
);
const [rootFolderPath, setRootFolderPath] = useState(NO_CHANGE);
const save = useCallback(() => {
let hasChanges = false;
const payload: SavePayload = {};
if (enableAuto !== NO_CHANGE) {
hasChanges = true;
payload.enableAuto = enableAuto === 'enabled';
}
if (qualityProfileId !== NO_CHANGE) {
hasChanges = true;
payload.qualityProfileId = qualityProfileId as number;
}
if (rootFolderPath !== NO_CHANGE) {
hasChanges = true;
payload.rootFolderPath = rootFolderPath;
}
if (hasChanges) {
onSavePress(payload);
}
onModalClose();
}, [enableAuto, qualityProfileId, rootFolderPath, onSavePress, onModalClose]);
const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'enableAuto':
setenableAuto(value);
break;
case 'qualityProfileId':
setQualityProfileId(value);
break;
case 'rootFolderPath':
setRootFolderPath(value);
break;
default:
console.warn('EditImportListModalContent Unknown Input');
}
},
[]
);
const selectedCount = importListIds.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('EditSelectedImportLists')}</ModalHeader>
<ModalBody>
<FormGroup>
<FormLabel>{translate('AutomaticAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="enableAuto"
value={enableAuto}
values={autoAddOptions}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
includeNoChangeDisabled={false}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RootFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
includeNoChangeDisabled={false}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={onInputChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('{count} import lists selected', { count: selectedCount })}
</div>
<div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button onPress={save}>{translate('ApplyChanges')}</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
export default ManageImportListsEditModalContent;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageImportListsModalContent from './ManageImportListsModalContent';
interface ManageImportListsModalProps {
isOpen: boolean;
onModalClose(): void;
}
function ManageImportListsModal(props: ManageImportListsModalProps) {
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageImportListsModalContent onModalClose={onModalClose} />
</Modal>
);
}
export default ManageImportListsModal;

View File

@@ -0,0 +1,16 @@
.leftButtons,
.rightButtons {
display: flex;
flex: 1 0 50%;
flex-wrap: wrap;
}
.rightButtons {
justify-content: flex-end;
}
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}

View File

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

View File

@@ -0,0 +1,283 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ImportListAppState } from 'App/State/SettingsAppState';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import {
bulkDeleteImportLists,
bulkEditImportLists,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import ManageImportListsEditModal from './Edit/ManageImportListsEditModal';
import ManageImportListsModalRow from './ManageImportListsModalRow';
import TagsModal from './Tags/TagsModal';
import styles from './ManageImportListsModalContent.css';
// TODO: This feels janky to do, but not sure of a better way currently
type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageImportListsModalRow
>['onSelectedChange'];
const COLUMNS = [
{
name: 'name',
label: 'Name',
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: 'Implementation',
isSortable: true,
isVisible: true,
},
{
name: 'qualityProfileId',
label: 'Quality Profile',
isSortable: true,
isVisible: true,
},
{
name: 'rootFolderPath',
label: 'Root Folder',
isSortable: true,
isVisible: true,
},
{
name: 'enableAuto',
label: 'Auto Add',
isSortable: true,
isVisible: true,
},
{
name: 'tags',
label: 'Tags',
isSortable: true,
isVisible: true,
},
];
interface ManageImportListsModalContentProps {
onModalClose(): void;
}
function ManageImportListsModalContent(
props: ManageImportListsModalContentProps
) {
const { onModalClose } = props;
const {
isFetching,
isPopulated,
isDeleting,
isSaving,
error,
items,
}: ImportListAppState = useSelector(
createClientSideCollectionSelector('settings.importLists')
);
const dispatch = useDispatch();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false);
const [isSavingTags, setIsSavingTags] = useState(false);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const selectedCount = selectedIds.length;
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
const onDeleteModalClose = useCallback(() => {
setIsDeleteModalOpen(false);
}, [setIsDeleteModalOpen]);
const onEditPress = useCallback(() => {
setIsEditModalOpen(true);
}, [setIsEditModalOpen]);
const onEditModalClose = useCallback(() => {
setIsEditModalOpen(false);
}, [setIsEditModalOpen]);
const onConfirmDelete = useCallback(() => {
dispatch(bulkDeleteImportLists({ ids: selectedIds }));
setIsDeleteModalOpen(false);
}, [selectedIds, dispatch]);
const onSavePress = useCallback(
(payload: object) => {
setIsEditModalOpen(false);
dispatch(
bulkEditImportLists({
ids: selectedIds,
...payload,
})
);
},
[selectedIds, dispatch]
);
const onTagsPress = useCallback(() => {
setIsTagsModalOpen(true);
}, [setIsTagsModalOpen]);
const onTagsModalClose = useCallback(() => {
setIsTagsModalOpen(false);
}, [setIsTagsModalOpen]);
const onApplyTagsPress = useCallback(
(tags: number[], applyTags: string) => {
setIsSavingTags(true);
setIsTagsModalOpen(false);
dispatch(
bulkEditImportLists({
ids: selectedIds,
tags,
applyTags,
})
);
},
[selectedIds, dispatch]
);
const onSelectAllChange = useCallback(
({ value }: SelectStateInputProps) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
({ id, value, shiftKey = false }) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const errorMessage = getErrorMessage(error, 'Unable to load import lists.');
const anySelected = selectedCount > 0;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Manage Import Lists</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{error ? <div>{errorMessage}</div> : null}
{isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table
columns={COLUMNS}
horizontalScroll={true}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
>
<TableBody>
{items.map((item) => {
return (
<ManageImportListsModalRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
columns={COLUMNS}
onSelectedChange={onSelectedChange}
/>
);
})}
</TableBody>
</Table>
) : null}
</ModalBody>
<ModalFooter>
<div className={styles.leftButtons}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!anySelected}
onPress={onDeletePress}
>
Delete
</SpinnerButton>
<SpinnerButton
isSpinning={isSaving}
isDisabled={!anySelected}
onPress={onEditPress}
>
Edit
</SpinnerButton>
<SpinnerButton
isSpinning={isSaving && isSavingTags}
isDisabled={!anySelected}
onPress={onTagsPress}
>
Set Tags
</SpinnerButton>
</div>
<Button onPress={onModalClose}>Close</Button>
</ModalFooter>
<ManageImportListsEditModal
isOpen={isEditModalOpen}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
importListIds={selectedIds}
/>
<TagsModal
isOpen={isTagsModalOpen}
ids={selectedIds}
onApplyTagsPress={onApplyTagsPress}
onModalClose={onTagsModalClose}
/>
<ConfirmModal
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title="Delete Import List(s)"
message={`Are you sure you want to delete ${selectedIds.length} import list(s)?`}
confirmLabel="Delete"
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
/>
</ModalContent>
);
}
export default ManageImportListsModalContent;

View File

@@ -0,0 +1,10 @@
.name,
.tags,
.enableAuto,
.qualityProfileId,
.rootFolderPath,
.implementation {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all;
}

View File

@@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'enableAuto': string;
'implementation': string;
'name': string;
'qualityProfileId': string;
'rootFolderPath': string;
'tags': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,84 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import TagListConnector from 'Components/TagListConnector';
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
import { SelectStateInputProps } from 'typings/props';
import styles from './ManageImportListsModalRow.css';
interface ManageImportListsModalRowProps {
id: number;
name: string;
rootFolderPath: string;
qualityProfileId: number;
implementation: string;
tags: number[];
enableAuto: boolean;
columns: Column[];
isSelected?: boolean;
onSelectedChange(result: SelectStateInputProps): void;
}
function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
const {
id,
isSelected,
name,
rootFolderPath,
qualityProfileId,
implementation,
enableAuto,
tags,
onSelectedChange,
} = props;
const qualityProfile = useSelector(
createQualityProfileSelectorForHook(qualityProfileId)
);
const onSelectedChangeWrapper = useCallback(
(result: SelectStateInputProps) => {
onSelectedChange({
...result,
});
},
[onSelectedChange]
);
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChangeWrapper}
/>
<TableRowCell className={styles.name}>{name}</TableRowCell>
<TableRowCell className={styles.implementation}>
{implementation}
</TableRowCell>
<TableRowCell className={styles.qualityProfileId}>
{qualityProfile?.name ?? 'None'}
</TableRowCell>
<TableRowCell className={styles.rootFolderPath}>
{rootFolderPath}
</TableRowCell>
<TableRowCell className={styles.enableAuto}>
{enableAuto ? 'Yes' : 'No'}
</TableRowCell>
<TableRowCell className={styles.tags}>
<TagListConnector tags={tags} />
</TableRowCell>
</TableRow>
);
}
export default ManageImportListsModalRow;

View File

@@ -0,0 +1,22 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import TagsModalContent from './TagsModalContent';
interface TagsModalProps {
isOpen: boolean;
ids: number[];
onApplyTagsPress: (tags: number[], applyTags: string) => void;
onModalClose: () => void;
}
function TagsModal(props: TagsModalProps) {
const { isOpen, onModalClose, ...otherProps } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<TagsModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}
export default TagsModal;

View File

@@ -0,0 +1,12 @@
.renameIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.result {
padding-top: 4px;
}

View File

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

View File

@@ -0,0 +1,178 @@
import { uniq } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import { ImportListAppState } from 'App/State/SettingsAppState';
import { Tag } from 'App/State/TagsAppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import ImportList from 'typings/ImportList';
import styles from './TagsModalContent.css';
interface TagsModalContentProps {
ids: number[];
onApplyTagsPress: (tags: number[], applyTags: string) => void;
onModalClose: () => void;
}
function TagsModalContent(props: TagsModalContentProps) {
const { ids, onModalClose, onApplyTagsPress } = props;
const allImportLists: ImportListAppState = useSelector(
(state: AppState) => state.settings.importLists
);
const tagList: Tag[] = useSelector(createTagsSelector());
const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add');
const seriesTags = useMemo(() => {
const tags = ids.reduce((acc: number[], id) => {
const s = allImportLists.items.find((s: ImportList) => s.id === id);
if (s) {
acc.push(...s.tags);
}
return acc;
}, []);
return uniq(tags);
}, [ids, allImportLists]);
const onTagsChange = useCallback(
({ value }: { value: number[] }) => {
setTags(value);
},
[setTags]
);
const onApplyTagsChange = useCallback(
({ value }: { value: string }) => {
setApplyTags(value);
},
[setApplyTags]
);
const onApplyPress = useCallback(() => {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: 'Add' },
{ key: 'remove', value: 'Remove' },
{ key: 'replace', value: 'Replace' },
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Tags</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>Tags</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
value={tags}
onChange={onTagsChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Apply Tags</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="applyTags"
value={applyTags}
values={applyTagsOptions}
helpTexts={[
'How to apply tags to the selected list',
'Add: Add the tags the existing list of tags',
'Remove: Remove the entered tags',
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
]}
onChange={onApplyTagsChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Result</FormLabel>
<div className={styles.result}>
{seriesTags.map((id) => {
const tag = tagList.find((t) => t.id === id);
if (!tag) {
return null;
}
const removeTag =
(applyTags === 'remove' && tags.indexOf(id) > -1) ||
(applyTags === 'replace' && tags.indexOf(id) === -1);
return (
<Label
key={tag.id}
title={removeTag ? 'Removing tag' : 'Existing tag'}
kind={removeTag ? kinds.INVERSE : kinds.INFO}
size={sizes.LARGE}
>
{tag.label}
</Label>
);
})}
{(applyTags === 'add' || applyTags === 'replace') &&
tags.map((id) => {
const tag = tagList.find((t) => t.id === id);
if (!tag) {
return null;
}
if (seriesTags.indexOf(id) > -1) {
return null;
}
return (
<Label
key={tag.id}
title={'Adding tag'}
kind={kinds.SUCCESS}
size={sizes.LARGE}
>
{tag.label}
</Label>
);
})}
</div>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
Apply
</Button>
</ModalFooter>
</ModalContent>
);
}
export default TagsModalContent;

View File

@@ -1,12 +1,13 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes } from 'Helpers/Props';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function ImportListOptions(props) {
@@ -37,9 +38,9 @@ function ImportListOptions(props) {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadListOptions')}
</div>
</Alert>
}
{

View File

@@ -8,6 +8,7 @@ import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import IndexersConnector from './Indexers/IndexersConnector';
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
import IndexerOptionsConnector from './Options/IndexerOptionsConnector';
import RestrictionsConnector from './Restrictions/RestrictionsConnector';
@@ -23,7 +24,8 @@ class IndexerSettings extends Component {
this.state = {
isSaving: false,
hasPendingChanges: false
hasPendingChanges: false,
isManageIndexersOpen: false
};
}
@@ -38,6 +40,14 @@ class IndexerSettings extends Component {
this.setState(payload);
};
onManageIndexersPress = () => {
this.setState({ isManageIndexersOpen: true });
};
onManageIndexersModalClose = () => {
this.setState({ isManageIndexersOpen: false });
};
onSavePress = () => {
if (this._saveCallback) {
this._saveCallback();
@@ -55,7 +65,8 @@ class IndexerSettings extends Component {
const {
isSaving,
hasPendingChanges
hasPendingChanges,
isManageIndexersOpen
} = this.state;
return (
@@ -73,6 +84,12 @@ class IndexerSettings extends Component {
isSpinning={isTestingAll}
onPress={dispatchTestAllIndexers}
/>
<PageToolbarButton
label="Manage Indexers"
iconName={icons.MANAGE}
onPress={this.onManageIndexersPress}
/>
</Fragment>
}
onSavePress={this.onSavePress}
@@ -87,6 +104,11 @@ class IndexerSettings extends Component {
/>
<RestrictionsConnector />
<ManageIndexersModal
isOpen={isManageIndexersOpen}
onModalClose={this.onManageIndexersModalClose}
/>
</PageContentBody>
</PageContent>
);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageIndexersEditModalContent from './ManageIndexersEditModalContent';
interface ManageIndexersEditModalProps {
isOpen: boolean;
indexerIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
function ManageIndexersEditModal(props: ManageIndexersEditModalProps) {
const { isOpen, indexerIds, onSavePress, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageIndexersEditModalContent
indexerIds={indexerIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default ManageIndexersEditModal;

View File

@@ -0,0 +1,16 @@
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.selected {
font-weight: bold;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
}

View File

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

View File

@@ -0,0 +1,178 @@
import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ManageIndexersEditModalContent.css';
interface SavePayload {
enableRss?: boolean;
enableAutomaticSearch?: boolean;
enableInteractiveSearch?: boolean;
priority?: number;
}
interface ManageIndexersEditModalContentProps {
indexerIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
const NO_CHANGE = 'noChange';
const enableOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true },
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabled', value: 'Disabled' },
];
function ManageIndexersEditModalContent(
props: ManageIndexersEditModalContentProps
) {
const { indexerIds, onSavePress, onModalClose } = props;
const [enableRss, setEnableRss] = useState(NO_CHANGE);
const [enableAutomaticSearch, setEnableAutomaticSearch] = useState(NO_CHANGE);
const [enableInteractiveSearch, setEnableInteractiveSearch] =
useState(NO_CHANGE);
const [priority, setPriority] = useState<null | string | number>(null);
const save = useCallback(() => {
let hasChanges = false;
const payload: SavePayload = {};
if (enableRss !== NO_CHANGE) {
hasChanges = true;
payload.enableRss = enableRss === 'enabled';
}
if (enableAutomaticSearch !== NO_CHANGE) {
hasChanges = true;
payload.enableAutomaticSearch = enableAutomaticSearch === 'enabled';
}
if (enableInteractiveSearch !== NO_CHANGE) {
hasChanges = true;
payload.enableInteractiveSearch = enableInteractiveSearch === 'enabled';
}
if (priority !== null) {
hasChanges = true;
payload.priority = priority as number;
}
if (hasChanges) {
onSavePress(payload);
}
onModalClose();
}, [
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
priority,
onSavePress,
onModalClose,
]);
const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'enableRss':
setEnableRss(value);
break;
case 'enableAutomaticSearch':
setEnableAutomaticSearch(value);
break;
case 'enableInteractiveSearch':
setEnableInteractiveSearch(value);
break;
case 'priority':
setPriority(value);
break;
default:
console.warn('EditIndexersModalContent Unknown Input');
}
},
[]
);
const selectedCount = indexerIds.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('EditSelectedIndexers')}</ModalHeader>
<ModalBody>
<FormGroup>
<FormLabel>{translate('EnableRss')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="enableRss"
value={enableRss}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="enableAutomaticSearch"
value={enableAutomaticSearch}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="enableInteractiveSearch"
value={enableInteractiveSearch}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Priority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
value={priority}
min={1}
max={50}
onChange={onInputChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('{count} indexers selected', { count: selectedCount })}
</div>
<div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button onPress={save}>{translate('Apply Changes')}</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
export default ManageIndexersEditModalContent;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageIndexersModalContent from './ManageIndexersModalContent';
interface ManageIndexersModalProps {
isOpen: boolean;
onModalClose(): void;
}
function ManageIndexersModal(props: ManageIndexersModalProps) {
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageIndexersModalContent onModalClose={onModalClose} />
</Modal>
);
}
export default ManageIndexersModal;

View File

@@ -0,0 +1,16 @@
.leftButtons,
.rightButtons {
display: flex;
flex: 1 0 50%;
flex-wrap: wrap;
}
.rightButtons {
justify-content: flex-end;
}
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}

View File

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

View File

@@ -0,0 +1,287 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IndexerAppState } from 'App/State/SettingsAppState';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import {
bulkDeleteIndexers,
bulkEditIndexers,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import ManageIndexersEditModal from './Edit/ManageIndexersEditModal';
import ManageIndexersModalRow from './ManageIndexersModalRow';
import TagsModal from './Tags/TagsModal';
import styles from './ManageIndexersModalContent.css';
// TODO: This feels janky to do, but not sure of a better way currently
type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageIndexersModalRow
>['onSelectedChange'];
const COLUMNS = [
{
name: 'name',
label: 'Name',
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: 'Implementation',
isSortable: true,
isVisible: true,
},
{
name: 'enableRss',
label: 'Enable RSS',
isSortable: true,
isVisible: true,
},
{
name: 'enableAutomaticSearch',
label: 'Enable Automatic Search',
isSortable: true,
isVisible: true,
},
{
name: 'enableInteractiveSearch',
label: 'Enable Interactive Search',
isSortable: true,
isVisible: true,
},
{
name: 'priority',
label: 'Priority',
isSortable: true,
isVisible: true,
},
{
name: 'tags',
label: 'Tags',
isSortable: true,
isVisible: true,
},
];
interface ManageIndexersModalContentProps {
onModalClose(): void;
}
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
const { onModalClose } = props;
const {
isFetching,
isPopulated,
isDeleting,
isSaving,
error,
items,
}: IndexerAppState = useSelector(
createClientSideCollectionSelector('settings.indexers')
);
const dispatch = useDispatch();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false);
const [isSavingTags, setIsSavingTags] = useState(false);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const selectedCount = selectedIds.length;
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
const onDeleteModalClose = useCallback(() => {
setIsDeleteModalOpen(false);
}, [setIsDeleteModalOpen]);
const onEditPress = useCallback(() => {
setIsEditModalOpen(true);
}, [setIsEditModalOpen]);
const onEditModalClose = useCallback(() => {
setIsEditModalOpen(false);
}, [setIsEditModalOpen]);
const onConfirmDelete = useCallback(() => {
dispatch(bulkDeleteIndexers({ ids: selectedIds }));
setIsDeleteModalOpen(false);
}, [selectedIds, dispatch]);
const onSavePress = useCallback(
(payload: object) => {
setIsEditModalOpen(false);
dispatch(
bulkEditIndexers({
ids: selectedIds,
...payload,
})
);
},
[selectedIds, dispatch]
);
const onTagsPress = useCallback(() => {
setIsTagsModalOpen(true);
}, [setIsTagsModalOpen]);
const onTagsModalClose = useCallback(() => {
setIsTagsModalOpen(false);
}, [setIsTagsModalOpen]);
const onApplyTagsPress = useCallback(
(tags: number[], applyTags: string) => {
setIsSavingTags(true);
setIsTagsModalOpen(false);
dispatch(
bulkEditIndexers({
ids: selectedIds,
tags,
applyTags,
})
);
},
[selectedIds, dispatch]
);
const onSelectAllChange = useCallback(
({ value }: SelectStateInputProps) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
({ id, value, shiftKey = false }) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const errorMessage = getErrorMessage(error, 'Unable to load import lists.');
const anySelected = selectedCount > 0;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Manage Import Lists</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{error ? <div>{errorMessage}</div> : null}
{isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table
columns={COLUMNS}
horizontalScroll={true}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
>
<TableBody>
{items.map((item) => {
return (
<ManageIndexersModalRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
columns={COLUMNS}
onSelectedChange={onSelectedChange}
/>
);
})}
</TableBody>
</Table>
) : null}
</ModalBody>
<ModalFooter>
<div className={styles.leftButtons}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!anySelected}
onPress={onDeletePress}
>
Delete
</SpinnerButton>
<SpinnerButton
isSpinning={isSaving}
isDisabled={!anySelected}
onPress={onEditPress}
>
Edit
</SpinnerButton>
<SpinnerButton
isSpinning={isSaving && isSavingTags}
isDisabled={!anySelected}
onPress={onTagsPress}
>
Set Tags
</SpinnerButton>
</div>
<Button onPress={onModalClose}>Close</Button>
</ModalFooter>
<ManageIndexersEditModal
isOpen={isEditModalOpen}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
indexerIds={selectedIds}
/>
<TagsModal
isOpen={isTagsModalOpen}
ids={selectedIds}
onApplyTagsPress={onApplyTagsPress}
onModalClose={onTagsModalClose}
/>
<ConfirmModal
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title="Delete Import List(s)"
message={`Are you sure you want to delete ${selectedIds.length} import list(s)?`}
confirmLabel="Delete"
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
/>
</ModalContent>
);
}
export default ManageIndexersModalContent;

View File

@@ -0,0 +1,11 @@
.name,
.tags,
.enableRss,
.enableAutomaticSearch,
.enableInteractiveSearch,
.priority,
.implementation {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all;
}

View File

@@ -0,0 +1,13 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'enableAutomaticSearch': string;
'enableInteractiveSearch': string;
'enableRss': string;
'implementation': string;
'name': string;
'priority': string;
'tags': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,82 @@
import React, { useCallback } from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import TagListConnector from 'Components/TagListConnector';
import { SelectStateInputProps } from 'typings/props';
import styles from './ManageIndexersModalRow.css';
interface ManageIndexersModalRowProps {
id: number;
name: string;
enableRss: boolean;
enableAutomaticSearch: boolean;
enableInteractiveSearch: boolean;
priority: number;
implementation: string;
tags: number[];
columns: Column[];
isSelected?: boolean;
onSelectedChange(result: SelectStateInputProps): void;
}
function ManageIndexersModalRow(props: ManageIndexersModalRowProps) {
const {
id,
isSelected,
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
priority,
implementation,
tags,
onSelectedChange,
} = props;
const onSelectedChangeWrapper = useCallback(
(result: SelectStateInputProps) => {
onSelectedChange({
...result,
});
},
[onSelectedChange]
);
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChangeWrapper}
/>
<TableRowCell className={styles.name}>{name}</TableRowCell>
<TableRowCell className={styles.implementation}>
{implementation}
</TableRowCell>
<TableRowCell className={styles.enableRss}>
{enableRss ? 'Yes' : 'No'}
</TableRowCell>
<TableRowCell className={styles.enableAutomaticSearch}>
{enableAutomaticSearch ? 'Yes' : 'No'}
</TableRowCell>
<TableRowCell className={styles.enableInteractiveSearch}>
{enableInteractiveSearch ? 'Yes' : 'No'}
</TableRowCell>
<TableRowCell className={styles.priority}>{priority}</TableRowCell>
<TableRowCell className={styles.tags}>
<TagListConnector tags={tags} />
</TableRowCell>
</TableRow>
);
}
export default ManageIndexersModalRow;

View File

@@ -0,0 +1,22 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import TagsModalContent from './TagsModalContent';
interface TagsModalProps {
isOpen: boolean;
ids: number[];
onApplyTagsPress: (tags: number[], applyTags: string) => void;
onModalClose: () => void;
}
function TagsModal(props: TagsModalProps) {
const { isOpen, onModalClose, ...otherProps } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<TagsModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}
export default TagsModal;

View File

@@ -0,0 +1,12 @@
.renameIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.result {
padding-top: 4px;
}

View File

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

View File

@@ -0,0 +1,178 @@
import { uniq } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import { IndexerAppState } from 'App/State/SettingsAppState';
import { Tag } from 'App/State/TagsAppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import Indexer from 'typings/Indexer';
import styles from './TagsModalContent.css';
interface TagsModalContentProps {
ids: number[];
onApplyTagsPress: (tags: number[], applyTags: string) => void;
onModalClose: () => void;
}
function TagsModalContent(props: TagsModalContentProps) {
const { ids, onModalClose, onApplyTagsPress } = props;
const allIndexers: IndexerAppState = useSelector(
(state: AppState) => state.settings.indexers
);
const tagList: Tag[] = useSelector(createTagsSelector());
const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add');
const seriesTags = useMemo(() => {
const tags = ids.reduce((acc: number[], id) => {
const s = allIndexers.items.find((s: Indexer) => s.id === id);
if (s) {
acc.push(...s.tags);
}
return acc;
}, []);
return uniq(tags);
}, [ids, allIndexers]);
const onTagsChange = useCallback(
({ value }: { value: number[] }) => {
setTags(value);
},
[setTags]
);
const onApplyTagsChange = useCallback(
({ value }: { value: string }) => {
setApplyTags(value);
},
[setApplyTags]
);
const onApplyPress = useCallback(() => {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: 'Add' },
{ key: 'remove', value: 'Remove' },
{ key: 'replace', value: 'Replace' },
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Tags</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>Tags</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
value={tags}
onChange={onTagsChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Apply Tags</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="applyTags"
value={applyTags}
values={applyTagsOptions}
helpTexts={[
'How to apply tags to the selected indexer(s)',
'Add: Add the tags the existing list of tags',
'Remove: Remove the entered tags',
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
]}
onChange={onApplyTagsChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Result</FormLabel>
<div className={styles.result}>
{seriesTags.map((id) => {
const tag = tagList.find((t) => t.id === id);
if (!tag) {
return null;
}
const removeTag =
(applyTags === 'remove' && tags.indexOf(id) > -1) ||
(applyTags === 'replace' && tags.indexOf(id) === -1);
return (
<Label
key={tag.id}
title={removeTag ? 'Removing tag' : 'Existing tag'}
kind={removeTag ? kinds.INVERSE : kinds.INFO}
size={sizes.LARGE}
>
{tag.label}
</Label>
);
})}
{(applyTags === 'add' || applyTags === 'replace') &&
tags.map((id) => {
const tag = tagList.find((t) => t.id === id);
if (!tag) {
return null;
}
if (seriesTags.indexOf(id) > -1) {
return null;
}
return (
<Label
key={tag.id}
title={'Adding tag'}
kind={kinds.SUCCESS}
size={sizes.LARGE}
>
{tag.label}
</Label>
);
})}
</div>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
Apply
</Button>
</ModalFooter>
</ModalContent>
);
}
export default TagsModalContent;

View File

@@ -1,12 +1,13 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes } from 'Helpers/Props';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function IndexerOptions(props) {
@@ -28,9 +29,9 @@ function IndexerOptions(props) {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadIndexerOptions')}
</div>
</Alert>
}
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -8,7 +9,7 @@ import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes, sizes } from 'Helpers/Props';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import RootFoldersConnector from 'RootFolder/RootFoldersConnector';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
@@ -63,29 +64,29 @@ class MediaManagement extends Component {
<NamingConnector />
{
isFetching &&
isFetching ?
<FieldSet legend={translate('NamingSettings')}>
<LoadingIndicator />
</FieldSet>
</FieldSet> : null
}
{
!isFetching && error &&
!isFetching && error ?
<FieldSet legend={translate('NamingSettings')}>
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadMediaManagementSettings')}
</div>
</FieldSet>
</Alert>
</FieldSet> : null
}
{
hasSettings && !isFetching && !error &&
hasSettings && !isFetching && !error ?
<Form
id="mediaManagementSettings"
{...otherProps}
>
{
advancedSettings &&
advancedSettings ?
<FieldSet legend={translate('Folders')}>
<FormGroup
advancedSettings={advancedSettings}
@@ -120,11 +121,11 @@ class MediaManagement extends Component {
{...settings.deleteEmptyFolders}
/>
</FormGroup>
</FieldSet>
</FieldSet> : null
}
{
advancedSettings &&
advancedSettings ?
<FieldSet
legend={translate('Importing')}
>
@@ -181,6 +182,41 @@ class MediaManagement extends Component {
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('ImportUsingScript')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="useScriptImport"
helpText={translate('UseScriptImportHelpText')}
onChange={onInputChange}
{...settings.useScriptImport}
/>
</FormGroup>
{
settings.useScriptImport.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ImportScriptPath')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
includeFiles={true}
name="scriptImportPath"
helpText={translate('ScriptImportPathHelpText')}
onChange={onInputChange}
{...settings.scriptImportPath}
/>
</FormGroup> : null
}
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
@@ -194,7 +230,7 @@ class MediaManagement extends Component {
</FormGroup>
{
settings.importExtraFiles.value &&
settings.importExtraFiles.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
@@ -211,9 +247,9 @@ class MediaManagement extends Component {
onChange={onInputChange}
{...settings.extraFileExtensions}
/>
</FormGroup>
</FormGroup> : null
}
</FieldSet>
</FieldSet> : null
}
<FieldSet
@@ -339,7 +375,7 @@ class MediaManagement extends Component {
</FieldSet>
{
advancedSettings && !isWindows &&
advancedSettings && !isWindows ?
<FieldSet
legend={translate('Permissions')}
>
@@ -392,9 +428,9 @@ class MediaManagement extends Component {
{...settings.chownGroup}
/>
</FormGroup>
</FieldSet>
</FieldSet> : null
}
</Form>
</Form> : null
}
<FieldSet legend={translate('RootFolders')}>

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -7,7 +8,7 @@ import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes, sizes } from 'Helpers/Props';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import NamingModal from './NamingModal';
import styles from './Naming.css';
@@ -110,9 +111,9 @@ class Naming extends Component {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadNamingSettings')}
</div>
</Alert>
}
{

View File

@@ -6,8 +6,9 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes } from 'Helpers/Props';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import Alert from '../../../Components/Alert';
// Note: Do Not Translate Certification Countries
@@ -43,9 +44,9 @@ function MetadataOptions(props) {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadIndexerOptions')}
</div>
</Alert>
}
{

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditNotificationModalConnector from './EditNotificationModalConnector';
@@ -78,7 +79,9 @@ class Notification extends Component {
supportsOnHealthIssue,
supportsOnHealthRestored,
supportsOnApplicationUpdate,
supportsOnManualInteractionRequired
supportsOnManualInteractionRequired,
tags,
tagList
} = this.props;
return (
@@ -198,6 +201,11 @@ class Notification extends Component {
null
}
<TagList
tags={tags}
tagList={tagList}
/>
<EditNotificationModalConnector
id={id}
isOpen={this.state.isEditNotificationModalOpen}
@@ -246,6 +254,8 @@ Notification.propTypes = {
supportsOnHealthRestored: PropTypes.bool.isRequired,
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
supportsOnManualInteractionRequired: PropTypes.bool.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteNotification: PropTypes.func.isRequired
};

View File

@@ -49,6 +49,7 @@ class Notifications extends Component {
render() {
const {
items,
tagList,
onConfirmDeleteNotification,
...otherProps
} = this.props;
@@ -71,6 +72,7 @@ class Notifications extends Component {
<Notification
key={item.id}
{...item}
tagList={tagList}
onConfirmDeleteNotification={onConfirmDeleteNotification}
/>
);
@@ -109,6 +111,7 @@ Notifications.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteNotification: PropTypes.func.isRequired
};

View File

@@ -4,13 +4,20 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName';
import Notifications from './Notifications';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.notifications', sortByName),
(notifications) => notifications
createTagsSelector(),
(notifications, tagList) => {
return {
...notifications,
tagList
};
}
);
}

View File

@@ -141,7 +141,7 @@ class DelayProfile extends Component {
isOpen={this.state.isDeleteDelayProfileModalOpen}
kind={kinds.DANGER}
title={translate('DeleteDelayProfile')}
message={translate('AreYouSureYouWantToDeleteThisDelayProfile')}
message={translate('DeleteDelayProfileMessageText')}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteDelayProfile}
onCancel={this.onDeleteDelayProfileModalClose}

View File

@@ -60,17 +60,19 @@ class ResetQualityDefinitionsModalContent extends Component {
<ModalBody>
<div className={styles.messageContainer}>
{translate('AreYouSureYouWantToResetQualityDefinitions')}
{translate('ResetQualityDefinitionsMessageText')}
</div>
<FormGroup>
<FormLabel>{translate('ResetTitles')}</FormLabel>
<FormLabel>
{translate('ResetTitles')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="resetDefinitionTitles"
value={resetDefinitionTitles}
helpText={translate('ResetTitlesHelpText')}
helpText={translate('ResetDefinitionTitlesHelpText')}
onChange={this.onResetDefinitionTitlesChange}
/>
</FormGroup>

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