Compare commits

...

72 Commits

Author SHA1 Message Date
Stevie Robinson 8c9555f82e New: Informational text on Custom Formats modal
(cherry picked from commit 238ba85f0a2639608d9890292dfe0b96c0212f10)

Closes #3405
2024-04-06 16:47:46 +03:00
Cuki ee20ba1811 Fixed: Use widely supported display mode for PWA
(cherry picked from commit 1562d3bae3002947f9e428321d2b162ad69c3309)
2024-04-06 16:45:17 +03:00
Mark McDowall 4cf1215cfa Fixed: Cleanse BHD RSS key in log files
(cherry picked from commit 60ee7cc716d344fc904fa6fb28f7be0386ae710d)
2024-04-06 16:45:00 +03:00
Mark McDowall a8ab099177 Fixed: Sending ntfy.sh notifications with unicode characters
(cherry picked from commit a169ebff2adda5c8585c6aae6249b1c1f7c12264)
2024-04-06 16:44:45 +03:00
Servarr 50af8a12d4 Automated API Docs update 2024-04-06 06:56:02 +03:00
Weblate 510c39c5d8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2024-04-06 06:19:30 +03:00
Bogdan dd4a0121f2 Fixed: Migrations running on newer versions of SQLite 2024-04-06 06:18:29 +03:00
Mark McDowall 4fb62c072a Fixed: Task with removed author causing error
(cherry picked from commit fc6494c569324c839debdb1d08dde23b8f1b8d76)

Closes #3376
2024-03-28 11:22:01 +02:00
Louis R 2b100d0f72 Fixed: Exceptions when checking for routable IPv4 addresses
(cherry picked from commit 060b789bc6f10f667795697eb536d4bd3851da49)
2024-03-28 11:19:16 +02:00
Weblate abfdc44f92 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Casselluu <jack10193@163.com>
Co-authored-by: CharlesEnard <charles.enard@gmail.com>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Stanislav <prekop3@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: shimmyx <shimmygodx@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2024-03-28 11:06:09 +02:00
Bogdan 6e76f9966a New: Allow HEAD requests to ping endpoint
(cherry picked from commit 7353fe479dbb8d0dab76993ebed92d48e1b05524)
2024-03-28 11:05:17 +02:00
Stevie Robinson 2b6ceab9d4 Fixed: Handling torrents with relative path in rTorrent
(cherry picked from commit 35d0e6a6f806c68756450a7d199600d7fb49d6c5)
2024-03-28 11:05:05 +02:00
Bogdan b636729960 New: Advanced settings toggle in import list, notification and download client modals
(cherry picked from commit 13c925b3418d1d48ec041e3d97ab51aaf2b8977a)
2024-03-28 11:04:53 +02:00
Carlos Gustavo Sarmiento 8af8366575 Fixed: qBittorrent not correctly handling retention during testing
(cherry picked from commit 588372fd950fc85f5e9a4275fbcb423b247ed0ee)
2024-03-28 11:04:41 +02:00
Bogdan 1d31e9b9d9 Bump version to 0.3.22 2024-03-24 17:46:11 +02:00
Mark McDowall 37a9f670dd Fixed: Task progress messages in the UI
(cherry picked from commit c6417337812f3578a27f9dc1e44fdad80f557271)

Closes #3370
2024-03-22 11:45:46 +02:00
Bogdan 11eda3b11b Fix BookInfo tests 2024-03-17 00:36:02 +02:00
Weblate 04682c9d91 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dennis Langthjem <dennis@langthjem.dk>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ihor Mudryi <mudryy33@gmail.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com>
Co-authored-by: Mark Martines <mark-martines@hotmail.com>
Co-authored-by: Maxence Winandy <maxence.winandy@gmail.com>
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: infoaitek24 <info@aitekph.com>
Co-authored-by: linkin931 <931linkin@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2024-03-16 18:19:11 +02:00
Mina Gaid 50fdc449ac Fixed icons for macOS application 2024-03-16 17:52:10 +02:00
Bogdan b8c295727a Link to author from book details
Co-authored-by: plmcgrn <889547+plmcgrn@users.noreply.github.com>

Closes #3356
2024-03-16 00:46:31 +02:00
Mark McDowall 93ee466780 New: Show author names after task name when applicable
(cherry picked from commit 6d552f2a60f44052079b5e8944f5e1bbabac56e0)

Closes #3361
2024-03-15 23:50:22 +02:00
Mark McDowall 77f1e8f8c9 Fixed: Disabled select option still selectable
(cherry picked from commit 063dba22a803295adee4fdcbe42718af3e85ca78)

Closes #3362
2024-03-15 23:32:38 +02:00
Bogdan 1aa746bea1 Ensure authors are populated in PageConnector 2024-03-15 23:28:18 +02:00
Bogdan 490041d77c Ensure not allowed cursor is shown for disabled select inputs 2024-03-15 23:26:02 +02:00
Stevie Robinson 5dc5592c17 Fixed: Wrapping of naming tokens with alternate separators
(cherry picked from commit 80630bf97f5bb3b49d4824dc039d2edfc74e4797)

Closes #3294
Closes #3360
2024-03-15 23:25:16 +02:00
Servarr 8fb1aff68a Automated API Docs update 2024-03-14 07:58:15 +02:00
Mark McDowall a397a19034 Fixed: Release push with only Magnet URL
(cherry picked from commit 9f705e4161af3f4dd55b399d56b0b9c5a36e181b)
2024-03-14 07:13:17 +02:00
Bogdan d0df761422 New: Indexer flags
(cherry picked from commit 7a768b5d0faf9aa57e78aee19cefee8fb19a42d5)
2024-03-10 19:11:25 +02:00
Bogdan 4781675c1a Bump ImageSharp, Polly 2024-03-10 13:14:58 +02:00
Bogdan 0361262bb4 Bump version to 0.3.21 2024-03-10 09:06:37 +02:00
Bogdan 3407cc9a7a New: XXL modal size 2024-03-08 12:46:56 +02:00
Helvio Pedreschi 4829916f0a Fixed: WebApp functionality on Apple devices
(cherry picked from commit c7dd7abf892eead7796fcc482aa2f2aabaf88712)
2024-03-08 12:43:45 +02:00
Bogdan c505eafd30 Fixed: Append author name to Interactive Search header in Wanted/Missing 2024-03-06 16:27:52 +02:00
Bogdan 07f218f294 Fix proxy search test 2024-03-06 08:15:13 +02:00
Mark McDowall 42751b598b Fixed: Misaligned table border
Closes #2232

(cherry picked from commit aa938d911b61b08185dc57a0887f3f33e3c6e1f2)
2024-03-06 08:05:15 +02:00
Bogdan 5e7e0eb50b New: Append author name to Interactive Search header
Closes #3343
2024-03-06 07:56:40 +02:00
Mark McDowall d6c631457c New: URL Base setting for Plex Server connections
Plus some translations

(cherry picked from commit 9fd193d2a82d5c2cdc0f36c1f984e4b6b68aaa8d)
2024-03-06 07:34:55 +02:00
Mark McDowall 12ee76d222 Queue Manual Import commands at high priority
(cherry picked from commit 64c6a8879beb1b17122c8f6f74bf7b3cf4dd1570)
2024-03-06 07:28:12 +02:00
Louis R 3ea80038d3 Fixed: Don't disable IPv6 in IPv6-only Environment
(cherry picked from commit 13af6f57796e54c3949cf340e03f020e6f8575c4)
2024-03-06 07:26:12 +02:00
nopoz 55404cdf24 New: Add download directory & move completed for Deluge
(cherry picked from commit 07bd159436935a7adb87ae1b6924a4d42d719b0f)
2024-03-06 07:25:18 +02:00
Weblate 83a9cd4f3e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Nicolò Castagnola <nipica@outlook.it>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translation: Servarr/Readarr
2024-03-03 02:35:06 +02:00
Weblate 3572d7330d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Chaoshuai Lü <lcs@meta.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Sadi A. Nogueira <contato@sadi.eti.br>
Co-authored-by: Steve Hansen <steve@hansenconsultancy.be>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: 闫锦彪 <yanjinbiaohere@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2024-03-02 09:29:37 +02:00
Mark McDowall a9b652a280 Fixed: Multi-word genres in Auto Tags
Fixed #6488

(cherry picked from commit 5c4f82999368edfedd038a0a27d323e04b81a400)
2024-03-02 09:28:10 +02:00
Bogdan 8efb2eb71a Fixed: Selection of last added custom filter
Plus some translations and typos

(cherry picked from commit 1f97679868012b70beecc553557e96e6c8bc80e3)
2024-03-02 09:25:15 +02:00
Bogdan 17094f1998 New: Options button for Missing/Cutoff Unmet
(cherry picked from commit 2773f77e1c4e3a8c8d01bcbea67333801c7840df)
2024-03-02 09:21:42 +02:00
Bogdan ddf5dc25a1 Update caniuse-lite
(cherry picked from commit 64f4365fe98b569efdf436710d5f56684f2aab66)
2024-03-02 09:21:34 +02:00
Mark McDowall fa2614954b Increase migration timeout to 5 minutes
(cherry picked from commit 086d3b5afaa7680d22835ca66da2afcb6dd5865e)
2024-03-02 09:21:23 +02:00
Mark McDowall 2e2894b3d3 New: Bypass archived history for failed downloads in SABnzbd
(cherry picked from commit c99d81e79ba5e6ecec01ddd942440d8a48a1c23b)
2024-03-02 09:21:11 +02:00
Bogdan 59ff407e76 Bump node to v20.x on builder 2024-02-23 20:19:37 +02:00
Bogdan bbd7b9f92e Bump version to 0.3.20 2024-02-18 20:32:32 +02:00
Bogdan c77d820763 Fix tests for storing last search time for books 2024-02-17 23:47:11 +02:00
Servarr 3327ed0f49 Automated API Docs update 2024-02-17 23:47:03 +02:00
Mark McDowall 44009e980b Fixed: A potential issue when extra files for multiple authors have the same relative path
(cherry picked from commit a6a68b4cae7688506c45ff6cf10989fe6596c274)

Closes #1650
2024-02-17 23:09:31 +02:00
Mark McDowall 02fd733223 Fixed: Don't convert author/book selection filter to lower case in state
(cherry picked from commit ca52eb76ca2e286479f1803f399d5f5b563cfb41)

Closes #692
2024-02-17 23:06:18 +02:00
Bogdan 2fa9576d05 New: Missing/Cutoff Unmet searches will search for books that haven't been searched recently first
Closes #2088

Simplify filter expression for cutoff unmet album search
2024-02-17 23:04:22 +02:00
Mark McDowall c7ee278ee4 New: Store last search time for BookSearch
(cherry picked from commit 9af57c6786eedd9beda4e1c6b8cdca20d165b622)
2024-02-17 22:58:01 +02:00
Bogdan d72c27ceed Fixed: Refresh tags state to clear removed tags by housekeeping
(cherry picked from commit 2510f44c25bee6fede27d9fa2b9614176d12cb55)

(cherry picked from commit ed27bcf213bdbc5cede650f89eb65593dc9631b4)
2024-02-14 03:11:46 +02:00
Bogdan 7a20fe2288 Improve messaging on indexer specified download client is not available
(cherry picked from commit 84e657482d37eed35f09c6dab3c2b8b5ebd5bac4)
2024-02-14 03:11:36 +02:00
Bogdan 042b62a2a5 Show download client ID as hint in select options
(cherry picked from commit c0b17d9345367ab6500b7cca6bb70c1e3b930284)
2024-02-14 03:11:22 +02:00
abcasada 88141e9d63 Hints for week column and short dates in UI settings
(cherry picked from commit 4558f552820b52bb1f9cd97fdabe03654ce9924a)

(cherry picked from commit f1d343218cdbd5a63abeb2eb97bba1105dc8035d)
2024-02-14 03:11:11 +02:00
Weblate 7fa1114edf Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translation: Servarr/Readarr
2024-02-12 00:54:08 +02:00
Bogdan d4262532e2 Ignore tests temporarily 2024-02-12 00:52:33 +02:00
Bogdan a21f83aae1 Some translations for Manual Import dropdowns 2024-02-12 00:05:20 +02:00
Bogdan d659e86a7d Fixed: Progress bar for authors and books 2024-02-12 00:04:45 +02:00
Weblate 0b924005ec Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hicabi Erdem <bilgi@hicabierdem.com>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: aghus <aghus.m@outlook.com>
Co-authored-by: bai0012 <baicongrui@gmail.com>
Co-authored-by: savin-msk <ns@a77.io>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2024-02-11 05:10:12 +02:00
Mark McDowall ba2fad5d9c Fixed: Don't use sub folder to check for free disk space for update
(cherry picked from commit f722d49b3a9efefa65bef1b24d90be9332ca62ea)

Closes #3299
2024-02-07 09:00:22 +02:00
Mark McDowall 58416cee67 New: Log database engine version on startup
(cherry picked from commit 6ab1d8e16b29e98b4d2ebb68e0356f6f2d3a2c10)
2024-02-07 08:58:46 +02:00
Mark McDowall 38124313c7 Fixed: Redirecting after login
(cherry picked from commit 745b92daf4bf4b9562ffe52dad84a12a5561add5)
2024-02-07 08:58:33 +02:00
Bogdan 3fc9f6c0a4 Bump version to 0.3.19 2024-02-04 12:55:21 +02:00
Weblate 79ce5abd53 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: aghus <aghus.m@outlook.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2024-02-03 22:42:17 +02:00
Weblate 7f01d597cb Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Crocmou <slaanesh8854@gmail.com>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Stas Panasiuk <temnyip@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2024-02-01 08:22:34 +02:00
Bogdan 31f35df71d Only bind shortcut for pending changes confirmation when it's shown
(cherry picked from commit ded7c3c6e2459f041297d479c788febc5d061854)
2024-02-01 08:21:58 +02:00
215 changed files with 2978 additions and 1186 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

+2 -2
View File
@@ -9,14 +9,14 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.3.18' majorVersion: '0.3.22'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)' readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)' buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.417' dotnetVersion: '6.0.417'
nodeVersion: '16.X' nodeVersion: '20.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04' linuxImage: 'ubuntu-20.04'
+6 -4
View File
@@ -218,10 +218,12 @@ class HistoryRow extends Component {
key={name} key={name}
className={styles.details} className={styles.details}
> >
<IconButton <div className={styles.actionContents}>
name={icons.INFO} <IconButton
onPress={this.onDetailsPress} name={icons.INFO}
/> onPress={this.onDetailsPress}
/>
</div>
</TableRowCell> </TableRowCell>
); );
} }
+4
View File
@@ -1,3 +1,5 @@
import AuthorsAppState from './AuthorsAppState';
import CommandAppState from './CommandAppState';
import SettingsAppState from './SettingsAppState'; import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState'; import TagsAppState from './TagsAppState';
@@ -34,6 +36,8 @@ export interface CustomFilter {
} }
interface AppState { interface AppState {
authors: AuthorsAppState;
commands: CommandAppState;
settings: SettingsAppState; settings: SettingsAppState;
tags: TagsAppState; tags: TagsAppState;
} }
+18
View File
@@ -0,0 +1,18 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import Author from 'Author/Author';
interface AuthorsAppState
extends AppSectionState<Author>,
AppSectionDeleteState,
AppSectionSaveState {
itemMap: Record<number, number>;
deleteOptions: {
addImportListExclusion: boolean;
};
}
export default AuthorsAppState;
@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;
@@ -5,6 +5,7 @@ import AppSectionState, {
import DownloadClient from 'typings/DownloadClient'; import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList'; import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer'; import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification'; import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings'; import { UiSettings } from 'typings/UiSettings';
@@ -27,11 +28,13 @@ export interface NotificationAppState
extends AppSectionState<Notification>, extends AppSectionState<Notification>,
AppSectionDeleteState {} AppSectionDeleteState {}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type UiSettingsAppState = AppSectionState<UiSettings>; export type UiSettingsAppState = AppSectionState<UiSettings>;
interface SettingsAppState { interface SettingsAppState {
downloadClients: DownloadClientAppState; downloadClients: DownloadClientAppState;
importLists: ImportListAppState; importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState; indexers: IndexerAppState;
notifications: NotificationAppState; notifications: NotificationAppState;
uiSettings: UiSettingsAppState; uiSettings: UiSettingsAppState;
+18
View File
@@ -0,0 +1,18 @@
import ModelBase from 'App/ModelBase';
interface Author extends ModelBase {
added: string;
genres: string[];
monitored: boolean;
overview: string;
path: string;
qualityProfileId: number;
metadataProfileId: number;
rootFolderPath: string;
sortName: string;
tags: number[];
authorName: string;
isSaving?: boolean;
}
export default Author;
+2 -2
View File
@@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
function AuthorNameLink({ titleSlug, authorName }) { function AuthorNameLink({ titleSlug, authorName, ...otherProps }) {
const link = `/author/${titleSlug}`; const link = `/author/${titleSlug}`;
return ( return (
<Link to={link}> <Link to={link} {...otherProps}>
{authorName} {authorName}
</Link> </Link>
); );
+6
View File
@@ -27,3 +27,9 @@
width: 80px; width: 80px;
} }
.indexerFlags {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}
+1
View File
@@ -1,6 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'indexerFlags': string;
'monitored': string; 'monitored': string;
'pageCount': string; 'pageCount': string;
'position': string; 'position': string;
+32
View File
@@ -2,12 +2,17 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import BookSearchCellConnector from 'Book/BookSearchCellConnector'; import BookSearchCellConnector from 'Book/BookSearchCellConnector';
import BookTitleLink from 'Book/BookTitleLink'; import BookTitleLink from 'Book/BookTitleLink';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton'; import MonitorToggleButton from 'Components/MonitorToggleButton';
import StarRating from 'Components/StarRating'; import StarRating from 'Components/StarRating';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BookStatus from './BookStatus'; import BookStatus from './BookStatus';
import styles from './BookRow.css'; import styles from './BookRow.css';
@@ -59,6 +64,7 @@ class BookRow extends Component {
releaseDate, releaseDate,
title, title,
seriesTitle, seriesTitle,
authorName,
position, position,
pageCount, pageCount,
ratings, ratings,
@@ -66,6 +72,7 @@ class BookRow extends Component {
authorMonitored, authorMonitored,
titleSlug, titleSlug,
bookFiles, bookFiles,
indexerFlags,
isEditorActive, isEditorActive,
isSelected, isSelected,
onSelectedChange, onSelectedChange,
@@ -189,6 +196,24 @@ class BookRow extends Component {
); );
} }
if (name === 'indexerFlags') {
return (
<TableRowCell
key={name}
className={styles.indexerFlags}
>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
);
}
if (name === 'status') { if (name === 'status') {
return ( return (
<TableRowCell <TableRowCell
@@ -211,6 +236,7 @@ class BookRow extends Component {
bookId={id} bookId={id}
authorId={authorId} authorId={authorId}
bookTitle={title} bookTitle={title}
authorName={authorName}
/> />
); );
} }
@@ -229,9 +255,11 @@ BookRow.propTypes = {
releaseDate: PropTypes.string, releaseDate: PropTypes.string,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
seriesTitle: PropTypes.string.isRequired, seriesTitle: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired,
position: PropTypes.string, position: PropTypes.string,
pageCount: PropTypes.number, pageCount: PropTypes.number,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
indexerFlags: PropTypes.number.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
isSaving: PropTypes.bool, isSaving: PropTypes.bool,
authorMonitored: PropTypes.bool.isRequired, authorMonitored: PropTypes.bool.isRequired,
@@ -243,4 +271,8 @@ BookRow.propTypes = {
onMonitorBookPress: PropTypes.func.isRequired onMonitorBookPress: PropTypes.func.isRequired
}; };
BookRow.defaultProps = {
indexerFlags: 0
};
export default BookRow; export default BookRow;
@@ -7,21 +7,18 @@ import BookRow from './BookRow';
const selectBookFiles = createSelector( const selectBookFiles = createSelector(
(state) => state.bookFiles, (state) => state.bookFiles,
(bookFiles) => { (bookFiles) => {
const { const { items } = bookFiles;
items
} = bookFiles;
const bookFileDict = items.reduce((acc, file) => { return items.reduce((acc, file) => {
const bookId = file.bookId; const bookId = file.bookId;
if (!acc.hasOwnProperty(bookId)) { if (!acc.hasOwnProperty(bookId)) {
acc[bookId] = []; acc[bookId] = [];
} }
acc[bookId].push(file); acc[bookId].push(file);
return acc; return acc;
}, {}); }, {});
return bookFileDict;
} }
); );
@@ -31,9 +28,14 @@ function createMapStateToProps() {
selectBookFiles, selectBookFiles,
(state, { id }) => id, (state, { id }) => id,
(author = {}, bookFiles, bookId) => { (author = {}, bookFiles, bookId) => {
const files = bookFiles[bookId] ?? [];
const bookFile = files[0];
return { return {
authorMonitored: author.monitored, authorMonitored: author.monitored,
bookFiles: bookFiles[bookId] ?? [] authorName: author.authorName,
bookFiles: files,
indexerFlags: bookFile ? bookFile.indexerFlags : 0
}; };
} }
); );
@@ -173,7 +173,7 @@ class AuthorEditorFooter extends Component {
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') }, { key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') } { key: 'unmonitored', value: translate('Unmonitored') }
]; ];
@@ -90,7 +90,7 @@ class AuthorIndexOverview extends Component {
status, status,
titleSlug, titleSlug,
nextAiring, nextAiring,
statistics, statistics = {},
images, images,
posterWidth, posterWidth,
posterHeight, posterHeight,
@@ -113,10 +113,11 @@ class AuthorIndexOverview extends Component {
} = this.props; } = this.props;
const { const {
bookCount, bookCount = 0,
sizeOnDisk, availableBookCount = 0,
bookFileCount, bookFileCount = 0,
totalBookCount totalBookCount = 0,
sizeOnDisk = 0
} = statistics; } = statistics;
const { const {
@@ -179,6 +180,7 @@ class AuthorIndexOverview extends Component {
monitored={monitored} monitored={monitored}
status={status} status={status}
bookCount={bookCount} bookCount={bookCount}
availableBookCount={availableBookCount}
bookFileCount={bookFileCount} bookFileCount={bookFileCount}
totalBookCount={totalBookCount} totalBookCount={totalBookCount}
posterWidth={posterWidth} posterWidth={posterWidth}
@@ -85,7 +85,7 @@ class AuthorIndexPoster extends Component {
titleSlug, titleSlug,
status, status,
nextAiring, nextAiring,
statistics, statistics = {},
images, images,
posterWidth, posterWidth,
posterHeight, posterHeight,
@@ -110,10 +110,11 @@ class AuthorIndexPoster extends Component {
} = this.props; } = this.props;
const { const {
bookCount, bookCount = 0,
sizeOnDisk, availableBookCount = 0,
bookFileCount, bookFileCount = 0,
totalBookCount totalBookCount = 0,
sizeOnDisk = 0
} = statistics; } = statistics;
const { const {
@@ -213,6 +214,7 @@ class AuthorIndexPoster extends Component {
monitored={monitored} monitored={monitored}
status={status} status={status}
bookCount={bookCount} bookCount={bookCount}
availableBookCount={availableBookCount}
bookFileCount={bookFileCount} bookFileCount={bookFileCount}
totalBookCount={totalBookCount} totalBookCount={totalBookCount}
posterWidth={posterWidth} posterWidth={posterWidth}
@@ -11,14 +11,15 @@ function AuthorIndexProgressBar(props) {
monitored, monitored,
status, status,
bookCount, bookCount,
availableBookCount,
bookFileCount, bookFileCount,
totalBookCount, totalBookCount,
posterWidth, posterWidth,
detailedProgressBar detailedProgressBar
} = props; } = props;
const progress = bookCount ? bookCount / totalBookCount * 100 : 100; const progress = bookCount ? (availableBookCount / bookCount) * 100 : 100;
const text = `${bookCount} / ${totalBookCount}`; const text = `${availableBookCount} / ${bookCount}`;
return ( return (
<ProgressBar <ProgressBar
@@ -29,7 +30,7 @@ function AuthorIndexProgressBar(props) {
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL} size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
showText={detailedProgressBar} showText={detailedProgressBar}
text={text} text={text}
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])} title={translate('AuthorProgressBarText', { bookCount, availableBookCount, bookFileCount, totalBookCount })}
width={posterWidth} width={posterWidth}
/> />
); );
@@ -39,6 +40,7 @@ AuthorIndexProgressBar.propTypes = {
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
bookCount: PropTypes.number.isRequired, bookCount: PropTypes.number.isRequired,
availableBookCount: PropTypes.number.isRequired,
bookFileCount: PropTypes.number.isRequired, bookFileCount: PropTypes.number.isRequired,
totalBookCount: PropTypes.number.isRequired, totalBookCount: PropTypes.number.isRequired,
posterWidth: PropTypes.number.isRequired, posterWidth: PropTypes.number.isRequired,
@@ -90,7 +90,7 @@ class AuthorIndexRow extends Component {
nextBook, nextBook,
lastBook, lastBook,
added, added,
statistics, statistics = {},
genres, genres,
ratings, ratings,
path, path,
@@ -110,10 +110,11 @@ class AuthorIndexRow extends Component {
} = this.props; } = this.props;
const { const {
bookCount, bookCount = 0,
bookFileCount, availableBookCount = 0,
totalBookCount, bookFileCount = 0,
sizeOnDisk totalBookCount = 0,
sizeOnDisk = 0
} = statistics; } = statistics;
const { const {
@@ -286,7 +287,7 @@ class AuthorIndexRow extends Component {
} }
if (name === 'bookProgress') { if (name === 'bookProgress') {
const progress = bookCount ? bookFileCount / bookCount * 100 : 100; const progress = bookCount ? (availableBookCount / bookCount) * 100 : 100;
return ( return (
<VirtualTableRowCell <VirtualTableRowCell
@@ -297,8 +298,8 @@ class AuthorIndexRow extends Component {
progress={progress} progress={progress}
kind={getProgressBarKind(status, monitored, progress)} kind={getProgressBarKind(status, monitored, progress)}
showText={true} showText={true}
text={`${bookCount} / ${totalBookCount}`} text={`${availableBookCount} / ${bookCount}`}
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])} title={translate('AuthorProgressBarText', { bookCount, availableBookCount, bookFileCount, totalBookCount })}
width={125} width={125}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
+3
View File
@@ -38,6 +38,7 @@ class BookSearchCell extends Component {
const { const {
bookId, bookId,
bookTitle, bookTitle,
authorName,
isSearching, isSearching,
onSearchPress, onSearchPress,
...otherProps ...otherProps
@@ -60,6 +61,7 @@ class BookSearchCell extends Component {
isOpen={this.state.isDetailsModalOpen} isOpen={this.state.isDetailsModalOpen}
bookId={bookId} bookId={bookId}
bookTitle={bookTitle} bookTitle={bookTitle}
authorName={authorName}
onModalClose={this.onDetailsModalClose} onModalClose={this.onDetailsModalClose}
{...otherProps} {...otherProps}
/> />
@@ -73,6 +75,7 @@ BookSearchCell.propTypes = {
bookId: PropTypes.number.isRequired, bookId: PropTypes.number.isRequired,
authorId: PropTypes.number.isRequired, authorId: PropTypes.number.isRequired,
bookTitle: PropTypes.string.isRequired, bookTitle: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired,
isSearching: PropTypes.bool.isRequired, isSearching: PropTypes.bool.isRequired,
onSearchPress: PropTypes.func.isRequired onSearchPress: PropTypes.func.isRequired
}; };
@@ -84,9 +84,15 @@
font-size: 20px; font-size: 20px;
} }
.authorLink {
composes: link from '~Components/Link/Link.css';
margin-right: 15px;
color: var(--white);
}
.duration { .duration {
margin-right: 15px; margin-right: 15px;
margin-left: 10px;
} }
.detailsLabel { .detailsLabel {
+1
View File
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'alternateTitlesIconContainer': string; 'alternateTitlesIconContainer': string;
'authorLink': string;
'backdrop': string; 'backdrop': string;
'backdropOverlay': string; 'backdropOverlay': string;
'cover': string; 'cover': string;
@@ -2,6 +2,7 @@ import moment from 'moment';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate'; import TextTruncate from 'react-text-truncate';
import AuthorNameLink from 'Author/AuthorNameLink';
import BookCover from 'Book/BookCover'; import BookCover from 'Book/BookCover';
import HeartRating from 'Components/HeartRating'; import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
@@ -113,7 +114,7 @@ class BookDetailsHeader extends Component {
className={styles.monitorToggleButton} className={styles.monitorToggleButton}
monitored={monitored} monitored={monitored}
isSaving={isSaving} isSaving={isSaving}
size={isSmallScreen ? 30: 40} size={isSmallScreen ? 30 : 40}
onPress={onMonitorTogglePress} onPress={onMonitorTogglePress}
/> />
</div> </div>
@@ -131,7 +132,12 @@ class BookDetailsHeader extends Component {
</div> </div>
<div> <div>
{author.authorName} <AuthorNameLink
className={styles.authorLink}
titleSlug={author.titleSlug}
authorName={author.authorName}
/>
{ {
!!pageCount && !!pageCount &&
<span className={styles.duration}> <span className={styles.duration}>
+1 -1
View File
@@ -89,7 +89,7 @@ class BookEditorFooter extends Component {
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') }, { key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') } { key: 'unmonitored', value: translate('Unmonitored') }
]; ];
@@ -16,8 +16,8 @@ function BookIndexProgressBar(props) {
detailedProgressBar detailedProgressBar
} = props; } = props;
const progress = bookCount ? bookFileCount / totalBookCount * 100 : 0; const progress = bookFileCount && bookCount ? (totalBookCount / bookCount) * 100 : 0;
const text = `${bookFileCount} / ${bookCount}`; const text = `${bookFileCount ? bookCount : 0} / ${totalBookCount}`;
return ( return (
<ProgressBar <ProgressBar
@@ -28,7 +28,11 @@ function BookIndexProgressBar(props) {
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL} size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
showText={detailedProgressBar} showText={detailedProgressBar}
text={text} text={text}
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])} title={translate('BookProgressBarText', {
bookCount: bookFileCount ? bookCount : 0,
bookFileCount,
totalBookCount
})}
width={posterWidth} width={posterWidth}
/> />
); );
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
interface IndexerFlagsProps {
indexerFlags: number;
}
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
const flags = allIndexerFlags.items.filter(
// eslint-disable-next-line no-bitwise
(item) => (indexerFlags & item.id) === item.id
);
return flags.length ? (
<ul>
{flags.map((flag, index) => {
return <li key={index}>{flag.name}</li>;
})}
</ul>
) : null;
}
export default IndexerFlags;
@@ -9,19 +9,21 @@ function BookInteractiveSearchModal(props) {
isOpen, isOpen,
bookId, bookId,
bookTitle, bookTitle,
authorName,
onModalClose onModalClose
} = props; } = props;
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
size={sizes.EXTRA_LARGE} size={sizes.EXTRA_EXTRA_LARGE}
closeOnBackgroundClick={false} closeOnBackgroundClick={false}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
<BookInteractiveSearchModalContent <BookInteractiveSearchModalContent
bookId={bookId} bookId={bookId}
bookTitle={bookTitle} bookTitle={bookTitle}
authorName={authorName}
onModalClose={onModalClose} onModalClose={onModalClose}
/> />
</Modal> </Modal>
@@ -32,6 +34,7 @@ BookInteractiveSearchModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
bookId: PropTypes.number.isRequired, bookId: PropTypes.number.isRequired,
bookTitle: PropTypes.string.isRequired, bookTitle: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
@@ -7,18 +7,23 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { scrollDirections } from 'Helpers/Props'; import { scrollDirections } from 'Helpers/Props';
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector'; import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
import translate from 'Utilities/String/translate';
function BookInteractiveSearchModalContent(props) { function BookInteractiveSearchModalContent(props) {
const { const {
bookId, bookId,
bookTitle, bookTitle,
authorName,
onModalClose onModalClose
} = props; } = props;
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Interactive Search {bookId != null && `- ${bookTitle}`} {bookId === null ?
translate('InteractiveSearchModalHeader') :
translate('InteractiveSearchModalHeaderBookAuthor', { bookTitle, authorName })
}
</ModalHeader> </ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}> <ModalBody scrollDirection={scrollDirections.BOTH}>
@@ -32,7 +37,7 @@ function BookInteractiveSearchModalContent(props) {
<ModalFooter> <ModalFooter>
<Button onPress={onModalClose}> <Button onPress={onModalClose}>
Close {translate('Close')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -42,6 +47,7 @@ function BookInteractiveSearchModalContent(props) {
BookInteractiveSearchModalContent.propTypes = { BookInteractiveSearchModalContent.propTypes = {
bookId: PropTypes.number.isRequired, bookId: PropTypes.number.isRequired,
bookTitle: PropTypes.string.isRequired, bookTitle: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
@@ -116,7 +116,7 @@ class BookFileEditorTableContent extends Component {
}); });
return acc; return acc;
}, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]); }, [{ key: 'selectQuality', value: translate('SelectQuality'), isDisabled: true }]);
const hasSelectedFiles = this.getSelectedIds().length > 0; const hasSelectedFiles = this.getSelectedIds().length > 0;
+11 -6
View File
@@ -27,14 +27,15 @@ class BookshelfBook extends Component {
title, title,
disambiguation, disambiguation,
monitored, monitored,
statistics, statistics = {},
isSaving isSaving
} = this.props; } = this.props;
const { const {
bookFileCount, bookCount = 0,
totalBookCount, bookFileCount = 0,
percentOfBooks totalBookCount = 0,
percentOfBooks = 0
} = statistics; } = statistics;
return ( return (
@@ -59,10 +60,14 @@ class BookshelfBook extends Component {
percentOfBooks < 100 && monitored && styles.missingWanted, percentOfBooks < 100 && monitored && styles.missingWanted,
percentOfBooks === 100 && styles.allBooks percentOfBooks === 100 && styles.allBooks
)} )}
title={translate('BookFileCounttotalBookCountBooksDownloadedInterp', [bookFileCount, totalBookCount])} title={translate('BookProgressBarText', {
bookCount: bookFileCount ? bookCount : 0,
bookFileCount,
totalBookCount
})}
> >
{ {
totalBookCount === 0 ? '0/0' : `${bookFileCount}/${totalBookCount}` totalBookCount === 0 ? '0/0' : `${bookFileCount ? bookCount : 0}/${totalBookCount}`
} }
</div> </div>
</div> </div>
+1 -1
View File
@@ -88,7 +88,7 @@ class BookshelfFooter extends Component {
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') }, { key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') } { key: 'unmonitored', value: translate('Unmonitored') }
]; ];
+38
View File
@@ -0,0 +1,38 @@
import ModelBase from 'App/ModelBase';
export interface CommandBody {
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
completionMessage: string;
requiresDiskAccess: boolean;
isExclusive: boolean;
isLongRunning: boolean;
name: string;
lastExecutionTime: string;
lastStartTime: string;
trigger: string;
suppressMessages: boolean;
authorId?: number;
authorIds?: number[];
}
interface Command extends ModelBase {
name: string;
commandName: string;
message: string;
body: CommandBody;
priority: string;
status: string;
result: string;
queued: string;
started: string;
ended: string;
duration: string;
trigger: string;
stateChangeTime: string;
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
lastExecutionTime: string;
}
export default Command;
@@ -1,3 +1,4 @@
import { maxBy } from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
if (id) { if (id) {
dispatchSetFilter({ selectedFilterKey: id }); dispatchSetFilter({ selectedFilterKey: id });
} else { } else {
const last = customFilters[customFilters.length -1]; const last = maxBy(customFilters, 'id');
dispatchSetFilter({ selectedFilterKey: last.id }); dispatchSetFilter({ selectedFilterKey: last.id });
} }
@@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
this.setState({ this.setState({
labelErrors: [ labelErrors: [
{ {
message: 'Label is required' message: translate('LabelIsRequired')
} }
] ]
}); });
@@ -146,13 +147,13 @@ class FilterBuilderModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Custom Filter {translate('CustomFilter')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div className={styles.labelContainer}> <div className={styles.labelContainer}>
<div className={styles.label}> <div className={styles.label}>
Label {translate('Label')}
</div> </div>
<div className={styles.labelInputContainer}> <div className={styles.labelInputContainer}>
@@ -195,7 +196,7 @@ class FilterBuilderModalContent extends Component {
<ModalFooter> <ModalFooter>
<Button onPress={onCancelPress}> <Button onPress={onCancelPress}>
Cancel {translate('Cancel')}
</Button> </Button>
<SpinnerErrorButton <SpinnerErrorButton
@@ -203,7 +204,7 @@ class FilterBuilderModalContent extends Component {
error={saveError} error={saveError}
onPress={this.onSaveFilterPress} onPress={this.onSaveFilterPress}
> >
Save {translate('Save')}
</SpinnerErrorButton> </SpinnerErrorButton>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -37,8 +37,8 @@ class CustomFilter extends Component {
dispatchSetFilter dispatchSetFilter
} = this.props; } = this.props;
// Assume that delete and then unmounting means the delete was successful. // Assume that delete and then unmounting means the deletion was successful.
// Moving this check to a ancestor would be more accurate, but would have // Moving this check to an ancestor would be more accurate, but would have
// more boilerplate. // more boilerplate.
if (this.state.isDeleting && id === selectedFilterKey) { if (this.state.isDeleting && id === selectedFilterKey) {
dispatchSetFilter({ selectedFilterKey: 'all' }); dispatchSetFilter({ selectedFilterKey: 'all' });
@@ -25,7 +25,8 @@ function createMapStateToProps() {
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => { const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
return { return {
key: downloadClient.id, key: downloadClient.id,
value: downloadClient.name value: downloadClient.name,
hint: `(${downloadClient.id})`
}; };
}); });
@@ -19,7 +19,7 @@
.isDisabled { .isDisabled {
opacity: 0.7; opacity: 0.7;
cursor: not-allowed; cursor: not-allowed !important;
} }
.dropdownArrowContainer { .dropdownArrowContainer {
@@ -14,6 +14,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector'; import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText'; import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
import IndexerSelectInputConnector from './IndexerSelectInputConnector'; import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput'; import KeyValueListInput from './KeyValueListInput';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector'; import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
@@ -83,6 +84,9 @@ function getComponent(type) {
case inputTypes.INDEXER_SELECT: case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector; return IndexerSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInput;
case inputTypes.DOWNLOAD_CLIENT_SELECT: case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector; return DownloadClientSelectInputConnector;
@@ -288,6 +292,7 @@ FormInputGroup.propTypes = {
includeNoChange: PropTypes.bool, includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object, selectedValueOptions: PropTypes.object,
indexerFlags: PropTypes.number,
pending: PropTypes.bool, pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object), errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object), warnings: PropTypes.arrayOf(PropTypes.object),
@@ -0,0 +1,62 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import EnhancedSelectInput from './EnhancedSelectInput';
const selectIndexerFlagsValues = (selectedFlags: number) =>
createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => {
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
// eslint-disable-next-line no-bitwise
if ((selectedFlags & id) === id) {
acc.push(id);
}
return acc;
}, []);
const values = indexerFlags.items.map(({ id, name }) => ({
key: id,
value: name,
}));
return {
value,
values,
};
}
);
interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
onChange(payload: object): void;
}
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
const { indexerFlags, onChange } = props;
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
const onChangeWrapper = useCallback(
({ name, value }: { name: string; value: number[] }) => {
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
onChange({ name, value: indexerFlags });
},
[onChange]
);
return (
<EnhancedSelectInput
{...props}
value={value}
values={values}
onChange={onChangeWrapper}
/>
);
}
export default IndexerFlagsSelectInput;
@@ -39,7 +39,7 @@ function createMapStateToProps() {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: translate('NoChange'), value: translate('NoChange'),
disabled: includeNoChangeDisabled isDisabled: includeNoChangeDisabled
}); });
} }
@@ -47,7 +47,7 @@ function createMapStateToProps() {
values.unshift({ values.unshift({
key: 'mixed', key: 'mixed',
value: '(Mixed)', value: '(Mixed)',
disabled: true isDisabled: true
}); });
} }
@@ -18,7 +18,7 @@ function MonitorBooksSelectInput(props) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: translate('NoChange'), value: translate('NoChange'),
disabled: true isDisabled: true
}); });
} }
@@ -26,7 +26,7 @@ function MonitorBooksSelectInput(props) {
values.unshift({ values.unshift({
key: 'mixed', key: 'mixed',
value: '(Mixed)', value: '(Mixed)',
disabled: true isDisabled: true
}); });
} }
@@ -17,7 +17,7 @@ function MonitorNewItemsSelectInput(props) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: translate('NoChange'), value: translate('NoChange'),
disabled: true isDisabled: true
}); });
} }
@@ -25,7 +25,7 @@ function MonitorNewItemsSelectInput(props) {
values.unshift({ values.unshift({
key: 'mixed', key: 'mixed',
value: '(Mixed)', value: '(Mixed)',
disabled: true isDisabled: true
}); });
} }
@@ -26,7 +26,7 @@ function createMapStateToProps() {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: translate('NoChange'), value: translate('NoChange'),
disabled: includeNoChangeDisabled isDisabled: includeNoChangeDisabled
}); });
} }
@@ -34,7 +34,7 @@ function createMapStateToProps() {
values.unshift({ values.unshift({
key: 'mixed', key: 'mixed',
value: '(Mixed)', value: '(Mixed)',
disabled: true isDisabled: true
}); });
} }
@@ -91,6 +91,7 @@ class TextTagInputConnector extends Component {
render() { render() {
return ( return (
<TagInput <TagInput
delimiters={['Tab', 'Enter', ',']}
tagList={[]} tagList={[]}
onTagAdd={this.onTagAdd} onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete} onTagDelete={this.onTagDelete}
+4
View File
@@ -2,3 +2,7 @@
margin-right: 5px; margin-right: 5px;
color: var(--themeRed); color: var(--themeRed);
} }
.rating {
margin-right: 15px;
}
+1
View File
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'heart': string; 'heart': string;
'rating': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
+1 -1
View File
@@ -6,7 +6,7 @@ import styles from './HeartRating.css';
function HeartRating({ rating, iconSize }) { function HeartRating({ rating, iconSize }) {
return ( return (
<span> <span className={styles.rating}>
<Icon <Icon
className={styles.heart} className={styles.heart}
name={icons.HEART} name={icons.HEART}
+9 -1
View File
@@ -63,6 +63,13 @@
width: 1280px; width: 1280px;
} }
.extraExtraLarge {
composes: modal;
width: 1600px;
}
@media only screen and (max-width: $breakpointExtraLarge) { @media only screen and (max-width: $breakpointExtraLarge) {
.modal.extraLarge { .modal.extraLarge {
width: 90%; width: 90%;
@@ -90,7 +97,8 @@
.modal.small, .modal.small,
.modal.medium, .modal.medium,
.modal.large, .modal.large,
.modal.extraLarge { .modal.extraLarge,
.modal.extraExtraLarge {
max-height: 100%; max-height: 100%;
width: 100%; width: 100%;
height: 100% !important; height: 100% !important;
+1
View File
@@ -1,6 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'extraExtraLarge': string;
'extraLarge': string; 'extraLarge': string;
'large': string; 'large': string;
'medium': string; 'medium': string;
+27 -1
View File
@@ -7,7 +7,14 @@ import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Ac
import { fetchAuthor } from 'Store/Actions/authorActions'; import { fetchAuthor } from 'Store/Actions/authorActions';
import { fetchBooks } from 'Store/Actions/bookActions'; import { fetchBooks } from 'Store/Actions/bookActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; import {
fetchImportLists,
fetchIndexerFlags,
fetchLanguages,
fetchMetadataProfiles,
fetchQualityProfiles,
fetchUISettings
} from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions'; import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -44,6 +51,7 @@ const selectAppProps = createSelector(
); );
const selectIsPopulated = createSelector( const selectIsPopulated = createSelector(
(state) => state.authors.isPopulated,
(state) => state.customFilters.isPopulated, (state) => state.customFilters.isPopulated,
(state) => state.tags.isPopulated, (state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated, (state) => state.settings.ui.isPopulated,
@@ -51,9 +59,11 @@ const selectIsPopulated = createSelector(
(state) => state.settings.qualityProfiles.isPopulated, (state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.metadataProfiles.isPopulated, (state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated, (state) => state.settings.importLists.isPopulated,
(state) => state.settings.indexerFlags.isPopulated,
(state) => state.system.status.isPopulated, (state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated, (state) => state.app.translations.isPopulated,
( (
authorsIsPopulated,
customFiltersIsPopulated, customFiltersIsPopulated,
tagsIsPopulated, tagsIsPopulated,
uiSettingsIsPopulated, uiSettingsIsPopulated,
@@ -61,10 +71,12 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated, qualityProfilesIsPopulated,
metadataProfilesIsPopulated, metadataProfilesIsPopulated,
importListsIsPopulated, importListsIsPopulated,
indexerFlagsIsPopulated,
systemStatusIsPopulated, systemStatusIsPopulated,
translationsIsPopulated translationsIsPopulated
) => { ) => {
return ( return (
authorsIsPopulated &&
customFiltersIsPopulated && customFiltersIsPopulated &&
tagsIsPopulated && tagsIsPopulated &&
uiSettingsIsPopulated && uiSettingsIsPopulated &&
@@ -72,6 +84,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated && qualityProfilesIsPopulated &&
metadataProfilesIsPopulated && metadataProfilesIsPopulated &&
importListsIsPopulated && importListsIsPopulated &&
indexerFlagsIsPopulated &&
systemStatusIsPopulated && systemStatusIsPopulated &&
translationsIsPopulated translationsIsPopulated
); );
@@ -79,6 +92,7 @@ const selectIsPopulated = createSelector(
); );
const selectErrors = createSelector( const selectErrors = createSelector(
(state) => state.authors.error,
(state) => state.customFilters.error, (state) => state.customFilters.error,
(state) => state.tags.error, (state) => state.tags.error,
(state) => state.settings.ui.error, (state) => state.settings.ui.error,
@@ -86,9 +100,11 @@ const selectErrors = createSelector(
(state) => state.settings.qualityProfiles.error, (state) => state.settings.qualityProfiles.error,
(state) => state.settings.metadataProfiles.error, (state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error, (state) => state.settings.importLists.error,
(state) => state.settings.indexerFlags.error,
(state) => state.system.status.error, (state) => state.system.status.error,
(state) => state.app.translations.error, (state) => state.app.translations.error,
( (
authorsError,
customFiltersError, customFiltersError,
tagsError, tagsError,
uiSettingsError, uiSettingsError,
@@ -96,10 +112,12 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
indexerFlagsError,
systemStatusError, systemStatusError,
translationsError translationsError
) => { ) => {
const hasError = !!( const hasError = !!(
authorsError ||
customFiltersError || customFiltersError ||
tagsError || tagsError ||
uiSettingsError || uiSettingsError ||
@@ -107,6 +125,7 @@ const selectErrors = createSelector(
qualityProfilesError || qualityProfilesError ||
metadataProfilesError || metadataProfilesError ||
importListsError || importListsError ||
indexerFlagsError ||
systemStatusError || systemStatusError ||
translationsError translationsError
); );
@@ -120,6 +139,7 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
indexerFlagsError,
systemStatusError, systemStatusError,
translationsError translationsError
}; };
@@ -177,6 +197,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchImportLists() { dispatchFetchImportLists() {
dispatch(fetchImportLists()); dispatch(fetchImportLists());
}, },
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchUISettings() { dispatchFetchUISettings() {
dispatch(fetchUISettings()); dispatch(fetchUISettings());
}, },
@@ -218,6 +241,7 @@ class PageConnector extends Component {
this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchMetadataProfiles(); this.props.dispatchFetchMetadataProfiles();
this.props.dispatchFetchImportLists(); this.props.dispatchFetchImportLists();
this.props.dispatchFetchIndexerFlags();
this.props.dispatchFetchUISettings(); this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus(); this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations(); this.props.dispatchFetchTranslations();
@@ -245,6 +269,7 @@ class PageConnector extends Component {
dispatchFetchQualityProfiles, dispatchFetchQualityProfiles,
dispatchFetchMetadataProfiles, dispatchFetchMetadataProfiles,
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchIndexerFlags,
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchStatus, dispatchFetchStatus,
dispatchFetchTranslations, dispatchFetchTranslations,
@@ -287,6 +312,7 @@ PageConnector.propTypes = {
dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchMetadataProfiles: PropTypes.func.isRequired, dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired, dispatchFetchTranslations: PropTypes.func.isRequired,
@@ -15,5 +15,5 @@
"start_url": "../../../../", "start_url": "../../../../",
"theme_color": "#3a3f51", "theme_color": "#3a3f51",
"background_color": "#3a3f51", "background_color": "#3a3f51",
"display": "minimal-ui" "display": "standalone"
} }
@@ -0,0 +1,17 @@
import { useCallback, useState } from 'react';
export default function useModalOpenState(
initialState: boolean
): [boolean, () => void, () => void] {
const [isOpen, setOpen] = useState(initialState);
const setModalOpen = useCallback(() => {
setOpen(true);
}, [setOpen]);
const setModalClosed = useCallback(() => {
setOpen(false);
}, [setOpen]);
return [isOpen, setModalOpen, setModalClosed];
}
+2
View File
@@ -60,6 +60,7 @@ import {
faFileImport as fasFileImport, faFileImport as fasFileImport,
faFileInvoice as farFileInvoice, faFileInvoice as farFileInvoice,
faFilter as fasFilter, faFilter as fasFilter,
faFlag as fasFlag,
faFolderOpen as fasFolderOpen, faFolderOpen as fasFolderOpen,
faForward as fasForward, faForward as fasForward,
faHeart as fasHeart, faHeart as fasHeart,
@@ -155,6 +156,7 @@ export const FATAL = fasTimesCircle;
export const FILE = farFile; export const FILE = farFile;
export const FILEIMPORT = fasFileImport; export const FILEIMPORT = fasFileImport;
export const FILTER = fasFilter; export const FILTER = fasFilter;
export const FLAG = fasFlag;
export const FOLDER = farFolder; export const FOLDER = farFolder;
export const FOLDER_OPEN = fasFolderOpen; export const FOLDER_OPEN = fasFolderOpen;
export const GROUP = farObjectGroup; export const GROUP = farObjectGroup;
+1
View File
@@ -15,6 +15,7 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect'; export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
export const BOOK_EDITION_SELECT = 'bookEditionSelect'; export const BOOK_EDITION_SELECT = 'bookEditionSelect';
export const INDEXER_SELECT = 'indexerSelect'; export const INDEXER_SELECT = 'indexerSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect'; export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select'; export const SELECT = 'select';
+2 -2
View File
@@ -3,5 +3,5 @@ export const SMALL = 'small';
export const MEDIUM = 'medium'; export const MEDIUM = 'medium';
export const LARGE = 'large'; export const LARGE = 'large';
export const EXTRA_LARGE = 'extraLarge'; export const EXTRA_LARGE = 'extraLarge';
export const EXTRA_EXTRA_LARGE = 'extraExtraLarge';
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE]; export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE];
@@ -29,7 +29,7 @@ class SelectAuthorModalContent extends Component {
// Listeners // Listeners
onFilterChange = ({ value }) => { onFilterChange = ({ value }) => {
this.setState({ filter: value.toLowerCase() }); this.setState({ filter: value });
}; };
// //
@@ -43,6 +43,7 @@ class SelectAuthorModalContent extends Component {
} = this.props; } = this.props;
const filter = this.state.filter; const filter = this.state.filter;
const filterLower = filter.toLowerCase();
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
@@ -66,7 +67,7 @@ class SelectAuthorModalContent extends Component {
<Scroller className={styles.scroller}> <Scroller className={styles.scroller}>
{ {
items.map((item) => { items.map((item) => {
return item.authorName.toLowerCase().includes(filter) ? return item.authorName.toLowerCase().includes(filterLower) ?
( (
<SelectAuthorRow <SelectAuthorRow
key={item.id} key={item.id}
@@ -52,7 +52,7 @@ class SelectBookModalContent extends Component {
// Listeners // Listeners
onFilterChange = ({ value }) => { onFilterChange = ({ value }) => {
this.setState({ filter: value.toLowerCase() }); this.setState({ filter: value });
}; };
// //
@@ -68,6 +68,7 @@ class SelectBookModalContent extends Component {
} = this.props; } = this.props;
const filter = this.state.filter; const filter = this.state.filter;
const filterLower = filter.toLowerCase();
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
@@ -101,7 +102,7 @@ class SelectBookModalContent extends Component {
<TableBody> <TableBody>
{ {
items.map((item) => { items.map((item) => {
return item.title.toLowerCase().includes(filter) ? return item.title.toLowerCase().includes(filterLower) ?
( (
<SelectBookRow <SelectBookRow
key={item.id} key={item.id}
@@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectIndexerFlagsModalContentConnector from './SelectIndexerFlagsModalContentConnector';
class SelectIndexerFlagsModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectIndexerFlagsModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectIndexerFlagsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectIndexerFlagsModal;
@@ -0,0 +1,7 @@
.modalBody {
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'modalBody': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,106 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
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 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, scrollDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './SelectIndexerFlagsModalContent.css';
class SelectIndexerFlagsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
indexerFlags
} = props;
this.state = {
indexerFlags
};
}
//
// Listeners
onIndexerFlagsChange = ({ value }) => {
this.setState({ indexerFlags: value });
};
onIndexerFlagsSelect = () => {
this.props.onIndexerFlagsSelect(this.state);
};
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
indexerFlags
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Set indexer Flags
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Form>
<FormGroup>
<FormLabel>
{translate('IndexerFlags')}
</FormLabel>
<FormInputGroup
type={inputTypes.INDEXER_FLAGS_SELECT}
name="indexerFlags"
indexerFlags={indexerFlags}
autoFocus={true}
onChange={this.onIndexerFlagsChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onIndexerFlagsSelect}
>
{translate('SetIndexerFlags')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectIndexerFlagsModalContent.propTypes = {
indexerFlags: PropTypes.number.isRequired,
onIndexerFlagsSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectIndexerFlagsModalContent;
@@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
const mapDispatchToProps = {
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchSaveInteractiveImportItems: saveInteractiveImportItem
};
class SelectIndexerFlagsModalContentConnector extends Component {
//
// Listeners
onIndexerFlagsSelect = ({ indexerFlags }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchSaveInteractiveImportItems
} = this.props;
dispatchUpdateInteractiveImportItems({
ids,
indexerFlags
});
dispatchSaveInteractiveImportItems({ ids });
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<SelectIndexerFlagsModalContent
{...this.props}
onIndexerFlagsSelect={this.onIndexerFlagsSelect}
/>
);
}
}
SelectIndexerFlagsModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchSaveInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(SelectIndexerFlagsModalContentConnector);
@@ -20,6 +20,7 @@ import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal'; import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal'; import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal'; import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
@@ -30,7 +31,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
import InteractiveImportRow from './InteractiveImportRow'; import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css'; import styles from './InteractiveImportModalContent.css';
const columns = [ const COLUMNS = [
{ {
name: 'path', name: 'path',
label: 'Path', label: 'Path',
@@ -74,11 +75,21 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{ {
name: 'rejections', name: 'rejections',
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.DANGER, name: icons.DANGER,
kind: kinds.DANGER kind: kinds.DANGER,
title: () => translate('Rejections')
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
@@ -102,6 +113,7 @@ const BOOK = 'book';
const EDITION = 'edition'; const EDITION = 'edition';
const RELEASE_GROUP = 'releaseGroup'; const RELEASE_GROUP = 'releaseGroup';
const QUALITY = 'quality'; const QUALITY = 'quality';
const INDEXER_FLAGS = 'indexerFlags';
const replaceExistingFilesOptions = { const replaceExistingFilesOptions = {
COMBINE: 'combine', COMBINE: 'combine',
@@ -288,6 +300,21 @@ class InteractiveImportModalContent extends Component {
inconsistentBookReleases inconsistentBookReleases
} = this.state; } = this.state;
const allColumns = _.cloneDeep(COLUMNS);
const columns = allColumns.map((column) => {
const showIndexerFlags = items.some((item) => item.indexerFlags);
if (!showIndexerFlags) {
const indexerFlagsColumn = allColumns.find((c) => c.name === 'indexerFlags');
if (indexerFlagsColumn) {
indexerFlagsColumn.isVisible = false;
}
}
return column;
});
const selectedIds = this.getSelectedIds(); const selectedIds = this.getSelectedIds();
const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null; const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null;
const importIdsByBook = _.chain(items).filter((x) => x.book).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value(); const importIdsByBook = _.chain(items).filter((x) => x.book).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value();
@@ -295,11 +322,12 @@ class InteractiveImportModalContent extends Component {
const errorMessage = getErrorMessage(error, 'Unable to load manual import items'); const errorMessage = getErrorMessage(error, 'Unable to load manual import items');
const bulkSelectOptions = [ const bulkSelectOptions = [
{ key: SELECT, value: 'Select...', disabled: true }, { key: SELECT, value: translate('SelectDropdown'), disabled: true },
{ key: BOOK, value: 'Select Book' }, { key: BOOK, value: translate('SelectBook') },
{ key: EDITION, value: 'Select Edition' }, { key: EDITION, value: translate('SelectEdition') },
{ key: QUALITY, value: 'Select Quality' }, { key: QUALITY, value: translate('SelectQuality') },
{ key: RELEASE_GROUP, value: 'Select ReleaseGroup' } { key: RELEASE_GROUP, value: translate('SelectReleaseGroup') },
{ key: INDEXER_FLAGS, value: translate('SelectIndexerFlags') }
]; ];
if (allowAuthorChange) { if (allowAuthorChange) {
@@ -422,6 +450,7 @@ class InteractiveImportModalContent extends Component {
isSaving={isSaving} isSaving={isSaving}
{...item} {...item}
allowAuthorChange={allowAuthorChange} allowAuthorChange={allowAuthorChange}
columns={columns}
onSelectedChange={this.onSelectedChange} onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange} onValidRowChange={this.onValidRowChange}
/> />
@@ -518,6 +547,13 @@ class InteractiveImportModalContent extends Component {
onModalClose={this.onSelectModalClose} onModalClose={this.onSelectModalClose}
/> />
<SelectIndexerFlagsModal
isOpen={selectModalOpen === INDEXER_FLAGS}
ids={selectedIds}
indexerFlags={0}
onModalClose={this.onSelectModalClose}
/>
<ConfirmImportModal <ConfirmImportModal
isOpen={isConfirmImportModalOpen} isOpen={isConfirmImportModalOpen}
books={booksImported} books={booksImported}
@@ -134,6 +134,7 @@ class InteractiveImportModalContentConnector extends Component {
book, book,
foreignEditionId, foreignEditionId,
quality, quality,
indexerFlags,
disableReleaseSwitching disableReleaseSwitching
} = item; } = item;
@@ -158,6 +159,7 @@ class InteractiveImportModalContentConnector extends Component {
bookId: book.id, bookId: book.id,
foreignEditionId, foreignEditionId,
quality, quality,
indexerFlags,
downloadId: this.props.downloadId, downloadId: this.props.downloadId,
disableReleaseSwitching disableReleaseSwitching
}); });
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import BookFormats from 'Book/BookFormats'; import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality'; import BookQuality from 'Book/BookQuality';
import IndexerFlags from 'Book/IndexerFlags';
import FileDetails from 'BookFile/FileDetails'; import FileDetails from 'BookFile/FileDetails';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -14,6 +15,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal'; import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal'; import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
@@ -34,7 +36,8 @@ class InteractiveImportRow extends Component {
isSelectAuthorModalOpen: false, isSelectAuthorModalOpen: false,
isSelectBookModalOpen: false, isSelectBookModalOpen: false,
isSelectReleaseGroupModalOpen: false, isSelectReleaseGroupModalOpen: false,
isSelectQualityModalOpen: false isSelectQualityModalOpen: false,
isSelectIndexerFlagsModalOpen: false
}; };
} }
@@ -133,6 +136,10 @@ class InteractiveImportRow extends Component {
this.setState({ isSelectQualityModalOpen: true }); this.setState({ isSelectQualityModalOpen: true });
}; };
onSelectIndexerFlagsPress = () => {
this.setState({ isSelectIndexerFlagsModalOpen: true });
};
onSelectAuthorModalClose = (changed) => { onSelectAuthorModalClose = (changed) => {
this.setState({ isSelectAuthorModalOpen: false }); this.setState({ isSelectAuthorModalOpen: false });
this.selectRowAfterChange(changed); this.selectRowAfterChange(changed);
@@ -153,6 +160,11 @@ class InteractiveImportRow extends Component {
this.selectRowAfterChange(changed); this.selectRowAfterChange(changed);
}; };
onSelectIndexerFlagsModalClose = (changed) => {
this.setState({ isSelectIndexerFlagsModalOpen: false });
this.selectRowAfterChange(changed);
};
// //
// Render // Render
@@ -167,7 +179,9 @@ class InteractiveImportRow extends Component {
releaseGroup, releaseGroup,
size, size,
customFormats, customFormats,
indexerFlags,
rejections, rejections,
columns,
additionalFile, additionalFile,
isSelected, isSelected,
isReprocessing, isReprocessing,
@@ -180,7 +194,8 @@ class InteractiveImportRow extends Component {
isSelectAuthorModalOpen, isSelectAuthorModalOpen,
isSelectBookModalOpen, isSelectBookModalOpen,
isSelectReleaseGroupModalOpen, isSelectReleaseGroupModalOpen,
isSelectQualityModalOpen isSelectQualityModalOpen,
isSelectIndexerFlagsModalOpen
} = this.state; } = this.state;
const authorName = author ? author.authorName : ''; const authorName = author ? author.authorName : '';
@@ -193,6 +208,7 @@ class InteractiveImportRow extends Component {
const showBookNumberPlaceholder = !isReprocessing && isSelected && !!author && !book; const showBookNumberPlaceholder = !isReprocessing && isSelected && !!author && !book;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup; const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality; const showQualityPlaceholder = isSelected && !quality;
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
const pathCellContents = ( const pathCellContents = (
<div onClick={this.onDetailsPress}> <div onClick={this.onDetailsPress}>
@@ -215,6 +231,8 @@ class InteractiveImportRow extends Component {
/> />
); );
const isIndexerFlagsColumnVisible = columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false;
return ( return (
<TableRow <TableRow
className={additionalFile ? styles.additionalFile : undefined} className={additionalFile ? styles.additionalFile : undefined}
@@ -307,6 +325,28 @@ class InteractiveImportRow extends Component {
} }
</TableRowCell> </TableRowCell>
{isIndexerFlagsColumnVisible ? (
<TableRowCellButton
title={translate('ClickToChangeIndexerFlags')}
onPress={this.onSelectIndexerFlagsPress}
>
{showIndexerFlagsPlaceholder ? (
<InteractiveImportRowCellPlaceholder isOptional={true} />
) : (
<>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</>
)}
</TableRowCellButton>
) : null}
<TableRowCell> <TableRowCell>
{ {
rejections.length ? rejections.length ?
@@ -378,6 +418,13 @@ class InteractiveImportRow extends Component {
real={quality ? quality.revision.real > 0 : false} real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose} onModalClose={this.onSelectQualityModalClose}
/> />
<SelectIndexerFlagsModal
isOpen={isSelectIndexerFlagsModalOpen}
ids={[id]}
indexerFlags={indexerFlags ?? 0}
onModalClose={this.onSelectIndexerFlagsModalClose}
/>
</TableRow> </TableRow>
); );
} }
@@ -395,7 +442,9 @@ InteractiveImportRow.propTypes = {
quality: PropTypes.object, quality: PropTypes.object,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object), customFormats: PropTypes.arrayOf(PropTypes.object),
indexerFlags: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired, rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
audioTags: PropTypes.object.isRequired, audioTags: PropTypes.object.isRequired,
additionalFile: PropTypes.bool.isRequired, additionalFile: PropTypes.bool.isRequired,
isReprocessing: PropTypes.bool, isReprocessing: PropTypes.bool,
@@ -48,7 +48,7 @@ class InteractiveImportModal extends Component {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
size={sizes.EXTRA_LARGE} size={sizes.EXTRA_EXTRA_LARGE}
closeOnBackgroundClick={false} closeOnBackgroundClick={false}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
@@ -62,6 +62,15 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{ {
name: 'rejections', name: 'rejections',
label: React.createElement(Icon, { label: React.createElement(Icon, {
@@ -8,12 +8,11 @@
width: 80px; width: 80px;
} }
.title { .titleContent {
composes: cell; display: flex;
} align-items: center;
justify-content: space-between;
.title div { word-break: break-all;
overflow-wrap: break-word;
} }
.indexer { .indexer {
@@ -41,6 +40,7 @@
} }
.rejected, .rejected,
.indexerFlags,
.download { .download {
composes: cell; composes: cell;
@@ -6,12 +6,13 @@ interface CssExports {
'customFormatScore': string; 'customFormatScore': string;
'download': string; 'download': string;
'indexer': string; 'indexer': string;
'indexerFlags': string;
'peers': string; 'peers': string;
'protocol': string; 'protocol': string;
'quality': string; 'quality': string;
'rejected': string; 'rejected': string;
'size': string; 'size': string;
'title': string; 'titleContent': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import BookFormats from 'Book/BookFormats'; import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality'; import BookQuality from 'Book/BookQuality';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -129,6 +130,7 @@ class InteractiveSearchRow extends Component {
quality, quality,
customFormatScore, customFormatScore,
customFormats, customFormats,
indexerFlags = 0,
rejections, rejections,
downloadAllowed, downloadAllowed,
isGrabbing, isGrabbing,
@@ -153,10 +155,12 @@ class InteractiveSearchRow extends Component {
{formatAge(age, ageHours, ageMinutes)} {formatAge(age, ageHours, ageMinutes)}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.title}> <TableRowCell>
<Link to={infoUrl}> <div className={styles.titleContent}>
{title} <Link to={infoUrl}>
</Link> {title}
</Link>
</div>
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.indexer}> <TableRowCell className={styles.indexer}>
@@ -187,10 +191,21 @@ class InteractiveSearchRow extends Component {
formatCustomFormatScore(customFormatScore, customFormats.length) formatCustomFormatScore(customFormatScore, customFormats.length)
} }
tooltip={<BookFormats formats={customFormats} />} tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM} position={tooltipPositions.LEFT}
/> />
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.indexerFlags}>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.rejected}> <TableRowCell className={styles.rejected}>
{ {
!!rejections.length && !!rejections.length &&
@@ -263,6 +278,7 @@ InteractiveSearchRow.propTypes = {
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object), customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired, customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.string).isRequired, rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
downloadAllowed: PropTypes.bool.isRequired, downloadAllowed: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired, isGrabbing: PropTypes.bool.isRequired,
@@ -275,6 +291,7 @@ InteractiveSearchRow.propTypes = {
}; };
InteractiveSearchRow.defaultProps = { InteractiveSearchRow.defaultProps = {
indexerFlags: 0,
rejections: [], rejections: [],
isGrabbing: false, isGrabbing: false,
isGrabbed: false isGrabbed: false
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Card from 'Components/Card'; import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
@@ -150,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
</Form> </Form>
<FieldSet legend={translate('Conditions')}> <FieldSet legend={translate('Conditions')}>
<Alert kind={kinds.INFO}>
<div>
{translate('CustomFormatsSettingsTriggerInfo')}
</div>
</Alert>
<div className={styles.customFormats}> <div className={styles.customFormats}>
{ {
specifications.map((tag) => { specifications.map((tag) => {
@@ -15,6 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditDownloadClientModalContent.css'; import styles from './EditDownloadClientModalContent.css';
@@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component {
onModalClose, onModalClose,
onSavePress, onSavePress,
onTestPress, onTestPress,
onAdvancedSettingsPress,
onDeleteDownloadClientPress, onDeleteDownloadClientPress,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -205,6 +207,12 @@ class EditDownloadClientModalContent extends Component {
</Button> </Button>
} }
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton <SpinnerErrorButton
isSpinning={isTesting} isSpinning={isTesting}
error={saveError} error={saveError}
@@ -245,6 +253,7 @@ EditDownloadClientModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired, onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteDownloadClientPress: PropTypes.func onDeleteDownloadClientPress: PropTypes.func
}; };
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions'; import {
saveDownloadClient,
setDownloadClientFieldValue,
setDownloadClientValue,
testDownloadClient,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditDownloadClientModalContent from './EditDownloadClientModalContent'; import EditDownloadClientModalContent from './EditDownloadClientModalContent';
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
setDownloadClientValue, setDownloadClientValue,
setDownloadClientFieldValue, setDownloadClientFieldValue,
saveDownloadClient, saveDownloadClient,
testDownloadClient testDownloadClient,
toggleAdvancedSettings
}; };
class EditDownloadClientModalContentConnector extends Component { class EditDownloadClientModalContentConnector extends Component {
@@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component {
this.props.testDownloadClient({ id: this.props.id }); this.props.testDownloadClient({ id: this.props.id });
}; };
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
// //
// Render // Render
@@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component {
{...this.props} {...this.props}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
onTestPress={this.onTestPress} onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange} onFieldChange={this.onFieldChange}
/> />
@@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = {
setDownloadClientFieldValue: PropTypes.func.isRequired, setDownloadClientFieldValue: PropTypes.func.isRequired,
saveDownloadClient: PropTypes.func.isRequired, saveDownloadClient: PropTypes.func.isRequired,
testDownloadClient: PropTypes.func.isRequired, testDownloadClient: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
@@ -32,7 +32,7 @@ const enableOptions = [
get value() { get value() {
return translate('NoChange'); return translate('NoChange');
}, },
disabled: true, isDisabled: true,
}, },
{ {
key: 'enabled', key: 'enabled',
@@ -20,6 +20,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan'; import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditImportListModalContent.css'; import styles from './EditImportListModalContent.css';
@@ -66,6 +67,7 @@ function EditImportListModalContent(props) {
onModalClose, onModalClose,
onSavePress, onSavePress,
onTestPress, onTestPress,
onAdvancedSettingsPress,
onDeleteImportListPress, onDeleteImportListPress,
showMetadataProfile, showMetadataProfile,
...otherProps ...otherProps
@@ -332,6 +334,12 @@ function EditImportListModalContent(props) {
</Button> </Button>
} }
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton <SpinnerErrorButton
isSpinning={isTesting} isSpinning={isTesting}
error={saveError} error={saveError}
@@ -372,6 +380,7 @@ EditImportListModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired, onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteImportListPress: PropTypes.func onDeleteImportListPress: PropTypes.func
}; };
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions'; import {
saveImportList,
setImportListFieldValue,
setImportListValue,
testImportList,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditImportListModalContent from './EditImportListModalContent'; import EditImportListModalContent from './EditImportListModalContent';
@@ -25,7 +31,8 @@ const mapDispatchToProps = {
setImportListValue, setImportListValue,
setImportListFieldValue, setImportListFieldValue,
saveImportList, saveImportList,
testImportList testImportList,
toggleAdvancedSettings
}; };
class EditImportListModalContentConnector extends Component { class EditImportListModalContentConnector extends Component {
@@ -58,6 +65,10 @@ class EditImportListModalContentConnector extends Component {
this.props.testImportList({ id: this.props.id }); this.props.testImportList({ id: this.props.id });
}; };
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
// //
// Render // Render
@@ -67,6 +78,7 @@ class EditImportListModalContentConnector extends Component {
{...this.props} {...this.props}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
onTestPress={this.onTestPress} onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange} onFieldChange={this.onFieldChange}
/> />
@@ -84,6 +96,7 @@ EditImportListModalContentConnector.propTypes = {
setImportListFieldValue: PropTypes.func.isRequired, setImportListFieldValue: PropTypes.func.isRequired,
saveImportList: PropTypes.func.isRequired, saveImportList: PropTypes.func.isRequired,
testImportList: PropTypes.func.isRequired, testImportList: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
@@ -32,7 +32,7 @@ const autoAddOptions = [
get value() { get value() {
return translate('NoChange'); return translate('NoChange');
}, },
disabled: true, isDisabled: true,
}, },
{ {
key: 'enabled', key: 'enabled',
@@ -32,7 +32,7 @@ const enableOptions = [
get value() { get value() {
return translate('NoChange'); return translate('NoChange');
}, },
disabled: true, isDisabled: true,
}, },
{ {
key: 'enabled', key: 'enabled',
@@ -1,6 +1,6 @@
.option { .option {
display: flex; display: flex;
align-items: center; align-items: stretch;
flex-wrap: wrap; flex-wrap: wrap;
margin: 3px; margin: 3px;
border: 1px solid var(--borderColor); border: 1px solid var(--borderColor);
@@ -17,7 +17,7 @@
} }
.small { .small {
width: 460px; width: 490px;
} }
.large { .large {
@@ -26,7 +26,7 @@
.token { .token {
flex: 0 0 50%; flex: 0 0 50%;
padding: 6px 16px; padding: 6px;
background-color: var(--popoverTitleBackgroundColor); background-color: var(--popoverTitleBackgroundColor);
font-family: $monoSpaceFontFamily; font-family: $monoSpaceFontFamily;
} }
@@ -34,9 +34,9 @@
.example { .example {
display: flex; display: flex;
align-items: center; align-items: center;
align-self: stretch; justify-content: space-between;
flex: 0 0 50%; flex: 0 0 50%;
padding: 6px 16px; padding: 6px;
background-color: var(--popoverBodyBackgroundColor); background-color: var(--popoverBodyBackgroundColor);
.footNote { .footNote {
@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import NotificationEventItems from './NotificationEventItems'; import NotificationEventItems from './NotificationEventItems';
import styles from './EditNotificationModalContent.css'; import styles from './EditNotificationModalContent.css';
@@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
onModalClose, onModalClose,
onSavePress, onSavePress,
onTestPress, onTestPress,
onAdvancedSettingsPress,
onDeleteNotificationPress, onDeleteNotificationPress,
...otherProps ...otherProps
} = props; } = props;
@@ -140,6 +142,12 @@ function EditNotificationModalContent(props) {
</Button> </Button>
} }
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton <SpinnerErrorButton
isSpinning={isTesting} isSpinning={isTesting}
error={saveError} error={saveError}
@@ -179,6 +187,7 @@ EditNotificationModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired, onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteNotificationPress: PropTypes.func onDeleteNotificationPress: PropTypes.func
}; };
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions'; import {
saveNotification,
setNotificationFieldValue,
setNotificationValue,
testNotification,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditNotificationModalContent from './EditNotificationModalContent'; import EditNotificationModalContent from './EditNotificationModalContent';
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
setNotificationValue, setNotificationValue,
setNotificationFieldValue, setNotificationFieldValue,
saveNotification, saveNotification,
testNotification testNotification,
toggleAdvancedSettings
}; };
class EditNotificationModalContentConnector extends Component { class EditNotificationModalContentConnector extends Component {
@@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
this.props.testNotification({ id: this.props.id }); this.props.testNotification({ id: this.props.id });
}; };
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
// //
// Render // Render
@@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
{...this.props} {...this.props}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
onTestPress={this.onTestPress} onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange} onFieldChange={this.onFieldChange}
/> />
@@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
setNotificationFieldValue: PropTypes.func.isRequired, setNotificationFieldValue: PropTypes.func.isRequired,
saveNotification: PropTypes.func.isRequired, saveNotification: PropTypes.func.isRequired,
testNotification: PropTypes.func.isRequired, testNotification: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
+10 -4
View File
@@ -14,12 +14,17 @@ function PendingChangesModal(props) {
isOpen, isOpen,
onConfirm, onConfirm,
onCancel, onCancel,
bindShortcut bindShortcut,
unbindShortcut
} = props; } = props;
useEffect(() => { useEffect(() => {
bindShortcut('enter', onConfirm); if (isOpen) {
}, [bindShortcut, onConfirm]); bindShortcut('enter', onConfirm);
return () => unbindShortcut('enter', onConfirm);
}
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
return ( return (
<Modal <Modal
@@ -60,7 +65,8 @@ PendingChangesModal.propTypes = {
kind: PropTypes.oneOf(kinds.all), kind: PropTypes.oneOf(kinds.all),
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
bindShortcut: PropTypes.func.isRequired bindShortcut: PropTypes.func.isRequired,
unbindShortcut: PropTypes.func.isRequired
}; };
PendingChangesModal.defaultProps = { PendingChangesModal.defaultProps = {
+5 -1
View File
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
import { fetchTagDetails } from 'Store/Actions/tagActions'; import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
import Tags from './Tags'; import Tags from './Tags';
function createMapStateToProps() { function createMapStateToProps() {
@@ -25,6 +25,7 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
dispatchFetchTags: fetchTags,
dispatchFetchTagDetails: fetchTagDetails, dispatchFetchTagDetails: fetchTagDetails,
dispatchFetchDelayProfiles: fetchDelayProfiles, dispatchFetchDelayProfiles: fetchDelayProfiles,
dispatchFetchImportLists: fetchImportLists, dispatchFetchImportLists: fetchImportLists,
@@ -41,6 +42,7 @@ class MetadatasConnector extends Component {
componentDidMount() { componentDidMount() {
const { const {
dispatchFetchTags,
dispatchFetchTagDetails, dispatchFetchTagDetails,
dispatchFetchDelayProfiles, dispatchFetchDelayProfiles,
dispatchFetchImportLists, dispatchFetchImportLists,
@@ -50,6 +52,7 @@ class MetadatasConnector extends Component {
dispatchFetchDownloadClients dispatchFetchDownloadClients
} = this.props; } = this.props;
dispatchFetchTags();
dispatchFetchTagDetails(); dispatchFetchTagDetails();
dispatchFetchDelayProfiles(); dispatchFetchDelayProfiles();
dispatchFetchImportLists(); dispatchFetchImportLists();
@@ -72,6 +75,7 @@ class MetadatasConnector extends Component {
} }
MetadatasConnector.propTypes = { MetadatasConnector.propTypes = {
dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchTagDetails: PropTypes.func.isRequired, dispatchFetchTagDetails: PropTypes.func.isRequired,
dispatchFetchDelayProfiles: PropTypes.func.isRequired, dispatchFetchDelayProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired,
+10 -10
View File
@@ -21,19 +21,19 @@ export const firstDayOfWeekOptions = [
]; ];
export const weekColumnOptions = [ export const weekColumnOptions = [
{ key: 'ddd M/D', value: 'Tue 3/25' }, { key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' },
{ key: 'ddd MM/DD', value: 'Tue 03/25' }, { key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' },
{ key: 'ddd D/M', value: 'Tue 25/3' }, { key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' },
{ key: 'ddd DD/MM', value: 'Tue 25/03' } { key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' }
]; ];
const shortDateFormatOptions = [ const shortDateFormatOptions = [
{ key: 'MMM D YYYY', value: 'Mar 25 2014' }, { key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' },
{ key: 'DD MMM YYYY', value: '25 Mar 2014' }, { key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' },
{ key: 'MM/D/YYYY', value: '03/25/2014' }, { key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' },
{ key: 'MM/DD/YYYY', value: '03/25/2014' }, { key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' },
{ key: 'DD/MM/YYYY', value: '25/03/2014' }, { key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' },
{ key: 'YYYY-MM-DD', value: '2014-03-25' } { key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' }
]; ];
const longDateFormatOptions = [ const longDateFormatOptions = [
@@ -0,0 +1,48 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.indexerFlags';
//
// Actions Types
export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags';
//
// Action Creators
export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
//
// Action Handlers
actionHandlers: {
[FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag')
},
//
// Reducers
reducers: {
}
};
+12 -1
View File
@@ -1,9 +1,11 @@
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import React from 'react';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import bookEntities from 'Book/bookEntities'; import bookEntities from 'Book/bookEntities';
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; import Icon from 'Components/Icon';
import { filterTypePredicates, filterTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
@@ -243,6 +245,15 @@ export const defaultState = {
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'indexerFlags',
columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isVisible: false
},
{ {
name: 'status', name: 'status',
label: 'Status', label: 'Status',
@@ -207,6 +207,7 @@ export const actionHandlers = handleThunks({
foreignEditionId: item.foreignEditionId ? item.ForeignEditionId : undefined, foreignEditionId: item.foreignEditionId ? item.ForeignEditionId : undefined,
quality: item.quality, quality: item.quality,
releaseGroup: item.releaseGroup, releaseGroup: item.releaseGroup,
indexerFlags: item.indexerFlags,
downloadId: item.downloadId, downloadId: item.downloadId,
additionalFile: item.additionalFile, additionalFile: item.additionalFile,
replaceExistingFiles: item.replaceExistingFiles, replaceExistingFiles: item.replaceExistingFiles,
@@ -10,6 +10,7 @@ import downloadClients from './Settings/downloadClients';
import general from './Settings/general'; import general from './Settings/general';
import importListExclusions from './Settings/importListExclusions'; import importListExclusions from './Settings/importListExclusions';
import importLists from './Settings/importLists'; import importLists from './Settings/importLists';
import indexerFlags from './Settings/indexerFlags';
import indexerOptions from './Settings/indexerOptions'; import indexerOptions from './Settings/indexerOptions';
import indexers from './Settings/indexers'; import indexers from './Settings/indexers';
import languages from './Settings/languages'; import languages from './Settings/languages';
@@ -35,6 +36,7 @@ export * from './Settings/downloadClientOptions';
export * from './Settings/general'; export * from './Settings/general';
export * from './Settings/importLists'; export * from './Settings/importLists';
export * from './Settings/importListExclusions'; export * from './Settings/importListExclusions';
export * from './Settings/indexerFlags';
export * from './Settings/indexerOptions'; export * from './Settings/indexerOptions';
export * from './Settings/indexers'; export * from './Settings/indexers';
export * from './Settings/languages'; export * from './Settings/languages';
@@ -70,6 +72,7 @@ export const defaultState = {
downloadClients: downloadClients.defaultState, downloadClients: downloadClients.defaultState,
downloadClientOptions: downloadClientOptions.defaultState, downloadClientOptions: downloadClientOptions.defaultState,
general: general.defaultState, general: general.defaultState,
indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState, indexerOptions: indexerOptions.defaultState,
indexers: indexers.defaultState, indexers: indexers.defaultState,
importLists: importLists.defaultState, importLists: importLists.defaultState,
@@ -115,6 +118,7 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers, ...downloadClients.actionHandlers,
...downloadClientOptions.actionHandlers, ...downloadClientOptions.actionHandlers,
...general.actionHandlers, ...general.actionHandlers,
...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers, ...indexerOptions.actionHandlers,
...indexers.actionHandlers, ...indexers.actionHandlers,
...importLists.actionHandlers, ...importLists.actionHandlers,
@@ -151,6 +155,7 @@ export const reducers = createHandleActions({
...downloadClients.reducers, ...downloadClients.reducers,
...downloadClientOptions.reducers, ...downloadClientOptions.reducers,
...general.reducers, ...general.reducers,
...indexerFlags.reducers,
...indexerOptions.reducers, ...indexerOptions.reducers,
...indexers.reducers, ...indexers.reducers,
...importLists.reducers, ...importLists.reducers,
@@ -0,0 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
const createIndexerFlagsSelector = createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => indexerFlags
);
export default createIndexerFlagsSelector;
@@ -0,0 +1,23 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Author from 'Author/Author';
function createMultiAuthorsSelector(authorIds: number[]) {
return createSelector(
(state: AppState) => state.authors.itemMap,
(state: AppState) => state.authors.items,
(itemMap, allAuthors) => {
return authorIds.reduce((acc: Author[], authorId) => {
const author = allAuthors[itemMap[authorId]];
if (author) {
acc.push(author);
}
return acc;
}, []);
}
);
}
export default createMultiAuthorsSelector;
@@ -10,15 +10,6 @@
width: 100%; width: 100%;
} }
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}
.queued, .queued,
.started, .started,
.ended { .ended {
@@ -2,14 +2,12 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'commandName': string;
'duration': string; 'duration': string;
'ended': string; 'ended': string;
'queued': string; 'queued': string;
'started': string; 'started': string;
'trigger': string; 'trigger': string;
'triggerContent': string; 'triggerContent': string;
'userAgent': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -1,279 +0,0 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import styles from './QueuedTaskRow.css';
function getStatusIconProps(status, message) {
const title = titleCase(status);
switch (status) {
case 'queued':
return {
name: icons.PENDING,
title
};
case 'started':
return {
name: icons.REFRESH,
isSpinning: true,
title
};
case 'completed':
return {
name: icons.CHECK,
kind: kinds.SUCCESS,
title: message === 'Completed' ? title : `${title}: ${message}`
};
case 'failed':
return {
name: icons.FATAL,
kind: kinds.DANGER,
title: `${title}: ${message}`
};
default:
return {
name: icons.UNKNOWN,
title
};
}
}
function getFormattedDates(props) {
const {
queued,
started,
ended,
showRelativeDates,
shortDateFormat
} = props;
if (showRelativeDates) {
return {
queuedAt: moment(queued).fromNow(),
startedAt: started ? moment(started).fromNow() : '-',
endedAt: ended ? moment(ended).fromNow() : '-'
};
}
return {
queuedAt: formatDate(queued, shortDateFormat),
startedAt: started ? formatDate(started, shortDateFormat) : '-',
endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
};
}
class QueuedTaskRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
...getFormattedDates(props),
isCancelConfirmModalOpen: false
};
this._updateTimeoutId = null;
}
componentDidMount() {
this.setUpdateTimer();
}
componentDidUpdate(prevProps) {
const {
queued,
started,
ended
} = this.props;
if (
queued !== prevProps.queued ||
started !== prevProps.started ||
ended !== prevProps.ended
) {
this.setState(getFormattedDates(this.props));
}
}
componentWillUnmount() {
if (this._updateTimeoutId) {
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
}
}
//
// Control
setUpdateTimer() {
this._updateTimeoutId = setTimeout(() => {
this.setState(getFormattedDates(this.props));
this.setUpdateTimer();
}, 30000);
}
//
// Listeners
onCancelPress = () => {
this.setState({
isCancelConfirmModalOpen: true
});
};
onAbortCancel = () => {
this.setState({
isCancelConfirmModalOpen: false
});
};
//
// Render
render() {
const {
trigger,
commandName,
queued,
started,
ended,
status,
duration,
message,
clientUserAgent,
longDateFormat,
timeFormat,
onCancelPress
} = this.props;
const {
queuedAt,
startedAt,
endedAt,
isCancelConfirmModalOpen
} = this.state;
let triggerIcon = icons.QUICK;
if (trigger === 'manual') {
triggerIcon = icons.INTERACTIVE;
} else if (trigger === 'scheduled') {
triggerIcon = icons.SCHEDULED;
}
return (
<TableRow>
<TableRowCell className={styles.trigger}>
<span className={styles.triggerContent}>
<Icon
name={triggerIcon}
title={titleCase(trigger)}
/>
<Icon
{...getStatusIconProps(status, message)}
/>
</span>
</TableRowCell>
<TableRowCell>
<span className={styles.commandName}>
{commandName}
</span>
{
clientUserAgent ?
<span className={styles.userAgent} title={translate('UserAgentProvidedByTheAppThatCalledTheAPI')}>
from: {clientUserAgent}
</span> :
null
}
</TableRowCell>
<TableRowCell
className={styles.queued}
title={formatDateTime(queued, longDateFormat, timeFormat)}
>
{queuedAt}
</TableRowCell>
<TableRowCell
className={styles.started}
title={formatDateTime(started, longDateFormat, timeFormat)}
>
{startedAt}
</TableRowCell>
<TableRowCell
className={styles.ended}
title={formatDateTime(ended, longDateFormat, timeFormat)}
>
{endedAt}
</TableRowCell>
<TableRowCell className={styles.duration}>
{formatTimeSpan(duration)}
</TableRowCell>
<TableRowCell
className={styles.actions}
>
{
status === 'queued' &&
<IconButton
title={translate('RemovedFromTaskQueue')}
name={icons.REMOVE}
onPress={this.onCancelPress}
/>
}
</TableRowCell>
<ConfirmModal
isOpen={isCancelConfirmModalOpen}
kind={kinds.DANGER}
title={translate('Cancel')}
message={translate('CancelMessageText')}
confirmLabel={translate('YesCancel')}
cancelLabel={translate('NoLeaveIt')}
onConfirm={onCancelPress}
onCancel={this.onAbortCancel}
/>
</TableRow>
);
}
}
QueuedTaskRow.propTypes = {
trigger: PropTypes.string.isRequired,
commandName: PropTypes.string.isRequired,
queued: PropTypes.string.isRequired,
started: PropTypes.string,
ended: PropTypes.string,
status: PropTypes.string.isRequired,
duration: PropTypes.string,
message: PropTypes.string,
clientUserAgent: PropTypes.string,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onCancelPress: PropTypes.func.isRequired
};
export default QueuedTaskRow;
@@ -0,0 +1,238 @@
import moment from 'moment';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds } from 'Helpers/Props';
import { cancelCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
import styles from './QueuedTaskRow.css';
function getStatusIconProps(status: string, message: string | undefined) {
const title = titleCase(status);
switch (status) {
case 'queued':
return {
name: icons.PENDING,
title,
};
case 'started':
return {
name: icons.REFRESH,
isSpinning: true,
title,
};
case 'completed':
return {
name: icons.CHECK,
kind: kinds.SUCCESS,
title: message === 'Completed' ? title : `${title}: ${message}`,
};
case 'failed':
return {
name: icons.FATAL,
kind: kinds.DANGER,
title: `${title}: ${message}`,
};
default:
return {
name: icons.UNKNOWN,
title,
};
}
}
function getFormattedDates(
queued: string,
started: string | undefined,
ended: string | undefined,
showRelativeDates: boolean,
shortDateFormat: string
) {
if (showRelativeDates) {
return {
queuedAt: moment(queued).fromNow(),
startedAt: started ? moment(started).fromNow() : '-',
endedAt: ended ? moment(ended).fromNow() : '-',
};
}
return {
queuedAt: formatDate(queued, shortDateFormat),
startedAt: started ? formatDate(started, shortDateFormat) : '-',
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
};
}
interface QueuedTimes {
queuedAt: string;
startedAt: string;
endedAt: string;
}
export interface QueuedTaskRowProps {
id: number;
trigger: string;
commandName: string;
queued: string;
started?: string;
ended?: string;
status: string;
duration?: string;
message?: string;
body: CommandBody;
clientUserAgent?: string;
}
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
const {
id,
trigger,
commandName,
queued,
started,
ended,
status,
duration,
message,
body,
clientUserAgent,
} = props;
const dispatch = useDispatch();
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
useSelector(createUISettingsSelector());
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
null
);
const [times, setTimes] = useState<QueuedTimes>(
getFormattedDates(
queued,
started,
ended,
showRelativeDates,
shortDateFormat
)
);
const [
isCancelConfirmModalOpen,
openCancelConfirmModal,
closeCancelConfirmModal,
] = useModalOpenState(false);
const handleCancelPress = useCallback(() => {
dispatch(cancelCommand({ id }));
}, [id, dispatch]);
useEffect(() => {
updateTimeTimeoutId.current = setTimeout(() => {
setTimes(
getFormattedDates(
queued,
started,
ended,
showRelativeDates,
shortDateFormat
)
);
}, 30000);
return () => {
if (updateTimeTimeoutId.current) {
clearTimeout(updateTimeTimeoutId.current);
}
};
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
const { queuedAt, startedAt, endedAt } = times;
let triggerIcon = icons.QUICK;
if (trigger === 'manual') {
triggerIcon = icons.INTERACTIVE;
} else if (trigger === 'scheduled') {
triggerIcon = icons.SCHEDULED;
}
return (
<TableRow>
<TableRowCell className={styles.trigger}>
<span className={styles.triggerContent}>
<Icon name={triggerIcon} title={titleCase(trigger)} />
<Icon {...getStatusIconProps(status, message)} />
</span>
</TableRowCell>
<QueuedTaskRowNameCell
commandName={commandName}
body={body}
clientUserAgent={clientUserAgent}
/>
<TableRowCell
className={styles.queued}
title={formatDateTime(queued, longDateFormat, timeFormat)}
>
{queuedAt}
</TableRowCell>
<TableRowCell
className={styles.started}
title={formatDateTime(started, longDateFormat, timeFormat)}
>
{startedAt}
</TableRowCell>
<TableRowCell
className={styles.ended}
title={formatDateTime(ended, longDateFormat, timeFormat)}
>
{endedAt}
</TableRowCell>
<TableRowCell className={styles.duration}>
{formatTimeSpan(duration)}
</TableRowCell>
<TableRowCell className={styles.actions}>
{status === 'queued' && (
<IconButton
title={translate('RemovedFromTaskQueue')}
name={icons.REMOVE}
onPress={openCancelConfirmModal}
/>
)}
</TableRowCell>
<ConfirmModal
isOpen={isCancelConfirmModalOpen}
kind={kinds.DANGER}
title={translate('Cancel')}
message={translate('CancelPendingTask')}
confirmLabel={translate('YesCancel')}
cancelLabel={translate('NoLeaveIt')}
onConfirm={handleCancelPress}
onCancel={closeCancelConfirmModal}
/>
</TableRow>
);
}
@@ -1,31 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { cancelCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import QueuedTaskRow from './QueuedTaskRow';
function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onCancelPress() {
dispatch(cancelCommand({
id: props.id
}));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
@@ -0,0 +1,8 @@
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}
@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'commandName': string;
'userAgent': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,49 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import createMultiAuthorsSelector from 'Store/Selectors/createMultiAuthorsSelector';
import translate from 'Utilities/String/translate';
import styles from './QueuedTaskRowNameCell.css';
export interface QueuedTaskRowNameCellProps {
commandName: string;
body: CommandBody;
clientUserAgent?: string;
}
export default function QueuedTaskRowNameCell(
props: QueuedTaskRowNameCellProps
) {
const { commandName, body, clientUserAgent } = props;
const movieIds = [...(body.authorIds ?? [])];
if (body.authorId) {
movieIds.push(body.authorId);
}
const authors = useSelector(createMultiAuthorsSelector(movieIds));
const sortedAuthors = authors.sort((a, b) =>
a.sortName.localeCompare(b.sortName)
);
return (
<TableRowCell>
<span className={styles.commandName}>
{commandName}
{sortedAuthors.length ? (
<span> - {sortedAuthors.map((a) => a.authorName).join(', ')}</span>
) : null}
</span>
{clientUserAgent ? (
<span
className={styles.userAgent}
title={translate('TaskUserAgentTooltip')}
>
{translate('From')}: {clientUserAgent}
</span>
) : null}
</TableRowCell>
);
}
@@ -1,90 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import translate from 'Utilities/String/translate';
import QueuedTaskRowConnector from './QueuedTaskRowConnector';
const columns = [
{
name: 'trigger',
label: '',
isVisible: true
},
{
name: 'commandName',
label: () => translate('Name'),
isVisible: true
},
{
name: 'queued',
label: () => translate('Queued'),
isVisible: true
},
{
name: 'started',
label: () => translate('Started'),
isVisible: true
},
{
name: 'ended',
label: () => translate('Ended'),
isVisible: true
},
{
name: 'duration',
label: () => translate('Duration'),
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
function QueuedTasks(props) {
const {
isFetching,
isPopulated,
items
} = props;
return (
<FieldSet legend={translate('Queue')}>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
isPopulated &&
<Table
columns={columns}
>
<TableBody>
{
items.map((item) => {
return (
<QueuedTaskRowConnector
key={item.id}
{...item}
/>
);
})
}
</TableBody>
</Table>
}
</FieldSet>
);
}
QueuedTasks.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
items: PropTypes.array.isRequired
};
export default QueuedTasks;
@@ -0,0 +1,74 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { fetchCommands } from 'Store/Actions/commandActions';
import translate from 'Utilities/String/translate';
import QueuedTaskRow from './QueuedTaskRow';
const columns = [
{
name: 'trigger',
label: '',
isVisible: true,
},
{
name: 'commandName',
label: () => translate('Name'),
isVisible: true,
},
{
name: 'queued',
label: () => translate('Queued'),
isVisible: true,
},
{
name: 'started',
label: () => translate('Started'),
isVisible: true,
},
{
name: 'ended',
label: () => translate('Ended'),
isVisible: true,
},
{
name: 'duration',
label: () => translate('Duration'),
isVisible: true,
},
{
name: 'actions',
isVisible: true,
},
];
export default function QueuedTasks() {
const dispatch = useDispatch();
const { isFetching, isPopulated, items } = useSelector(
(state: AppState) => state.commands
);
useEffect(() => {
dispatch(fetchCommands());
}, [dispatch]);
return (
<FieldSet legend={translate('Queue')}>
{isFetching && !isPopulated && <LoadingIndicator />}
{isPopulated && (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
return <QueuedTaskRow key={item.id} {...item} />;
})}
</TableBody>
</Table>
)}
</FieldSet>
);
}
@@ -1,46 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchCommands } from 'Store/Actions/commandActions';
import QueuedTasks from './QueuedTasks';
function createMapStateToProps() {
return createSelector(
(state) => state.commands,
(commands) => {
return commands;
}
);
}
const mapDispatchToProps = {
dispatchFetchCommands: fetchCommands
};
class QueuedTasksConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchCommands();
}
//
// Render
render() {
return (
<QueuedTasks
{...this.props}
/>
);
}
}
QueuedTasksConnector.propTypes = {
dispatchFetchCommands: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);

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