Compare commits

...

124 Commits

Author SHA1 Message Date
Robin Dadswell
d0952d4a0c fixed small test issue 2021-01-19 19:06:48 -05:00
Robin Dadswell
579f1eae2d fixed health check selector 2021-01-19 19:06:48 -05:00
Robin Dadswell
091ebb5cee more linting fixes 2021-01-19 19:06:48 -05:00
Robin Dadswell
d3a96fac12 added missing css file 2021-01-19 19:06:47 -05:00
Robin Dadswell
da0b60dd96 Updated URL to readarr not lidarr 2021-01-19 19:06:47 -05:00
ta264
3bfdd4aafa New: Warn if UI won't update due to SignalR errors
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:47 -05:00
Robin Dadswell
da2bc75eab Most linting fixes completed 2021-01-19 19:06:47 -05:00
Robin Dadswell
1d922f3ce6 final backend fix 2021-01-19 19:06:47 -05:00
Robin Dadswell
b3bdaeeffa fixed RootFolderChecks 2021-01-19 19:06:47 -05:00
Qstick
604c354284 New: Event Driven HealthCheck Support
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:47 -05:00
Qstick
ec0fc6f3e1 Fixed: Edge case where import fails due to DB relationship mismatch
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:47 -05:00
Robin Dadswell
2d28359627 more backend build fixes 2021-01-19 19:06:47 -05:00
Robin Dadswell
45d0f4bdb7 fixes to build errors 2021-01-19 19:06:47 -05:00
Robin Dadswell
ba1f2a5a7b more build fixes 2021-01-19 19:06:47 -05:00
Robin Dadswell
0ef8f2b2f7 finished fixing IsUpgradable 2021-01-19 19:06:47 -05:00
Robin Dadswell
91d6fe90cd minor fixes to build - half finished 2021-01-19 19:06:47 -05:00
Robin Dadswell
baa728f2c1 removed additional whitespace 2021-01-19 19:06:47 -05:00
Robin Dadswell
d20f9ea269 fixed seriesActions 2021-01-19 19:06:47 -05:00
Robin Dadswell
db809f579c removed get columns as no longer user 2021-01-19 19:06:47 -05:00
Robin Dadswell
4970ed5323 fixed authoreditor 2021-01-19 19:06:47 -05:00
Robin Dadswell
fa653bf546 health check selector fix 2021-01-19 19:06:47 -05:00
Robin Dadswell
f496d96907 removed striphtml (as per lidarr) 2021-01-19 19:06:47 -05:00
Robin Dadswell
cfc52d12ea move to PageContentBody from PageContentBodyConnector 2021-01-19 19:06:47 -05:00
Robin Dadswell
40f156c16e regenerated lock file 2021-01-19 19:06:47 -05:00
bakerboy448
a322d9ed6d lint fixes 2021-01-19 19:06:46 -05:00
ta264
604666f961 Add SortKey validation
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
851ff948af Fixed: False Positives for RemotePath check with Deluge
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
b6f052f39c Fixed: Show TLS errors in UI when testing download clients
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
e6236823cf Resource missing from Gotify call
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
32c759d32b Gotify token as query parameter
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
993d5818a4 Convert Notifications from RestSharp to HttpClient
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
2b93aaa9b2 Fixed: Manual Import Fails on failed Import Items
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
4ec873bf7a Fixed binary execute permissions for osx and Radarr
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
b574048733 Fixed disk permission tests
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
5c4dfc5e7b New: Displaying folder-based permissions in UI rather than file-based permissions and with selectable sane presets
Fixed: Preserve setgid when applying unix permissions
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
5545ae94ac Readded 0 cat to the end of the Newznab list
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
15e0b8dd4d Improve use of All() for Path related queries
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
ac4a82abf4 Mass Editor size and options
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
cb07186970 Fixed: Size on disk sorting and display
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
e5409dbff6 New: Differentiate between short term and long term (more than 6 hours) indexer failures
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
59d6a77b74 Fixed: (Windows) clean up extraneous files in build folder during installation
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
db0151c39b New: Bulk remove from Blacklist
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
4ac646685c New: Show .net version in UI
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
d01d112bfe Fixed: Cleanse account and passwd from Download Station URLs
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
77ef282916 Fixed: Webhooks using lower case event types (in the future this could change)
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
e5f483eadc New: Health events for Webhooks
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
8e78bf71a1 Fixed: Failing file copy when running in docker on synology with btrfs
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
0c581c6146 Fixed: Regression causing updater to fail (manual update required if on 3.0.3.971, see forums)
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
cabec3c625 Fixed: Dataloss when moving series folder to root folder with only different casing
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth
a0ef1ebaad New: Newznab/Torznab categories dropdown with indexer provided category names
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
5abfee1bf8 Handle ratelimit api response for newznab caps endpoint on certain newznab indexers that have caps behind the apikey
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
f46d5534f4 Fixed: Preview rename tip wording
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
35225c799b Fixed: Artist/Album navigation buttons hidden with some titles
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
89584666ff Fixed: Links and already added icons overflowing on add artist/album search results
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
a776336b8c Fixed: Exception when parsing Quality in release title with colon
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
39a0cb3f43 Fixed: Long paths overflowing in artist history
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Mark McDowall
2abee1970f Don't process queue item without details
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Mark McDowall
cdf648670d Fixed: Show more information in UI when testing SAB fails in some cases
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Ryan
810a02090b Fixed: Typo/unclear text in backup retention
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
4282b84fb6 Remove stacktrace if hardlink resulted in EXDEV.
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
42acea55fd Fixed: Performance of symbolic link detection and infinite recursion
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
c7e7d48ca3 New: Fast copy using reflink on btrfs volumes
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
b1fbdebba9 Fixed: Removed hardlink-based transactional file transfer logic (instead relying on explicit copy+delete for cifs)
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
b4bfd7d07f Fixed: Sorting of queue by artist title when unknown items are included
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
470ba66d4a Moved Windows-only Permission function to Lidarr.Windows
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
400fa4a960 fix modifiers for various classes
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Mark McDowall
e9de0420dd Fixed: Indexer being disabled due to download client rejecting it
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Taloth Saldono
56a623bad9 Added PrivacyLevel option to FieldDefinition for later usage
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Taloth Saldono
26bf0c0542 Added MultiSelect input control for provider settings
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Taloth Saldono
33de5d2049 New: Added FileList.io indexer support
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Arthur Bols
1e3b775837 New: Removed chown and simplified chmod options for linux/osx
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Taloth Saldono
efa77c1823 Allow inline markdown in the changelog for linking to wiki
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Qstick
3428ee3ca2 Fixed: Not removing seeded download if it was manual imported in some cases
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Mark McDowall
27c9337071 Fixed: Rejections custom filter for Interactive Search (now Rejections Count)
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Qstick
1fbaefc054 Improve root folder health check
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Skyler Mäntysaari
b1cfa90a9f New: SendGrid Notifications
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Qstick
3e01c3089c Fixed: Added .org to website url filtering in parser
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
2e80c86e23 Fixed recursion issue when emptying recycle bin
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
31837958c1 Fixed: Tag details list series in alphabetical order
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
a22297c197 Added UserAgent to api request trace log
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
6e595bfad3 New: Add DownloadClient and DownloadId to Webhook notifications
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
8ffcc8a711 Clarify that Post-Import Category torrents are not monitored by Sonarr.
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
4b3db6f596 Fixed: Windows installer won't create shortcut if unchecked
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Taloth Saldono
778085544c Improved error message when nzb download contains an newznab error instead
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
81a20ab0e5 Fixed: Ended overlay on artist posters
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
770176e127 Rename FilterFiles to FilterPaths
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
2f56814f58 Fixed: Queue not always clearing checked items when updated
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
4247605dc9 Fixed: Remove website post fix before parsing
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Taloth Saldono
3d4c8df6f7 Linting error
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
a5e69528a9 Fix Release Push log statement
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Jacob
074bcb4ebf New: Added option to filter Release Profile to a specific indexer
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Mark McDowall
2bd4965763 New: Clone indexer button
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
fdef79a9dc Fixed: Manual Import sorting by quality
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
1423ebdb07 Fixed: Prompt to restart after resetting API key
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
06d3e47232 Fixed: Sorting by track count
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Taloth Saldono
255c19bb83 Added Norwegian Bokmal alias
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Taloth Saldono
4104efa9a0 Fixed: Representation of episode start time when not starting at the full hour in am/pm notation
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
5f1b928cd9 Fixed: RestClient does not use global proxy settings
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
49b085bac6 New: Limit recent folders in Manual import to 10 and descending order
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
5a4034ec6a Fix proptype warning for id of EnhancedSelectInputOption
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
ed1175ed86 Remove website prefixes with dashes in URL
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
31287eb0d7 Fixed: Details for episode history flashing on mobile devices
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Fossil
664905b4f2 Remove PFMonkey.com from Presets
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
a4223e8dbf Fixed: Test All not clearing health error
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
3af8c2f504 Improved some log messages
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
67086060b1 Fixed: Delete files from Artist Mass Editor not actually deleting files
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Taloth Saldono
099fd3166c Tiny fix in test, left-over from my on-windows test.
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Taloth Saldono
43363cbd08 Fixed: File imports on cloud drives slow due to transaction logic
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
7d0eeca9a9 Fixed: Use Proxy for MediaCovers and Metadata
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
aa7d289a48 Fixed: Set permissions on extra and subtitle files
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
c0f8d92c6e Fixed: Include releases that failed to parse in search results
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
2732ea43ad New: Event Driven HealthCheck Support
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
bc059c3f32 Fixed: Disregard Real when user disabled proper preference
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
172f8e4b31 Simplify ManualImportModule null check
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
2e9087d4c3 Fixed: Edge case where import fails due to DB relationship mismatch
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
d004f95297 Fixed: Improved failed series search messaging
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
566d84e22d Fixed: Manage Tracks not showing whether language/quality meets cutoff
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
8dd3c14132 Fixed: Delay profile being ignored for non-revision upgrades
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
acb3470988 Remove unnecessary usings
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
953d8a92c2 Remove Dotnet Framework Version Checks
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:41 -05:00
nitsua
129245cc5b New: Add label to disk usage progress bar
(cherry picked from commit 7c8ac300777583cb93d9deeed1328bcffaef555c)
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:41 -05:00
Qstick
4ecd7eaf08 Update README and CONTRIBUTING
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:41 -05:00
Qstick
698eedb51a Cache Yarn Packages on build
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:41 -05:00
Qstick
a94623d95b Catchup Linting
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:41 -05:00
374 changed files with 7642 additions and 3122 deletions

View File

@@ -19,11 +19,12 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.serva
1. Fork Readarr
2. Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories)
3. Install the required Node Packages `yarn install`
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
5. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `netcoreapp31`
6. Debug the project in Visual Studio
7. Open http://localhost:8787
3. Grab the submodules `git submodule init && git submodule update`
4. Install the required Node Packages `yarn install`
5. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
6. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `netcoreapp31`
7. Debug the project in Visual Studio
8. Open http://localhost:8787
### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Readarr/Readarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)

View File

@@ -2,6 +2,7 @@
[![Build Status](https://dev.azure.com/Readarr/Readarr/_apis/build/status/Readarr.Readarr?branchName=develop)](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/readarr)](https://hub.docker.com/r/hotio/readarr)
![Github Downloads](https://img.shields.io/github/downloads/readarr/readarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/readarr/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/readarr/sponsors/badge.svg)](#sponsors)
### Readarr is in early stages of development, alpha/beta binary builds are not yet available. Use of any test builds isn't recommend, and may have detrimental effects on your library.

View File

@@ -697,17 +697,6 @@ stages:
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh ${OSNAME} Automation Test
displayName: Run Automation Tests
- task: CopyFiles@2
displayName: 'Copy Screenshot to: $(Build.ArtifactStagingDirectory)'
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
**/*_test_screenshot.png
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
- publish: $(Build.ArtifactStagingDirectory)/screenshots
artifact: '$(osName)AutomationScreenshots'
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
displayName: Publish Screenshot Bundle
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'

View File

@@ -1,8 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
@@ -10,11 +11,83 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import { align, icons, kinds } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import BlacklistRowConnector from './BlacklistRowConnector';
class Blacklist extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
items: props.items
};
}
componentDidUpdate(prevProps) {
const {
items
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
}
onRemoveSelectedConfirmed = () => {
this.props.onRemoveSelected(this.getSelectedIds());
this.setState({ isConfirmRemoveModalOpen: false });
}
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
}
//
// Render
@@ -28,6 +101,7 @@ class Blacklist extends Component {
items,
columns,
totalRecords,
isRemoving,
isClearingBlacklistExecuting,
onClearBlacklistPress,
...otherProps
@@ -36,10 +110,27 @@ class Blacklist extends Component {
const isAllPopulated = isPopulated && isAuthorPopulated;
const isAnyFetching = isFetching || isAuthorFetching;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return (
<PageContent title="Blacklist">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton
label="Clear"
iconName={icons.CLEAR}
@@ -61,7 +152,7 @@ class Blacklist extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
<PageContentBody>
{
isAnyFetching && !isAllPopulated &&
<LoadingIndicator />
@@ -83,8 +174,12 @@ class Blacklist extends Component {
isAllPopulated && !error && !!items.length &&
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
@@ -92,8 +187,10 @@ class Blacklist extends Component {
return (
<BlacklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
@@ -108,7 +205,17 @@ class Blacklist extends Component {
/>
</div>
}
</PageContentBodyConnector>
</PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title="Remove Selected"
message={'Are you sure you want to remove the selected items from the blacklist?'}
confirmLabel="Remove Selected"
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
@@ -123,7 +230,9 @@ Blacklist.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired
};

View File

@@ -92,6 +92,10 @@ class BlacklistConnector extends Component {
this.props.gotoBlacklistPage({ page });
}
onRemoveSelected = (ids) => {
this.props.removeBlacklistItems({ ids });
}
onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey });
}
@@ -119,6 +123,7 @@ class BlacklistConnector extends Component {
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress}
@@ -138,6 +143,7 @@ BlacklistConnector.propTypes = {
gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired,
removeBlacklistItems: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: PropTypes.func.isRequired,

View File

@@ -5,6 +5,7 @@ import BookQuality from 'Book/BookQuality';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import BlacklistDetailsModal from './BlacklistDetailsModal';
@@ -39,6 +40,7 @@ class BlacklistRow extends Component {
render() {
const {
id,
author,
sourceTitle,
quality,
@@ -46,7 +48,9 @@ class BlacklistRow extends Component {
protocol,
indexer,
message,
isSelected,
columns,
onSelectedChange,
onRemovePress
} = this.props;
@@ -56,6 +60,12 @@ class BlacklistRow extends Component {
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
@@ -167,7 +177,9 @@ BlacklistRow.propTypes = {
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired
};

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import BlacklistRow from './BlacklistRow';
@@ -18,7 +18,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
dispatch(removeFromBlacklist({ id: props.id }));
dispatch(removeBlacklistItem({ id: props.id }));
}
};
}

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
@@ -98,7 +98,7 @@ class History extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
<PageContentBody>
{
isFetchingAny && !isAllPopulated &&
<LoadingIndicator />
@@ -149,7 +149,7 @@ class History extends Component {
/>
</div>
}
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
@@ -13,6 +13,7 @@ import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
@@ -36,34 +37,28 @@ class Queue extends Component {
lastToggled: null,
selectedState: {},
isPendingSelected: false,
isConfirmRemoveModalOpen: false
isConfirmRemoveModalOpen: false,
items: props.items
};
}
shouldComponentUpdate(nextProps) {
// Don't update when fetching has completed if items have changed,
// before books start fetching or when books start fetching.
componentDidUpdate(prevProps) {
const {
items,
isFetching,
isBooksFetching
} = this.props;
if (
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items) &&
nextProps.items.some((e) => e.bookId)
(!isBooksFetching && prevProps.isBooksFetching) ||
(!isFetching && prevProps.isFetching) ||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.bookId))
) {
return false;
}
if (!this.props.isBooksFetching && nextProps.isBooksFetching) {
return false;
}
return true;
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
this.setState((state) => {
return removeOldSelectedState(state, prevProps.items);
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
@@ -200,7 +195,7 @@ class Queue extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
<PageContentBody>
{
isRefreshing && !isAllPopulated &&
<LoadingIndicator />
@@ -257,7 +252,7 @@ class Queue extends Component {
/>
</div>
}
</PageContentBodyConnector>
</PageContentBody>
<RemoveQueueItemsModal
isOpen={isConfirmRemoveModalOpen}

View File

@@ -59,6 +59,7 @@
}
.titleRow {
position: relative;
display: flex;
justify-content: space-between;
flex: 0 0 auto;
@@ -95,11 +96,9 @@
margin-left: 20px;
}
.filterIcon {
float: right;
}
.authorNavigationButtons {
.artistNavigationButtons {
position: absolute;
right: 0;
white-space: nowrap;
}

View File

@@ -16,7 +16,7 @@ import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
@@ -31,7 +31,6 @@ import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import fonts from 'Styles/Variables/fonts';
import formatBytes from 'Utilities/Number/formatBytes';
import stripHtml from 'Utilities/String/stripHtml';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
@@ -295,7 +294,7 @@ class AuthorDetails extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector innerClassName={styles.innerContentBody}>
<PageContentBody innerClassName={styles.innerContentBody}>
<div className={styles.header}>
<div
className={styles.backdrop}
@@ -412,7 +411,7 @@ class AuthorDetails extends Component {
<span className={styles.sizeOnDisk}>
{
formatBytes(sizeOnDisk)
formatBytes(sizeOnDisk || 0)
}
</span>
</Label>
@@ -518,7 +517,7 @@ class AuthorDetails extends Component {
<div className={styles.overview}>
<TextTruncate
line={Math.floor(125 / (defaultFontSize * lineHeight))}
text={stripHtml(overview)}
text={overview}
/>
</div>
</div>
@@ -686,7 +685,7 @@ class AuthorDetails extends Component {
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -7,7 +7,7 @@ import { createSelector } from 'reselect';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import NotFound from 'Components/NotFound';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import AuthorDetailsConnector from './AuthorDetailsConnector';
import styles from './AuthorDetails.css';
@@ -74,9 +74,9 @@ class AuthorDetailsPageConnector extends Component {
if (isFetching && !isPopulated) {
return (
<PageContent title='loading'>
<PageContentBodyConnector>
<PageContentBody>
<LoadingIndicator />
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -4,12 +4,15 @@ import NoAuthor from 'Author/NoAuthor';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { align, sortDirections } from 'Helpers/Props';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
@@ -20,46 +23,6 @@ import AuthorEditorFooter from './AuthorEditorFooter';
import AuthorEditorRowConnector from './AuthorEditorRowConnector';
import OrganizeAuthorModal from './Organize/OrganizeAuthorModal';
function getColumns(showMetadataProfile) {
return [
{
name: 'status',
isSortable: true,
isVisible: true
},
{
name: 'sortName',
label: 'Name',
isSortable: true,
isVisible: true
},
{
name: 'qualityProfileId',
label: 'Quality Profile',
isSortable: true,
isVisible: true
},
{
name: 'metadataProfileId',
label: 'Metadata Profile',
isSortable: true,
isVisible: showMetadataProfile
},
{
name: 'path',
label: 'Path',
isSortable: true,
isVisible: true
},
{
name: 'tags',
label: 'Tags',
isSortable: false,
isVisible: true
}
];
}
class AuthorEditor extends Component {
//
@@ -74,8 +37,7 @@ class AuthorEditor extends Component {
lastToggled: null,
selectedState: {},
isOrganizingAuthorModalOpen: false,
isRetaggingAuthorModalOpen: false,
columns: getColumns(props.showMetadataProfile)
isRetaggingAuthorModalOpen: false
};
}
@@ -155,6 +117,7 @@ class AuthorEditor extends Component {
error,
totalItems,
items,
columns,
selectedFilterKey,
filters,
customFilters,
@@ -166,7 +129,7 @@ class AuthorEditor extends Component {
deleteError,
isOrganizingAuthor,
isRetaggingAuthor,
showMetadataProfile,
onTableOptionChange,
onSortPress,
onFilterSelect
} = this.props;
@@ -174,8 +137,7 @@ class AuthorEditor extends Component {
const {
allSelected,
allUnselected,
selectedState,
columns
selectedState
} = this.state;
const selectedAuthorIds = this.getSelectedIds();
@@ -185,6 +147,18 @@ class AuthorEditor extends Component {
<PageToolbar>
<PageToolbarSection />
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<PageToolbarSeparator />
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
@@ -196,7 +170,7 @@ class AuthorEditor extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
@@ -243,7 +217,7 @@ class AuthorEditor extends Component {
!error && isPopulated && !items.length &&
<NoAuthor totalItems={totalItems} />
}
</PageContentBodyConnector>
</PageContentBody>
<AuthorEditorFooter
authorIds={selectedAuthorIds}
@@ -254,7 +228,8 @@ class AuthorEditor extends Component {
deleteError={deleteError}
isOrganizingAuthor={isOrganizingAuthor}
isRetaggingAuthor={isRetaggingAuthor}
showMetadataProfile={showMetadataProfile}
columns={columns}
showMetadataProfile={columns.find((column) => column.name === 'metadataProfileId').isVisible}
onSaveSelected={this.onSaveSelected}
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
onRetagAuthorPress={this.onRetagAuthorPress}
@@ -283,6 +258,7 @@ AuthorEditor.propTypes = {
error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
@@ -294,7 +270,7 @@ AuthorEditor.propTypes = {
deleteError: PropTypes.object,
isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort } from 'Store/Actions/authorEditorActions';
import { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort, setAuthorEditorTableOption } from 'Store/Actions/authorEditorActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchRootFolders } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
@@ -12,15 +12,13 @@ import AuthorEditor from './AuthorEditor';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.metadataProfiles,
createClientSideCollectionSelector('authors', 'authorEditor'),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
(metadataProfiles, author, isOrganizingAuthor, isRetaggingAuthor) => {
(author, isOrganizingAuthor, isRetaggingAuthor) => {
return {
isOrganizingAuthor,
isRetaggingAuthor,
showMetadataProfile: metadataProfiles.items.length > 1,
...author
};
}
@@ -30,6 +28,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
dispatchSetAuthorEditorSort: setAuthorEditorSort,
dispatchSetAuthorEditorFilter: setAuthorEditorFilter,
dispatchSetAuthorEditorTableOption: setAuthorEditorTableOption,
dispatchSaveAuthorEditor: saveAuthorEditor,
dispatchFetchRootFolders: fetchRootFolders,
dispatchExecuteCommand: executeCommand
@@ -55,6 +54,10 @@ class AuthorEditorConnector extends Component {
this.props.dispatchSetAuthorEditorFilter({ selectedFilterKey });
}
onTableOptionChange = (payload) => {
this.props.dispatchSetAuthorEditorTableOption(payload);
}
onSaveSelected = (payload) => {
this.props.dispatchSaveAuthorEditor(payload);
}
@@ -76,6 +79,7 @@ class AuthorEditorConnector extends Component {
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onSaveSelected={this.onSaveSelected}
onTableOptionChange={this.onTableOptionChange}
/>
);
}
@@ -84,6 +88,7 @@ class AuthorEditorConnector extends Component {
AuthorEditorConnector.propTypes = {
dispatchSetAuthorEditorSort: PropTypes.func.isRequired,
dispatchSetAuthorEditorFilter: PropTypes.func.isRequired,
dispatchSetAuthorEditorTableOption: PropTypes.func.isRequired,
dispatchSaveAuthorEditor: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired

View File

@@ -138,7 +138,7 @@ class AuthorEditorFooter extends Component {
isDeleting,
isOrganizingAuthor,
isRetaggingAuthor,
showMetadataProfile,
columns,
onOrganizeAuthorPress,
onRetagAuthorPress
} = this.props;
@@ -147,6 +147,7 @@ class AuthorEditorFooter extends Component {
monitored,
qualityProfileId,
metadataProfileId,
bookFolder,
rootFolderPath,
savingTags,
isTagsModalOpen,
@@ -161,6 +162,12 @@ class AuthorEditorFooter extends Component {
{ key: 'unmonitored', value: 'Unmonitored' }
];
const bookFolderOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true },
{ key: 'yes', value: 'Yes' },
{ key: 'no', value: 'No' }
];
return (
<PageContentFooter>
<div className={styles.inputContainer}>
@@ -178,56 +185,110 @@ class AuthorEditorFooter extends Component {
/>
</div>
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label="Quality Profile"
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
{
showMetadataProfile &&
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label="Metadata Profile"
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/>
columns.map((column) => {
const {
name,
isVisible
} = column;
<MetadataProfileSelectInputConnector
name="metadataProfileId"
value={metadataProfileId}
includeNoChange={true}
includeNone={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
if (!isVisible) {
return null;
}
if (name === 'qualityProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label="Quality Profile"
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'metadataProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label="Metadata Profile"
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/>
<MetadataProfileSelectInputConnector
name="metadataProfileId"
value={metadataProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'bookFolder') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label="Book Folder"
isSaving={isSaving && bookFolder !== NO_CHANGE}
/>
<SelectInput
name="bookFolder"
value={bookFolder}
values={bookFolderOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'path') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label="Root Folder"
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
);
}
return null;
})
}
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label="Root Folder"
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<AuthorEditorFooterLabel
@@ -315,6 +376,7 @@ AuthorEditorFooter.propTypes = {
isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSaveSelected: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired,
onRetagAuthorPress: PropTypes.func.isRequired

View File

@@ -0,0 +1,5 @@
.bookFolder {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 150px;
}

View File

@@ -1,12 +1,14 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AuthorNameLink from 'Author/AuthorNameLink';
import AuthorStatusCell from 'Author/Index/Table/AuthorStatusCell';
import CheckInput from 'Components/Form/CheckInput';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import TagListConnector from 'Components/TagListConnector';
import formatBytes from 'Utilities/Number/formatBytes';
import styles from './AuthorEditorRow.css';
class AuthorEditorRow extends Component {
@@ -25,16 +27,20 @@ class AuthorEditorRow extends Component {
const {
id,
status,
titleSlug,
foreignAuthorId,
authorName,
authorType,
bookFolder,
monitored,
metadataProfile,
qualityProfile,
path,
statistics,
tags,
columns,
isSaving,
isSelected,
onAuthorMonitoredPress,
onSelectedChange
} = this.props;
@@ -46,39 +52,105 @@ class AuthorEditorRow extends Component {
onSelectedChange={onSelectedChange}
/>
<AuthorStatusCell
authorType={authorType}
monitored={monitored}
status={status}
/>
<TableRowCell>
<AuthorNameLink
titleSlug={titleSlug}
authorName={authorName}
/>
</TableRowCell>
<TableRowCell>
{qualityProfile.name}
</TableRowCell>
{
_.find(columns, { name: 'metadataProfileId' }).isVisible &&
<TableRowCell>
{metadataProfile.name}
</TableRowCell>
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<AuthorStatusCell
key={name}
authorType={authorType}
monitored={monitored}
status={status}
isSaving={isSaving}
onMonitoredPress={onAuthorMonitoredPress}
/>
);
}
if (name === 'sortName') {
return (
<TableRowCell
key={name}
className={styles.title}
>
<AuthorNameLink
foreignAuthorId={foreignAuthorId}
authorName={authorName}
/>
</TableRowCell>
);
}
if (name === 'qualityProfileId') {
return (
<TableRowCell key={name}>
{qualityProfile.name}
</TableRowCell>
);
}
if (name === 'metadataProfileId') {
return (
<TableRowCell key={name}>
{metadataProfile.name}
</TableRowCell>
);
}
if (name === 'bookFolder') {
return (
<TableRowCell
key={name}
className={styles.bookFolder}
>
<CheckInput
name="bookFolder"
value={bookFolder}
isDisabled={true}
onChange={this.onBookFolderChange}
/>
</TableRowCell>
);
}
if (name === 'path') {
return (
<TableRowCell key={name}>
{path}
</TableRowCell>
);
}
if (name === 'sizeOnDisk') {
return (
<TableRowCell key={name}>
{formatBytes(statistics.sizeOnDisk)}
</TableRowCell>
);
}
if (name === 'tags') {
return (
<TableRowCell key={name}>
<TagListConnector
tags={tags}
/>
</TableRowCell>
);
}
return null;
})
}
<TableRowCell>
{path}
</TableRowCell>
<TableRowCell>
<TagListConnector
tags={tags}
/>
</TableRowCell>
</TableRow>
);
}
@@ -87,16 +159,21 @@ class AuthorEditorRow extends Component {
AuthorEditorRow.propTypes = {
id: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
foreignAuthorId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired,
authorType: PropTypes.string,
bookFolder: PropTypes.string,
monitored: PropTypes.bool.isRequired,
metadataProfile: PropTypes.object.isRequired,
qualityProfile: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onAuthorMonitoredPress: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired
};

View File

@@ -25,7 +25,7 @@ function OrganizeAuthorModalContent(props) {
<ModalBody>
<Alert>
Tip: To preview a rename... select "Cancel" then click any author name and use the
Tip: To preview a rename, select "Cancel", then select any artist name and use the
<Icon
className={styles.renameIcon}
name={icons.ORGANIZE}

View File

@@ -1,3 +1,9 @@
.sourceTitle {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-word;
}
.details,
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -94,7 +94,7 @@ class AuthorHistoryRow extends Component {
{book.title}
</TableRowCell>
<TableRowCell>
<TableRowCell className={styles.sourceTitle}>
{sourceTitle}
</TableRowCell>

View File

@@ -4,7 +4,7 @@ import React, { Component } from 'react';
import NoAuthor from 'Author/NoAuthor';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageJumpBar from 'Components/Page/PageJumpBar';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -312,7 +312,7 @@ class AuthorIndex extends Component {
</PageToolbar>
<div className={styles.pageContentBodyWrapper}>
<PageContentBodyConnector
<PageContentBody
registerScroller={this.setScrollerRef}
className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]}
@@ -351,7 +351,7 @@ class AuthorIndex extends Component {
!error && isPopulated && !items.length &&
<NoAuthor totalItems={totalItems} />
}
</PageContentBodyConnector>
</PageContentBody>
{
isLoaded && !!jumpBarItems.order.length &&

View File

@@ -8,8 +8,7 @@ import withScrollPosition from 'Components/withScrollPosition';
import { setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions';
import { executeCommand } from 'Store/Actions/commandActions';
import scrollPositions from 'Store/scrollPositions';
import createAuthorClientSideCollectionItemsSelector
from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
import createAuthorClientSideCollectionItemsSelector from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import AuthorIndex from './AuthorIndex';

View File

@@ -11,7 +11,6 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import stripHtml from 'Utilities/String/stripHtml';
import AuthorIndexOverviewInfo from './AuthorIndexOverviewInfo';
import styles from './AuthorIndexOverview.css';
@@ -205,7 +204,7 @@ class AuthorIndexOverview extends Component {
>
<TextTruncate
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
text={stripHtml(overview)}
text={overview}
/>
</Link>

View File

@@ -1,9 +1,5 @@
$hoverScale: 1.05;
.container {
padding: 10px;
}
.content {
transition: all 200ms ease-in;

View File

@@ -115,7 +115,7 @@ class AuthorIndexPoster extends Component {
};
return (
<div className={styles.container}>
<div>
<div className={styles.content}>
<div className={styles.posterContainer}>
<Label className={styles.controls}>

View File

@@ -105,6 +105,7 @@ class AuthorIndexPosters extends Component {
this._isInitialized = false;
this._grid = null;
this._padding = props.isSmallScreen ? columnPaddingSmallScreen : columnPadding;
}
componentDidUpdate(prevProps, prevState) {
@@ -112,7 +113,9 @@ class AuthorIndexPosters extends Component {
items,
sortKey,
posterOptions,
jumpToCharacter
jumpToCharacter,
scrollTop,
isSmallScreen
} = this.props;
const {
@@ -124,7 +127,7 @@ class AuthorIndexPosters extends Component {
if (prevProps.sortKey !== sortKey ||
prevProps.posterOptions !== posterOptions) {
this.calculateGrid();
this.calculateGrid(width, isSmallScreen);
}
if (this._grid &&
@@ -149,6 +152,10 @@ class AuthorIndexPosters extends Component {
});
}
}
if (this._grid && scrollTop !== 0) {
this._grid.scrollToPosition({ scrollTop });
}
}
//
@@ -164,10 +171,9 @@ class AuthorIndexPosters extends Component {
posterOptions
} = this.props;
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen);
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
const posterWidth = columnWidth - padding;
const posterWidth = columnWidth - this._padding * 2;
const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions);
@@ -214,7 +220,10 @@ class AuthorIndexPosters extends Component {
return (
<div
key={key}
style={style}
style={{
...style,
padding: this._padding
}}
>
<AuthorIndexItemConnector
key={author.id}
@@ -229,6 +238,7 @@ class AuthorIndexPosters extends Component {
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
style={style}
authorId={author.id}
qualityProfileId={author.qualityProfileId}
metadataProfileId={author.metadataProfileId}
@@ -249,9 +259,9 @@ class AuthorIndexPosters extends Component {
render() {
const {
scroller,
items,
isSmallScreen,
scroller
isSmallScreen
} = this.props;
const {
@@ -310,6 +320,7 @@ AuthorIndexPosters.propTypes = {
sortKey: PropTypes.string,
posterOptions: PropTypes.object.isRequired,
jumpToCharacter: PropTypes.string,
scrollTop: PropTypes.number.isRequired,
scroller: PropTypes.instanceOf(Element).isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,

View File

@@ -46,6 +46,7 @@
}
.titleRow {
position: relative;
display: flex;
justify-content: space-between;
flex: 0 0 auto;
@@ -83,6 +84,8 @@
}
.bookNavigationButtons {
position: absolute;
right: 0;
white-space: nowrap;
}

View File

@@ -17,7 +17,7 @@ import IconButton from 'Components/Link/IconButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
@@ -29,7 +29,6 @@ import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import fonts from 'Styles/Variables/fonts';
import formatBytes from 'Utilities/Number/formatBytes';
import stripHtml from 'Utilities/String/stripHtml';
import BookDetailsLinks from './BookDetailsLinks';
import styles from './BookDetails.css';
@@ -197,7 +196,7 @@ class BookDetails extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector innerClassName={styles.innerContentBody}>
<PageContentBody innerClassName={styles.innerContentBody}>
<div className={styles.header}>
<div
className={styles.backdrop}
@@ -361,7 +360,7 @@ class BookDetails extends Component {
<div className={styles.overview}>
<TextTruncate
line={Math.floor(125 / (defaultFontSize * lineHeight))}
text={stripHtml(overview)}
text={overview}
/>
</div>
</div>
@@ -467,7 +466,7 @@ class BookDetails extends Component {
onModalClose={this.onDeleteBookModalClose}
/>
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -7,7 +7,7 @@ import { createSelector } from 'reselect';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import NotFound from 'Components/NotFound';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
import BookDetailsConnector from './BookDetailsConnector';
@@ -103,9 +103,9 @@ class BookDetailsPageConnector extends Component {
(!isFetching && !isPopulated)) {
return (
<PageContent title='loading'>
<PageContentBodyConnector>
<PageContentBody>
<LoadingIndicator />
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -10,6 +10,7 @@ function BookFileEditorRow(props) {
id,
path,
quality,
qualityCutoffNotMet,
isSelected,
onSelectedChange
} = props;
@@ -28,6 +29,7 @@ function BookFileEditorRow(props) {
<TableRowCell>
<BookQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</TableRowCell>
</TableRow>
@@ -38,6 +40,7 @@ BookFileEditorRow.propTypes = {
id: PropTypes.number.isRequired,
path: PropTypes.string.isRequired,
quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};

View File

@@ -6,7 +6,7 @@ import NoAuthor from 'Author/NoAuthor';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageJumpBar from 'Components/Page/PageJumpBar';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
@@ -363,7 +363,7 @@ class Bookshelf extends Component {
</PageToolbar>
<div className={styles.pageContentBodyWrapper}>
<PageContentBodyConnector
<PageContentBody
registerScroller={this.setScrollerRef}
className={styles.contentBody}
innerClassName={styles.innerContentBody}
@@ -414,7 +414,7 @@ class Bookshelf extends Component {
!error && isPopulated && !items.length &&
<NoAuthor totalItems={totalItems} />
}
</PageContentBodyConnector>
</PageContentBody>
{
isPopulated && !!jumpBarItems.order.length &&

View File

@@ -5,7 +5,7 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Measure from 'Components/Measure';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
@@ -130,7 +130,7 @@ class CalendarPage extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector
<PageContentBody
className={styles.calendarPageBody}
innerClassName={styles.calendarInnerPageBody}
>
@@ -171,7 +171,7 @@ class CalendarPage extends Component {
hasAuthor && !!authorError &&
<LegendConnector />
}
</PageContentBodyConnector>
</PageContentBody>
<CalendarLinkModal
isOpen={isCalendarLinkModalOpen}

View File

@@ -2,13 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearOptions, fetchOptions } from 'Store/Actions/providerOptionActions';
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
import DeviceInput from './DeviceInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state) => state.providerOptions,
(state) => state.providerOptions.devices || defaultState,
(value, devices) => {
return {
@@ -51,7 +51,7 @@ class DeviceInputConnector extends Component {
}
componentWillUnmount = () => {
this.props.dispatchClearOptions();
this.props.dispatchClearOptions({ section: 'devices' });
}
//
@@ -65,6 +65,7 @@ class DeviceInputConnector extends Component {
} = this.props;
dispatchFetchOptions({
section: 'devices',
action: 'getDevices',
provider,
providerData

View File

@@ -1,19 +1,12 @@
.enhancedSelect {
composes: input from '~Components/Form/Input.css';
composes: link from '~Components/Link/Link.css';
position: relative;
display: flex;
align-items: center;
padding: 6px 16px;
}
.editableContainer {
width: 100%;
height: 35px;
border: 1px solid $inputBorderColor;
border-radius: 4px;
background-color: $white;
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
color: $black;
cursor: default;
}
.hasError {
@@ -33,6 +26,16 @@
margin-left: 12px;
}
.dropdownArrowContainerEditable {
position: absolute;
top: 0;
right: 0;
padding-right: 17px;
width: 30%;
height: 35px;
text-align: right;
}
.dropdownArrowContainerDisabled {
composes: dropdownArrowContainer;
@@ -76,3 +79,8 @@
border-radius: 4px;
background-color: $white;
}
.loading {
display: inline-block;
margin: 5px -5px 5px 0;
}

View File

@@ -5,6 +5,7 @@ import React, { Component } from 'react';
import { Manager, Popper, Reference } from 'react-popper';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Measure from 'Components/Measure';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
@@ -16,6 +17,7 @@ import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import HintedSelectInputOption from './HintedSelectInputOption';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput';
import styles from './EnhancedSelectInput.css';
function isArrowKey(keyCode) {
@@ -58,11 +60,30 @@ function getSelectedIndex(props) {
values
} = props;
if (Array.isArray(value)) {
return values.findIndex((v) => {
return value.size && v.key === value[0];
});
}
return values.findIndex((v) => {
return v.key === value;
});
}
function isSelectedItem(index, props) {
const {
value,
values
} = props;
if (Array.isArray(value)) {
return value.includes(values[index].key);
}
return values[index].key === value;
}
function getKey(selectedIndex, values) {
return values[selectedIndex].key;
}
@@ -92,7 +113,7 @@ class EnhancedSelectInput extends Component {
this._scheduleUpdate();
}
if (prevProps.value !== this.props.value) {
if (!Array.isArray(this.props.value) && prevProps.value !== this.props.value) {
this.setState({
selectedIndex: getSelectedIndex(this.props)
});
@@ -134,7 +155,7 @@ class EnhancedSelectInput extends Component {
const button = document.getElementById(this._buttonId);
const options = document.getElementById(this._optionsId);
if (!button || this.state.isMobile) {
if (!button || !event.target.isConnected || this.state.isMobile) {
return;
}
@@ -149,11 +170,21 @@ class EnhancedSelectInput extends Component {
}
}
onFocus = () => {
if (this.state.isOpen) {
this._removeListener();
this.setState({ isOpen: false });
}
}
onBlur = () => {
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
const origIndex = getSelectedIndex(this.props);
if (origIndex !== this.state.selectedIndex) {
this.setState({ selectedIndex: origIndex });
if (!this.props.isEditable) {
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
const origIndex = getSelectedIndex(this.props);
if (origIndex !== this.state.selectedIndex) {
this.setState({ selectedIndex: origIndex });
}
}
}
@@ -177,7 +208,7 @@ class EnhancedSelectInput extends Component {
}
if (
selectedIndex == null ||
selectedIndex == null || selectedIndex === -1 ||
getSelectedOption(selectedIndex, values).isDisabled
) {
if (keyCode === keyCodes.UP_ARROW) {
@@ -231,16 +262,35 @@ class EnhancedSelectInput extends Component {
this._addListener();
}
if (!this.state.isOpen && this.props.onOpen) {
this.props.onOpen();
}
this.setState({ isOpen: !this.state.isOpen });
}
onSelect = (value) => {
this.setState({ isOpen: false });
if (Array.isArray(this.props.value)) {
let newValue = null;
const index = this.props.value.indexOf(value);
if (index === -1) {
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
} else {
newValue = [...this.props.value];
newValue.splice(index, 1);
}
this.props.onChange({
name: this.props.name,
value: newValue
});
} else {
this.setState({ isOpen: false });
this.props.onChange({
name: this.props.name,
value
});
this.props.onChange({
name: this.props.name,
value
});
}
}
onMeasure = ({ width }) => {
@@ -258,13 +308,19 @@ class EnhancedSelectInput extends Component {
const {
className,
disabledClassName,
name,
value,
values,
isDisabled,
isEditable,
isFetching,
hasError,
hasWarning,
valueOptions,
selectedValueOptions,
selectedValueComponent: SelectedValueComponent,
optionComponent: OptionComponent
optionComponent: OptionComponent,
onChange
} = this.props;
const {
@@ -274,6 +330,7 @@ class EnhancedSelectInput extends Component {
isMobile
} = this.state;
const isMultiSelect = Array.isArray(value);
const selectedOption = getSelectedOption(selectedIndex, values);
return (
@@ -289,37 +346,94 @@ class EnhancedSelectInput extends Component {
whitelist={['width']}
onMeasure={this.onMeasure}
>
<Link
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
{
isEditable ?
<div
className={styles.editableContainer}
>
<TextInput
className={className}
name={name}
value={value}
readOnly={isDisabled}
hasError={hasError}
hasWarning={hasWarning}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={onChange}
/>
<Link
className={classNames(
styles.dropdownArrowContainerEditable,
isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer)
}
onPress={this.onPress}
>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</Link>
</div> :
<Link
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
value={value}
values={values}
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
isMultiSelect={isMultiSelect}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</div>
</Link>
}
</Measure>
</div>
)}
@@ -358,11 +472,18 @@ class EnhancedSelectInput extends Component {
>
{
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected = hasParent && value.includes(v.parentKey);
return (
<OptionComponent
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
depth={depth}
isSelected={isSelectedItem(index, this.props)}
isDisabled={parentSelected}
isMultiSelect={isMultiSelect}
{...valueOptions}
{...v}
isMobile={false}
onSelect={this.onSelect}
@@ -399,11 +520,18 @@ class EnhancedSelectInput extends Component {
<Scroller className={styles.optionsModalScroller}>
{
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected = hasParent && value.includes(v.parentKey);
return (
<OptionComponent
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
depth={depth}
isSelected={isSelectedItem(index, this.props)}
isMultiSelect={isMultiSelect}
isDisabled={parentSelected}
{...valueOptions}
{...v}
isMobile={true}
onSelect={this.onSelect}
@@ -426,14 +554,18 @@ EnhancedSelectInput.propTypes = {
className: PropTypes.string,
disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool,
isDisabled: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isEditable: PropTypes.bool.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
valueOptions: PropTypes.object.isRequired,
selectedValueOptions: PropTypes.object.isRequired,
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
optionComponent: PropTypes.elementType,
onOpen: PropTypes.func,
onChange: PropTypes.func.isRequired
};
@@ -441,6 +573,9 @@ EnhancedSelectInput.defaultProps = {
className: styles.enhancedSelect,
disabledClassName: styles.isDisabled,
isDisabled: false,
isFetching: false,
isEditable: false,
valueOptions: {},
selectedValueOptions: {},
selectedValueComponent: HintedSelectInputSelectedValue,
optionComponent: HintedSelectInputOption

View File

@@ -0,0 +1,159 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
import EnhancedSelectInput from './EnhancedSelectInput';
const importantFieldNames = [
'baseUrl',
'apiPath',
'apiKey'
];
function getProviderDataKey(providerData) {
if (!providerData || !providerData.fields) {
return null;
}
const fields = providerData.fields
.filter((f) => importantFieldNames.includes(f.name))
.map((f) => f.value);
return fields;
}
function getSelectOptions(items) {
if (!items) {
return [];
}
return items.map((option) => {
return {
key: option.value,
value: option.name,
hint: option.hint,
parentKey: option.parentValue
};
});
}
function createMapStateToProps() {
return createSelector(
(state, { selectOptionsProviderAction }) => state.providerOptions[selectOptionsProviderAction] || defaultState,
(options) => {
if (options) {
return {
isFetching: options.isFetching,
values: getSelectOptions(options.items)
};
}
}
);
}
const mapDispatchToProps = {
dispatchFetchOptions: fetchOptions,
dispatchClearOptions: clearOptions
};
class EnhancedSelectInputConnector extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
refetchRequired: false
};
}
componentDidMount = () => {
this._populate();
}
componentDidUpdate = (prevProps) => {
const prevKey = getProviderDataKey(prevProps.providerData);
const nextKey = getProviderDataKey(this.props.providerData);
if (!_.isEqual(prevKey, nextKey)) {
this.setState({ refetchRequired: true });
}
}
componentWillUnmount = () => {
this._cleanup();
}
//
// Listeners
onOpen = () => {
if (this.state.refetchRequired) {
this._populate();
}
}
//
// Control
_populate() {
const {
provider,
providerData,
selectOptionsProviderAction,
dispatchFetchOptions
} = this.props;
if (selectOptionsProviderAction) {
this.setState({ refetchRequired: false });
dispatchFetchOptions({
section: selectOptionsProviderAction,
action: selectOptionsProviderAction,
provider,
providerData
});
}
}
_cleanup() {
const {
selectOptionsProviderAction,
dispatchClearOptions
} = this.props;
if (selectOptionsProviderAction) {
dispatchClearOptions({ section: selectOptionsProviderAction });
}
}
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onOpen={this.onOpen}
/>
);
}
}
EnhancedSelectInputConnector.propTypes = {
provider: PropTypes.string.isRequired,
providerData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
selectOptionsProviderAction: PropTypes.string,
onChange: PropTypes.func.isRequired,
isFetching: PropTypes.bool.isRequired,
dispatchFetchOptions: PropTypes.func.isRequired,
dispatchClearOptions: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EnhancedSelectInputConnector);

View File

@@ -11,6 +11,18 @@
}
}
.optionCheck {
composes: container from '~./CheckInput.css';
flex: 0 0 0;
}
.optionCheckInput {
composes: input from '~./CheckInput.css';
margin-top: 0;
}
.isSelected {
background-color: #e2e2e2;

View File

@@ -4,6 +4,7 @@ import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import CheckInput from './CheckInput';
import styles from './EnhancedSelectInputOption.css';
class EnhancedSelectInputOption extends Component {
@@ -20,15 +21,22 @@ class EnhancedSelectInputOption extends Component {
onSelect(id);
}
onCheckPress = () => {
// CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation.
}
//
// Render
render() {
const {
className,
id,
depth,
isSelected,
isDisabled,
isHidden,
isMultiSelect,
isMobile,
children
} = this.props;
@@ -37,8 +45,8 @@ class EnhancedSelectInputOption extends Component {
<Link
className={classNames(
className,
isSelected && styles.isSelected,
isDisabled && styles.isDisabled,
isSelected && !isMultiSelect && styles.isSelected,
isDisabled && !isMultiSelect && styles.isDisabled,
isHidden && styles.isHidden,
isMobile && styles.isMobile
)}
@@ -46,6 +54,24 @@ class EnhancedSelectInputOption extends Component {
isDisabled={isDisabled}
onPress={this.onPress}
>
{
depth !== 0 &&
<div style={{ width: `${depth * 20}px` }} />
}
{
isMultiSelect &&
<CheckInput
className={styles.optionCheckInput}
containerClassName={styles.optionCheck}
name={`select-${id}`}
value={isSelected}
isDisabled={isDisabled}
onChange={this.onCheckPress}
/>
}
{children}
{
@@ -63,10 +89,12 @@ class EnhancedSelectInputOption extends Component {
EnhancedSelectInputOption.propTypes = {
className: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
depth: PropTypes.number.isRequired,
isSelected: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
isHidden: PropTypes.bool.isRequired,
isMultiSelect: PropTypes.bool.isRequired,
isMobile: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onSelect: PropTypes.func.isRequired
@@ -74,8 +102,10 @@ EnhancedSelectInputOption.propTypes = {
EnhancedSelectInputOption.defaultProps = {
className: styles.option,
depth: 0,
isDisabled: false,
isHidden: false
isHidden: false,
isMultiSelect: false
};
export default EnhancedSelectInputOption;

View File

@@ -9,7 +9,9 @@ import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
import MonitorBooksSelectInput from './MonitorBooksSelectInput';
@@ -23,6 +25,7 @@ import SeriesTypeSelectInput from './SeriesTypeSelectInput';
import TagInputConnector from './TagInputConnector';
import TextInput from './TextInput';
import TextTagInputConnector from './TextTagInputConnector';
import UMaskInput from './UMaskInput';
import styles from './FormInputGroup.css';
function getComponent(type) {
@@ -69,12 +72,18 @@ function getComponent(type) {
case inputTypes.BOOK_EDITION_SELECT:
return BookEditionSelectInputConnector;
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector;
case inputTypes.SELECT:
return EnhancedSelectInput;
case inputTypes.DYNAMIC_SELECT:
return EnhancedSelectInputConnector;
case inputTypes.SERIES_TYPE_SELECT:
return SeriesTypeSelectInput;
@@ -84,6 +93,9 @@ function getComponent(type) {
case inputTypes.TEXT_TAG:
return TextTagInputConnector;
case inputTypes.UMASK:
return UMaskInput;
default:
return TextInput;
}
@@ -191,7 +203,7 @@ function FormInputGroup(props) {
}
{
!checkInput && helpTextWarning &&
(!checkInput || helpText) && helpTextWarning &&
<FormInputHelpText
text={helpTextWarning}
isWarning={true}
@@ -214,7 +226,7 @@ function FormInputGroup(props) {
key={index}
text={error.message}
link={error.link}
linkTooltip={error.detailedMessage}
tooltip={error.detailedMessage}
isError={true}
isCheckInput={checkInput}
/>
@@ -229,7 +241,7 @@ function FormInputGroup(props) {
key={index}
text={warning.message}
link={warning.link}
linkTooltip={warning.detailedMessage}
tooltip={warning.detailedMessage}
isWarning={true}
isCheckInput={checkInput}
/>

View File

@@ -37,3 +37,7 @@
margin-left: 5px;
}
.details {
margin-left: 5px;
}

View File

@@ -11,7 +11,7 @@ function FormInputHelpText(props) {
className,
text,
link,
linkTooltip,
tooltip,
isError,
isWarning,
isCheckInput
@@ -28,16 +28,27 @@ function FormInputHelpText(props) {
{text}
{
!!link &&
link ?
<Link
className={styles.link}
to={link}
title={linkTooltip}
title={tooltip}
>
<Icon
name={icons.EXTERNAL_LINK}
/>
</Link>
</Link> :
null
}
{
!link && tooltip ?
<Icon
containerClassName={styles.details}
name={icons.INFO}
title={tooltip}
/> :
null
}
</div>
);
@@ -47,7 +58,7 @@ FormInputHelpText.propTypes = {
className: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
link: PropTypes.string,
linkTooltip: PropTypes.string,
tooltip: PropTypes.string,
isError: PropTypes.bool,
isWarning: PropTypes.bool,
isCheckInput: PropTypes.bool

View File

@@ -6,14 +6,25 @@ import styles from './HintedSelectInputOption.css';
function HintedSelectInputOption(props) {
const {
id,
value,
hint,
depth,
isSelected,
isDisabled,
isMultiSelect,
isMobile,
...otherProps
} = props;
return (
<EnhancedSelectInputOption
id={id}
depth={depth}
isSelected={isSelected}
isDisabled={isDisabled}
isHidden={isDisabled}
isMultiSelect={isMultiSelect}
isMobile={isMobile}
{...otherProps}
>
@@ -36,9 +47,20 @@ function HintedSelectInputOption(props) {
}
HintedSelectInputOption.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.string.isRequired,
hint: PropTypes.node,
depth: PropTypes.number,
isSelected: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
isMultiSelect: PropTypes.bool.isRequired,
isMobile: PropTypes.bool.isRequired
};
HintedSelectInputOption.defaultProps = {
isDisabled: false,
isHidden: false,
isMultiSelect: false
};
export default HintedSelectInputOption;

View File

@@ -1,23 +1,43 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
import styles from './HintedSelectInputSelectedValue.css';
function HintedSelectInputSelectedValue(props) {
const {
value,
values,
hint,
isMultiSelect,
includeHint,
...otherProps
} = props;
const valuesMap = isMultiSelect && _.keyBy(values, 'key');
return (
<EnhancedSelectInputSelectedValue
className={styles.selectedValue}
{...otherProps}
>
<div className={styles.valueText}>
{value}
{
isMultiSelect &&
value.map((key, index) => {
const v = valuesMap[key];
return (
<Label key={key}>
{v ? v.value : key}
</Label>
);
})
}
{
!isMultiSelect && value
}
</div>
{
@@ -31,12 +51,15 @@ function HintedSelectInputSelectedValue(props) {
}
HintedSelectInputSelectedValue.propTypes = {
value: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
hint: PropTypes.string,
isMultiSelect: PropTypes.bool.isRequired,
includeHint: PropTypes.bool.isRequired
};
HintedSelectInputSelectedValue.defaultProps = {
isMultiSelect: false,
includeHint: true
};

View File

@@ -0,0 +1,96 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexers } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers,
(state, { includeAny }) => includeAny,
(indexers, includeAny) => {
const {
isFetching,
isPopulated,
error,
items
} = indexers;
const values = _.map(items.sort(sortByName), (indexer) => {
return {
key: indexer.id,
value: indexer.name
};
});
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
});
}
return {
isFetching,
isPopulated,
error,
values
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexers: fetchIndexers
};
class IndexerSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchIndexers();
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
}
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
IndexerSelectInputConnector.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeAny: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired
};
IndexerSelectInputConnector.defaultProps = {
includeAny: false
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);

View File

@@ -98,7 +98,9 @@ class KeyValueListInput extends Component {
className,
value,
keyPlaceholder,
valuePlaceholder
valuePlaceholder,
hasError,
hasWarning
} = this.props;
const { isFocused } = this.state;
@@ -106,7 +108,9 @@ class KeyValueListInput extends Component {
return (
<div className={classNames(
className,
isFocused && styles.isFocused
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
>
{

View File

@@ -6,7 +6,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
function getType(type) {
function getType({ type, selectOptionsProviderAction }) {
switch (type) {
case 'captcha':
return inputTypes.CAPTCHA;
@@ -25,6 +25,9 @@ function getType(type) {
case 'filePath':
return inputTypes.PATH;
case 'select':
if (selectOptionsProviderAction) {
return inputTypes.DYNAMIC_SELECT;
}
return inputTypes.SELECT;
case 'tag':
return inputTypes.TEXT_TAG;
@@ -45,7 +48,8 @@ function getSelectValues(selectOptions) {
return _.reduce(selectOptions, (result, option) => {
result.push({
key: option.value,
value: option.name
value: option.name,
hint: option.hint
});
return result;
@@ -86,7 +90,7 @@ function ProviderFieldFormGroup(props) {
<FormLabel>{label}</FormLabel>
<FormInputGroup
type={getType(type)}
type={getType(props)}
name={name}
label={label}
helpText={helpText}
@@ -106,7 +110,8 @@ function ProviderFieldFormGroup(props) {
const selectOptionsShape = {
name: PropTypes.string.isRequired,
value: PropTypes.number.isRequired
value: PropTypes.number.isRequired,
hint: PropTypes.string
};
ProviderFieldFormGroup.propTypes = {
@@ -123,6 +128,7 @@ ProviderFieldFormGroup.propTypes = {
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)),
selectOptionsProviderAction: PropTypes.string,
onChange: PropTypes.func.isRequired
};

View File

@@ -12,6 +12,14 @@
}
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from '~Components/Form/Input.css';
}
.internalInput {
flex: 1 1 0%;
margin-left: 3px;

View File

@@ -210,6 +210,8 @@ class TagInput extends Component {
const {
className,
inputContainerClassName,
hasError,
hasWarning,
...otherProps
} = this.props;
@@ -226,7 +228,9 @@ class TagInput extends Component {
className={styles.internalInput}
inputContainerClassName={classNames(
inputContainerClassName,
isFocused && styles.isFocused
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
value={value}
suggestions={suggestions}

View File

@@ -0,0 +1,53 @@
.inputWrapper {
display: flex;
}
.inputFolder {
composes: input from '~Components/Form/Input.css';
max-width: 100px;
}
.inputUnitWrapper {
position: relative;
width: 100%;
}
.inputUnit {
composes: inputUnit from '~Components/Form/FormInputGroup.css';
right: 40px;
font-family: $monoSpaceFontFamily;
}
.unit {
font-family: $monoSpaceFontFamily;
}
.details {
margin-top: 5px;
margin-left: 17px;
line-height: 20px;
> div {
display: flex;
label {
flex: 0 0 50px;
}
.value {
width: 50px;
text-align: right;
}
.unit {
width: 90px;
text-align: right;
}
}
}
.readOnly {
background-color: #eee;
}

View File

@@ -0,0 +1,133 @@
/* eslint-disable no-bitwise */
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import EnhancedSelectInput from './EnhancedSelectInput';
import styles from './UMaskInput.css';
const umaskOptions = [
{
key: '755',
value: '755 - Owner write, Everyone else read',
hint: 'drwxr-xr-x'
},
{
key: '775',
value: '775 - Owner & Group write, Other read',
hint: 'drwxrwxr-x'
},
{
key: '770',
value: '770 - Owner & Group write',
hint: 'drwxrwx---'
},
{
key: '750',
value: '750 - Owner write, Group read',
hint: 'drwxr-x---'
},
{
key: '777',
value: '777 - Everyone write',
hint: 'drwxrwxrwx'
}
];
function formatPermissions(permissions) {
const hasSticky = permissions & 0o1000;
const hasSetGID = permissions & 0o2000;
const hasSetUID = permissions & 0o4000;
let result = '';
for (let i = 0; i < 9; i++) {
const bit = (permissions & (1 << i)) !== 0;
let digit = bit ? 'xwr'[i % 3] : '-';
if (i === 6 && hasSetUID) {
digit = bit ? 's' : 'S';
} else if (i === 3 && hasSetGID) {
digit = bit ? 's' : 'S';
} else if (i === 0 && hasSticky) {
digit = bit ? 't' : 'T';
}
result = digit + result;
}
return result;
}
class UMaskInput extends Component {
//
// Render
render() {
const {
name,
value,
onChange
} = this.props;
const valueNum = parseInt(value, 8);
const umaskNum = 0o777 & ~valueNum;
const umask = umaskNum.toString(8).padStart(4, '0');
const folderNum = 0o777 & ~umaskNum;
const folder = folderNum.toString(8).padStart(3, '0');
const fileNum = 0o666 & ~umaskNum;
const file = fileNum.toString(8).padStart(3, '0');
const unit = formatPermissions(folderNum);
const values = umaskOptions.map((v) => {
return { ...v, hint: <span className={styles.unit}>{v.hint}</span> };
});
return (
<div>
<div className={styles.inputWrapper}>
<div className={styles.inputUnitWrapper}>
<EnhancedSelectInput
name={name}
value={value}
values={values}
isEditable={true}
onChange={onChange}
/>
<div className={styles.inputUnit}>
d{unit}
</div>
</div>
</div>
<div className={styles.details}>
<div>
<label>UMask</label>
<div className={styles.value}>{umask}</div>
</div>
<div>
<label>Folder</label>
<div className={styles.value}>{folder}</div>
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
</div>
<div>
<label>File</label>
<div className={styles.value}>{file}</div>
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
</div>
</div>
</div>
);
}
}
UMaskInput.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func,
onBlur: PropTypes.func
};
export default UMaskInput;

View File

@@ -1,12 +1,15 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import MenuButton from 'Components/Menu/MenuButton';
import { icons } from 'Helpers/Props';
import styles from './ToolbarMenuButton.css';
function ToolbarMenuButton(props) {
const {
iconName,
indicator,
text,
...otherProps
} = props;
@@ -22,6 +25,21 @@ function ToolbarMenuButton(props) {
size={21}
/>
{
indicator &&
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
name={icons.CIRCLE}
size={9}
/>
</span>
}
<div className={styles.labelContainer}>
<div className={styles.label}>
{text}
@@ -34,7 +52,8 @@ function ToolbarMenuButton(props) {
ToolbarMenuButton.propTypes = {
iconName: PropTypes.object.isRequired,
text: PropTypes.string
text: PropTypes.string,
indicator: PropTypes.bool.isRequired
};
export default ToolbarMenuButton;

View File

@@ -3,11 +3,20 @@ import React, { Component } from 'react';
import OverlayScroller from 'Components/Scroller/OverlayScroller';
import Scroller from 'Components/Scroller/Scroller';
import { scrollDirections } from 'Helpers/Props';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import { isLocked } from 'Utilities/scrollLock';
import styles from './PageContentBody.css';
class PageContentBody extends Component {
//
// Lifecyle
constructor(props, context) {
super(props, context);
this._isMobile = isMobileUtil();
}
//
// Listeners
@@ -26,13 +35,12 @@ class PageContentBody extends Component {
const {
className,
innerClassName,
isSmallScreen,
children,
dispatch,
...otherProps
} = this.props;
const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller;
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
return (
<ScrollerComponent
@@ -52,7 +60,6 @@ class PageContentBody extends Component {
PageContentBody.propTypes = {
className: PropTypes.string,
innerClassName: PropTypes.string,
isSmallScreen: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onScroll: PropTypes.func,
dispatch: PropTypes.func

View File

@@ -1,17 +1,17 @@
import React from 'react';
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
import PageContentBodyConnector from './PageContentBodyConnector';
import PageContentBody from './PageContentBody';
import styles from './PageContentError.css';
function PageContentError(props) {
return (
<div className={styles.content}>
<PageContentBodyConnector>
<PageContentBody>
<ErrorBoundaryError
{...props}
message='There was an error loading this page'
/>
</PageContentBodyConnector>
</PageContentBody>
</div>
);
}

View File

@@ -120,7 +120,7 @@ class SignalRConnector extends Component {
this.connection.on('receiveMessage', this.onReceiveMessage);
this.connection.start().then(this.onConnected);
this.connection.start().then(this.onStart, this.onStartFail);
}
componentWillUnmount() {
@@ -286,7 +286,19 @@ class SignalRConnector extends Component {
//
// Listeners
onConnected = () => {
onStartFail = (error) => {
console.error('[signalR] failed to connect');
console.error(error);
this.props.dispatchSetAppValue({
isConnected: false,
isReconnecting: false,
isDisconnected: false,
isRestarting: false
});
}
onStart = () => {
console.debug('[signalR] connected');
this.props.dispatchSetAppValue({

View File

@@ -2,7 +2,7 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import HTML5Backend from 'react-dnd-html5-backend';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';

View File

@@ -54,9 +54,9 @@ class Tooltip extends Component {
} else if ((/^bottom/).test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom - 20;
} else if ((/^right/).test(data.placement)) {
data.styles.maxWidth = windowWidth - right - 30;
data.styles.maxWidth = windowWidth - right - 35;
} else {
data.styles.maxWidth = left - 30;
data.styles.maxWidth = left - 35;
}
return data;

View File

@@ -12,12 +12,15 @@ export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
export const BOOK_EDITION_SELECT = 'bookEditionSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select';
export const SERIES_TYPE_SELECT = 'authorTypeSelect';
export const DYNAMIC_SELECT = 'dynamicSelect';
export const TAG = 'tag';
export const TEXT = 'text';
export const TEXT_TAG = 'textTag';
export const UMASK = 'umask';
export const all = [
AUTO_COMPLETE,
@@ -34,10 +37,13 @@ export const all = [
QUALITY_PROFILE_SELECT,
METADATA_PROFILE_SELECT,
BOOK_EDITION_SELECT,
INDEXER_SELECT,
ROOT_FOLDER_SELECT,
SELECT,
DYNAMIC_SELECT,
SERIES_TYPE_SELECT,
TAG,
TEXT,
TEXT_TAG
TEXT_TAG,
UMASK
];

View File

@@ -93,7 +93,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
>
<TableBody>
{
recentFolders.map((recentFolder) => {
recentFolders.slice(0).reverse().map((recentFolder) => {
return (
<RecentFolderRow
key={recentFolder.folder}

View File

@@ -55,6 +55,7 @@ const columns = [
{
name: 'size',
label: 'Size',
isSortable: true,
isVisible: true
},
{

View File

@@ -8,7 +8,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import { icons, kinds, sortDirections, tooltipPositions } from 'Helpers/Props';
import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
@@ -272,6 +272,7 @@ class InteractiveImportRow extends Component {
isOpen={isSelectBookModalOpen}
ids={[id]}
authorId={author && author.id}
sortDirection={sortDirections.ASCENDING}
onModalClose={this.onSelectBookModalClose}
/>

View File

@@ -2,9 +2,12 @@ import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, sortDirections } from 'Helpers/Props';
import { align, icons, sortDirections } from 'Helpers/Props';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRow from './InteractiveSearchRow';
import styles from './InteractiveSearch.css';
@@ -87,16 +90,33 @@ function InteractiveSearch(props) {
error,
totalReleasesCount,
items,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
type,
longDateFormat,
timeFormat,
onSortPress,
onFilterSelect,
onGrabPress
} = props;
return (
<div>
<div className={styles.filterMenuContainer}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
filterModalConnectorComponentProps={{ type }}
onFilterSelect={onFilterSelect}
/>
</div>
{
isFetching ? <LoadingIndicator /> : null
}
@@ -171,12 +191,16 @@ InteractiveSearch.propTypes = {
error: PropTypes.object,
totalReleasesCount: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
type: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired
};

View File

@@ -6,8 +6,9 @@ import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import AddNewAuthorSearchResultConnector from './Author/AddNewAuthorSearchResultConnector';
import AddNewBookSearchResultConnector from './Book/AddNewBookSearchResultConnector';
import styles from './AddNewItem.css';
@@ -87,7 +88,7 @@ class AddNewItem extends Component {
return (
<PageContent title="Add New Item">
<PageContentBodyConnector>
<PageContentBody>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon
@@ -122,8 +123,13 @@ class AddNewItem extends Component {
}
{
!isFetching && !!error &&
<div>Failed to load search results, please try again.</div>
!isFetching && !!error ?
<div className={styles.message}>
<div className={styles.helpText}>
Failed to load search results, please try again.
</div>
<div>{getErrorMessage(error)}</div>
</div> : null
}
{
@@ -182,7 +188,7 @@ class AddNewItem extends Component {
}
<div />
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -34,10 +34,20 @@
.content {
flex: 0 1 100%;
overflow: hidden;
}
.nameRow {
display: flex;
}
.nameContainer {
display: flex;
align-items: flex-end;
flex: 0 1 auto;
}
.name {
display: flex;
font-weight: 300;
font-size: 36px;
}
@@ -47,6 +57,14 @@
color: $disabledColor;
}
.icons {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1 0 auto;
height: 55px;
}
.mbLink {
composes: link from '~Components/Link/Link.css';
@@ -69,3 +87,10 @@
margin-top: 20px;
text-align: justify;
}
@media only screen and (max-width: $breakpointMedium) {
.titleRow {
justify-content: space-between;
overflow: hidden;
}
}

View File

@@ -9,7 +9,6 @@ import Link from 'Components/Link/Link';
import { icons, kinds, sizes } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import stripHtml from 'Utilities/String/stripHtml';
import AddNewAuthorModal from './AddNewAuthorModal';
import styles from './AddNewAuthorSearchResult.css';
@@ -113,44 +112,49 @@ class AddNewAuthorSearchResult extends Component {
}
<div className={styles.content}>
<div className={styles.name}>
{authorName}
<div className={styles.nameRow}>
<div className={styles.nameContainer}>
<div className={styles.name}>
{authorName}
{
!name.contains(year) && year ?
<span className={styles.year}>
({year})
</span> :
null
}
{
!authorName.contains(year) && year ?
<span className={styles.year}>
({year})
</span> :
null
}
{
!!disambiguation &&
<span className={styles.year}>({disambiguation})</span>
}
</div>
</div>
{
!!disambiguation &&
<span className={styles.year}>({disambiguation})</span>
}
<div className={styles.icons}>
{
isExistingAuthor ?
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title="Already in your library"
/> :
null
}
{
isExistingAuthor ?
<Link
className={styles.mbLink}
to={`https://goodreads.com/author/show/${foreignAuthorId}`}
onPress={this.onMBLinkPress}
>
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title="Already in your library"
/> :
null
}
<Link
className={styles.mbLink}
to={`https://goodreads.com/author/show/${foreignAuthorId}`}
onPress={this.onMBLinkPress}
>
<Icon
className={styles.mbLinkIcon}
name={icons.EXTERNAL_LINK}
size={28}
/>
</Link>
className={styles.mbLinkIcon}
name={icons.EXTERNAL_LINK}
size={28}
/>
</Link>
</div>
</div>
<div>
@@ -186,7 +190,7 @@ class AddNewAuthorSearchResult extends Component {
<TextTruncate
truncateText="…"
line={Math.floor(height / (defaultFontSize * lineHeight))}
text={stripHtml(overview)}
text={overview}
/>
</div>
</div>

View File

@@ -9,7 +9,6 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import stripHtml from 'Utilities/String/stripHtml';
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
import styles from './AddNewBookModalContent.css';
@@ -94,7 +93,7 @@ class AddNewBookModalContent extends Component {
<TextTruncate
truncateText="…"
line={8}
text={stripHtml(overview)}
text={overview}
/>
</div> :
null

View File

@@ -34,24 +34,37 @@
.content {
flex: 0 1 100%;
overflow: hidden;
}
.name {
.titleRow {
display: flex;
}
.titleContainer {
display: flex;
align-items: flex-end;
flex: 0 1 auto;
}
.title {
font-weight: 300;
font-size: 36px;
}
.authorName {
font-weight: 300;
font-size: 20px;
}
.year {
margin-left: 10px;
color: $disabledColor;
}
.icons {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1 0 auto;
height: 55px;
}
.mbLink {
composes: link from '~Components/Link/Link.css';
@@ -74,3 +87,10 @@
margin-top: 20px;
text-align: justify;
}
@media only screen and (max-width: $breakpointMedium) {
.titleRow {
justify-content: space-between;
overflow: hidden;
}
}

View File

@@ -10,7 +10,6 @@ import Link from 'Components/Link/Link';
import { icons, sizes } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import stripHtml from 'Utilities/String/stripHtml';
import AddNewBookModal from './AddNewBookModal';
import styles from './AddNewBookSearchResult.css';
@@ -112,52 +111,42 @@ class AddNewBookSearchResult extends Component {
}
<div className={styles.content}>
<div className={styles.name}>
{title}
<div className={styles.titleRow}>
<div className={styles.titleContainer}>
<div className={styles.title}>
{title}
{
!!disambiguation &&
<span className={styles.year}>({disambiguation})</span>
}
{
!!disambiguation &&
<span className={styles.year}>({disambiguation})</span>
}
</div>
</div>
{
isExistingBook ?
<div className={styles.icons}>
{
isExistingBook ?
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title="Already in your library"
/> :
null
}
<Link
className={styles.mbLink}
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
onPress={this.onTVDBLinkPress}
>
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={20}
title="Book already in your library"
/> :
null
}
<Link
className={styles.mbLink}
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
onPress={this.onMBLinkPress}
>
<Icon
className={styles.mbLinkIcon}
name={icons.EXTERNAL_LINK}
size={28}
/>
</Link>
</div>
<div>
<span className={styles.authorName}> By: {author.authorName}</span>
{
isExistingAuthor ?
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={15}
title="Author already in your library"
/> :
null
}
className={styles.mbLinkIcon}
name={icons.EXTERNAL_LINK}
size={28}
/>
</Link>
</div>
</div>
<div>
@@ -186,7 +175,7 @@ class AddNewBookSearchResult extends Component {
<TextTruncate
truncateText="…"
line={Math.floor(height / (defaultFontSize * lineHeight))}
text={stripHtml(overview)}
text={overview}
/>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
@@ -77,7 +77,7 @@ class DownloadClientSettings extends Component {
onSavePress={this.onSavePress}
/>
<PageContentBodyConnector>
<PageContentBody>
<DownloadClientsConnector />
<DownloadClientOptionsConnector
@@ -86,7 +86,7 @@ class DownloadClientSettings extends Component {
/>
<RemotePathMappingsConnector />
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -66,7 +66,7 @@ function BackupSettings(props) {
type={inputTypes.NUMBER}
name="backupRetention"
unit="days"
helpText="Automatic backups older than the retention will be cleaned up automatically"
helpText="Automatic backups older than the retention period will be cleaned up automatically"
onChange={onInputChange}
{...backupRetention}
/>

View File

@@ -5,7 +5,7 @@ import Form from 'Components/Form/Form';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import { kinds } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import AnalyticSettings from './AnalyticSettings';
@@ -26,8 +26,7 @@ const requiresRestartKeys = [
'sslCertPassword',
'authenticationMethod',
'username',
'password',
'apiKey'
'password'
];
class GeneralSettings extends Component {
@@ -47,9 +46,15 @@ class GeneralSettings extends Component {
const {
settings,
isSaving,
saveError
saveError,
isResettingApiKey
} = this.props;
if (!isResettingApiKey && prevProps.isResettingApiKey) {
this.setState({ isRestartRequiredModalOpen: true });
return;
}
if (isSaving || saveError || !prevProps.isSaving) {
return;
}
@@ -113,7 +118,7 @@ class GeneralSettings extends Component {
{...otherProps}
/>
<PageContentBodyConnector>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
@@ -176,7 +181,7 @@ class GeneralSettings extends Component {
/>
</Form>
}
</PageContentBodyConnector>
</PageContentBody>
<ConfirmModal
isOpen={this.state.isRestartRequiredModalOpen}

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
@@ -73,10 +73,10 @@ class ImportListSettings extends Component {
onSavePress={this.onSavePress}
/>
<PageContentBodyConnector>
<PageContentBody>
<ImportListsConnector />
<ImportListsExclusionsConnector />
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
@@ -76,14 +76,14 @@ class IndexerSettings extends Component {
onSavePress={this.onSavePress}
/>
<PageContentBodyConnector>
<PageContentBody>
<IndexersConnector />
<IndexerOptionsConnector
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -4,6 +4,11 @@
width: 290px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
@@ -12,6 +17,12 @@
font-size: 24px;
}
.cloneButton {
composes: button from '~Components/Link/IconButton.css';
height: 36px;
}
.enabled {
display: flex;
flex-wrap: wrap;

View File

@@ -2,8 +2,9 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import { kinds } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import EditIndexerModalConnector from './EditIndexerModalConnector';
import styles from './Indexer.css';
@@ -47,6 +48,15 @@ class Indexer extends Component {
this.props.onConfirmDeleteIndexer(this.props.id);
}
onCloneIndexerPress = () => {
const {
id,
onCloneIndexerPress
} = this.props;
onCloneIndexerPress(id);
}
//
// Render
@@ -69,8 +79,17 @@ class Indexer extends Component {
overlayContent={true}
onPress={this.onEditIndexerPress}
>
<div className={styles.name}>
{name}
<div className={styles.nameContainer}>
<div className={styles.name}>
{name}
</div>
<IconButton
className={styles.cloneButton}
title="Clone Profile"
name={icons.CLONE}
onPress={this.onCloneIndexerPress}
/>
</div>
<div className={styles.enabled}>
@@ -144,6 +163,7 @@ Indexer.propTypes = {
supportsRss: PropTypes.bool.isRequired,
supportsSearch: PropTypes.bool.isRequired,
showPriority: PropTypes.bool.isRequired,
onCloneIndexerPress: PropTypes.func.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired
};

View File

@@ -31,6 +31,11 @@ class Indexers extends Component {
this.setState({ isAddIndexerModalOpen: true });
}
onCloneIndexerPress = (id) => {
this.props.dispatchCloneIndexer({ id });
this.setState({ isEditIndexerModalOpen: true });
}
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
this.setState({
isAddIndexerModalOpen: false,
@@ -48,6 +53,7 @@ class Indexers extends Component {
render() {
const {
items,
dispatchCloneIndexer,
onConfirmDeleteIndexer,
...otherProps
} = this.props;
@@ -73,6 +79,7 @@ class Indexers extends Component {
key={item.id}
{...item}
showPriority={showPriority}
onCloneIndexerPress={this.onCloneIndexerPress}
onConfirmDeleteIndexer={onConfirmDeleteIndexer}
/>
);
@@ -111,6 +118,7 @@ Indexers.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired
};

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import Indexers from './Indexers';
@@ -15,8 +15,9 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchIndexers,
deleteIndexer
dispatchFetchIndexers: fetchIndexers,
dispatchDeleteIndexer: deleteIndexer,
dispatchCloneIndexer: cloneIndexer
};
class IndexersConnector extends Component {
@@ -25,14 +26,14 @@ class IndexersConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchIndexers();
this.props.dispatchFetchIndexers();
}
//
// Listeners
onConfirmDeleteIndexer = (id) => {
this.props.deleteIndexer({ id });
this.props.dispatchDeleteIndexer({ id });
}
//
@@ -49,8 +50,9 @@ class IndexersConnector extends Component {
}
IndexersConnector.propTypes = {
fetchIndexers: PropTypes.func.isRequired,
deleteIndexer: PropTypes.func.isRequired
dispatchFetchIndexers: PropTypes.func.isRequired,
dispatchDeleteIndexer: PropTypes.func.isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector);

View File

@@ -7,7 +7,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes, sizes } from 'Helpers/Props';
import RemotePathMappingsConnector from 'Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
@@ -63,7 +63,7 @@ class MediaManagement extends Component {
onSavePress={onSavePress}
/>
<PageContentBodyConnector>
<PageContentBody>
<RootFoldersConnector />
<RemotePathMappingsConnector />
<NamingConnector />
@@ -372,7 +372,7 @@ class MediaManagement extends Component {
<FormInputGroup
type={inputTypes.CHECK}
name="setPermissionsLinux"
helpText="Should chmod/chown be run when files are imported/renamed?"
helpText="Should chmod be run when files are imported/renamed?"
helpTextWarning="If you're unsure what these settings do, do not alter them."
onChange={onInputChange}
{...settings.setPermissionsLinux}
@@ -383,46 +383,15 @@ class MediaManagement extends Component {
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>File chmod mode</FormLabel>
<FormLabel>chmod Folder</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="fileChmod"
helpText="Octal, applied to media files when imported/renamed by Readarr"
type={inputTypes.UMASK}
name="chmodFolder"
helpText="Octal, applied during import/rename to media folders and files (without execute bits)"
helpTextWarning="This only works if the user running Lidarr is the owner of the file. It's better to ensure the download client sets the permissions properly."
onChange={onInputChange}
{...settings.fileChmod}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>Folder chmod mode</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="folderChmod"
helpText="Octal, applied to author/book folders created by Readarr"
values={fileDateOptions}
onChange={onInputChange}
{...settings.folderChmod}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>chown User</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="chownUser"
helpText="Username or uid. Use uid for remote file systems."
values={fileDateOptions}
onChange={onInputChange}
{...settings.chownUser}
{...settings.chmodFolder}
/>
</FormGroup>
@@ -436,6 +405,7 @@ class MediaManagement extends Component {
type={inputTypes.TEXT}
name="chownGroup"
helpText="Group name or gid. Use gid for remote file systems."
helpTextWarning="This only works if the user running Readarr is the owner of the file. It's better to ensure the download client uses the same group as Readarr."
values={fileDateOptions}
onChange={onInputChange}
{...settings.chownGroup}
@@ -445,7 +415,7 @@ class MediaManagement extends Component {
}
</Form>
}
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import MetadatasConnector from './Metadata/MetadatasConnector';
import MetadataProviderConnector from './MetadataProvider/MetadataProviderConnector';
@@ -54,13 +54,13 @@ class MetadataSettings extends Component {
onSavePress={this.onSavePress}
/>
<PageContentBodyConnector>
<PageContentBody>
<MetadataProviderConnector
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<MetadatasConnector />
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import NotificationsConnector from './Notifications/NotificationsConnector';
@@ -11,9 +11,9 @@ function NotificationSettings() {
showSave={false}
/>
<PageContentBodyConnector>
<PageContentBody>
<NotificationsConnector />
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -0,0 +1,60 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CheckInput from 'Components/Form/CheckInput';
import styles from './TypeItem.css';
class PrimaryTypeItem extends Component {
//
// Listeners
onAllowedChange = ({ value }) => {
const {
albumTypeId,
onMetadataPrimaryTypeItemAllowedChange
} = this.props;
onMetadataPrimaryTypeItemAllowedChange(albumTypeId, value);
}
//
// Render
render() {
const {
name,
allowed
} = this.props;
return (
<div
className={classNames(
styles.metadataProfileItem
)}
>
<label
className={styles.albumTypeName}
>
<CheckInput
containerClassName={styles.checkContainer}
name={name}
value={allowed}
onChange={this.onAllowedChange}
/>
{name}
</label>
</div>
);
}
}
PrimaryTypeItem.propTypes = {
albumTypeId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
sortIndex: PropTypes.number.isRequired,
onMetadataPrimaryTypeItemAllowedChange: PropTypes.func
};
export default PrimaryTypeItem;

View File

@@ -0,0 +1,87 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
import FormLabel from 'Components/Form/FormLabel';
import PrimaryTypeItem from './PrimaryTypeItem';
import styles from './TypeItems.css';
class PrimaryTypeItems extends Component {
//
// Render
render() {
const {
metadataProfileItems,
errors,
warnings,
...otherProps
} = this.props;
return (
<FormGroup>
<FormLabel>Primary Types</FormLabel>
<div>
{
errors.map((error, index) => {
return (
<FormInputHelpText
key={index}
text={error.message}
isError={true}
isCheckInput={false}
/>
);
})
}
{
warnings.map((warning, index) => {
return (
<FormInputHelpText
key={index}
text={warning.message}
isWarning={true}
isCheckInput={false}
/>
);
})
}
<div className={styles.albumTypes}>
{
metadataProfileItems.map(({ allowed, albumType }, index) => {
return (
<PrimaryTypeItem
key={albumType.id}
albumTypeId={albumType.id}
name={albumType.name}
allowed={allowed}
sortIndex={index}
{...otherProps}
/>
);
}).reverse()
}
</div>
</div>
</FormGroup>
);
}
}
PrimaryTypeItems.propTypes = {
metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object),
formLabel: PropTypes.string
};
PrimaryTypeItems.defaultProps = {
errors: [],
warnings: []
};
export default PrimaryTypeItems;

View File

@@ -0,0 +1,60 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CheckInput from 'Components/Form/CheckInput';
import styles from './TypeItem.css';
class ReleaseStatusItem extends Component {
//
// Listeners
onAllowedChange = ({ value }) => {
const {
albumTypeId,
onMetadataReleaseStatusItemAllowedChange
} = this.props;
onMetadataReleaseStatusItemAllowedChange(albumTypeId, value);
}
//
// Render
render() {
const {
name,
allowed
} = this.props;
return (
<div
className={classNames(
styles.metadataProfileItem
)}
>
<label
className={styles.albumTypeName}
>
<CheckInput
containerClassName={styles.checkContainer}
name={name}
value={allowed}
onChange={this.onAllowedChange}
/>
{name}
</label>
</div>
);
}
}
ReleaseStatusItem.propTypes = {
albumTypeId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
sortIndex: PropTypes.number.isRequired,
onMetadataReleaseStatusItemAllowedChange: PropTypes.func
};
export default ReleaseStatusItem;

View File

@@ -0,0 +1,87 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
import FormLabel from 'Components/Form/FormLabel';
import ReleaseStatusItem from './ReleaseStatusItem';
import styles from './TypeItems.css';
class ReleaseStatusItems extends Component {
//
// Render
render() {
const {
metadataProfileItems,
errors,
warnings,
...otherProps
} = this.props;
return (
<FormGroup>
<FormLabel>Release Statuses</FormLabel>
<div>
{
errors.map((error, index) => {
return (
<FormInputHelpText
key={index}
text={error.message}
isError={true}
isCheckInput={false}
/>
);
})
}
{
warnings.map((warning, index) => {
return (
<FormInputHelpText
key={index}
text={warning.message}
isWarning={true}
isCheckInput={false}
/>
);
})
}
<div className={styles.albumTypes}>
{
metadataProfileItems.map(({ allowed, releaseStatus }, index) => {
return (
<ReleaseStatusItem
key={releaseStatus.id}
albumTypeId={releaseStatus.id}
name={releaseStatus.name}
allowed={allowed}
sortIndex={index}
{...otherProps}
/>
);
})
}
</div>
</div>
</FormGroup>
);
}
}
ReleaseStatusItems.propTypes = {
metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object),
formLabel: PropTypes.string
};
ReleaseStatusItems.defaultProps = {
errors: [],
warnings: []
};
export default ReleaseStatusItems;

View File

@@ -0,0 +1,60 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CheckInput from 'Components/Form/CheckInput';
import styles from './TypeItem.css';
class SecondaryTypeItem extends Component {
//
// Listeners
onAllowedChange = ({ value }) => {
const {
albumTypeId,
onMetadataSecondaryTypeItemAllowedChange
} = this.props;
onMetadataSecondaryTypeItemAllowedChange(albumTypeId, value);
}
//
// Render
render() {
const {
name,
allowed
} = this.props;
return (
<div
className={classNames(
styles.metadataProfileItem
)}
>
<label
className={styles.albumTypeName}
>
<CheckInput
containerClassName={styles.checkContainer}
name={name}
value={allowed}
onChange={this.onAllowedChange}
/>
{name}
</label>
</div>
);
}
}
SecondaryTypeItem.propTypes = {
albumTypeId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
sortIndex: PropTypes.number.isRequired,
onMetadataSecondaryTypeItemAllowedChange: PropTypes.func
};
export default SecondaryTypeItem;

View File

@@ -0,0 +1,87 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
import FormLabel from 'Components/Form/FormLabel';
import SecondaryTypeItem from './SecondaryTypeItem';
import styles from './TypeItems.css';
class SecondaryTypeItems extends Component {
//
// Render
render() {
const {
metadataProfileItems,
errors,
warnings,
...otherProps
} = this.props;
return (
<FormGroup>
<FormLabel>Secondary Types</FormLabel>
<div>
{
errors.map((error, index) => {
return (
<FormInputHelpText
key={index}
text={error.message}
isError={true}
isCheckInput={false}
/>
);
})
}
{
warnings.map((warning, index) => {
return (
<FormInputHelpText
key={index}
text={warning.message}
isWarning={true}
isCheckInput={false}
/>
);
})
}
<div className={styles.albumTypes}>
{
metadataProfileItems.map(({ allowed, albumType }, index) => {
return (
<SecondaryTypeItem
key={albumType.id}
albumTypeId={albumType.id}
name={albumType.name}
allowed={allowed}
sortIndex={index}
{...otherProps}
/>
);
})
}
</div>
</div>
</FormGroup>
);
}
}
SecondaryTypeItems.propTypes = {
metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object),
formLabel: PropTypes.string
};
SecondaryTypeItems.defaultProps = {
errors: [],
warnings: []
};
export default SecondaryTypeItems;

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
import MetadataProfilesConnector from './Metadata/MetadataProfilesConnector';
@@ -24,14 +24,14 @@ class Profiles extends Component {
showSave={false}
/>
<PageContentBodyConnector>
<PageContentBody>
<DndProvider backend={HTML5Backend}>
<QualityProfilesConnector />
<MetadataProfilesConnector />
<DelayProfilesConnector />
<ReleaseProfilesConnector />
</DndProvider>
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -30,11 +30,13 @@ function EditReleaseProfileModalContent(props) {
const {
id,
enabled,
required,
ignored,
preferred,
includePreferredWhenRenaming,
tags
tags,
indexerId
} = item;
return (
@@ -45,6 +47,18 @@ function EditReleaseProfileModalContent(props) {
<ModalBody>
<Form {...otherProps}>
<FormGroup>
<FormLabel>Enable Profile</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enabled"
helpText="Check to enable release profile"
{...enabled}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Must Contain</FormLabel>
@@ -99,9 +113,23 @@ function EditReleaseProfileModalContent(props) {
<FormInputGroup
type={inputTypes.CHECK}
name="includePreferredWhenRenaming"
helpText="Include in {Preferred Words} renaming format"
helpText={indexerId.value === 0 ? 'Include in {Preferred Words} renaming format' : 'Only supported when Indexer is set to (All)'}
{...includePreferredWhenRenaming}
onChange={onInputChange}
isDisabled={indexerId.value !== 0}
/>
</FormGroup>
<FormGroup>
<FormLabel>Indexer</FormLabel>
<FormInputGroup
type={inputTypes.INDEXER_SELECT}
name="indexerId"
helpText="Specify what indexer the profile applies to"
{...indexerId}
includeAny={true}
onChange={onInputChange}
/>
</FormGroup>

View File

@@ -8,11 +8,13 @@ import selectSettings from 'Store/Selectors/selectSettings';
import EditReleaseProfileModalContent from './EditReleaseProfileModalContent';
const newReleaseProfile = {
enabled: true,
required: '',
ignored: '',
preferred: [],
includePreferredWhenRenaming: false,
tags: []
tags: [],
indexerId: 0
};
function createMapStateToProps() {

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
@@ -55,11 +56,14 @@ class ReleaseProfile extends Component {
render() {
const {
id,
enabled,
required,
ignored,
preferred,
tags,
tagList
indexerId,
tagList,
indexerList
} = this.props;
const {
@@ -67,6 +71,8 @@ class ReleaseProfile extends Component {
isDeleteReleaseProfileModalOpen
} = this.state;
const indexer = indexerId !== 0 && _.find(indexerList, { id: indexerId });
return (
<Card
className={styles.releaseProfile}
@@ -92,6 +98,23 @@ class ReleaseProfile extends Component {
}
</div>
<div>
{
preferred.map((item) => {
const isPreferred = item.value >= 0;
return (
<Label
key={item.key}
kind={isPreferred ? kinds.DEFAULT : kinds.WARNING}
>
{item.key} {isPreferred && '+'}{item.value}
</Label>
);
})
}
</div>
<div>
{
split(ignored).map((item) => {
@@ -111,28 +134,33 @@ class ReleaseProfile extends Component {
}
</div>
<div>
{
preferred.map((item) => {
const isPreferred = item.value >= 0;
return (
<Label
key={item.key}
kind={isPreferred ? kinds.DEFAULT : kinds.WARNING}
>
{item.key} {isPreferred && '+'}{item.value}
</Label>
);
})
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<div>
{
!enabled &&
<Label
kind={kinds.DISABLED}
outline={true}
>
Disabled
</Label>
}
{
indexer &&
<Label
kind={kinds.INFO}
outline={true}
>
{indexer.name}
</Label>
}
</div>
<EditReleaseProfileModalConnector
id={id}
isOpen={isEditReleaseProfileModalOpen}
@@ -156,18 +184,23 @@ class ReleaseProfile extends Component {
ReleaseProfile.propTypes = {
id: PropTypes.number.isRequired,
enabled: PropTypes.bool.isRequired,
required: PropTypes.string.isRequired,
ignored: PropTypes.string.isRequired,
preferred: PropTypes.arrayOf(PropTypes.object).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerId: PropTypes.number.isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
};
ReleaseProfile.defaultProps = {
enabled: true,
required: '',
ignored: '',
preferred: []
preferred: [],
indexerId: 0
};
export default ReleaseProfile;

View File

@@ -40,6 +40,7 @@ class ReleaseProfiles extends Component {
const {
items,
tagList,
indexerList,
onConfirmDeleteReleaseProfile,
...otherProps
} = this.props;
@@ -69,6 +70,7 @@ class ReleaseProfiles extends Component {
<ReleaseProfile
key={item.id}
tagList={tagList}
indexerList={indexerList}
{...item}
onConfirmDeleteReleaseProfile={onConfirmDeleteReleaseProfile}
/>
@@ -92,6 +94,7 @@ ReleaseProfiles.propTypes = {
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
};

View File

@@ -2,24 +2,28 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteReleaseProfile, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
import { deleteReleaseProfile, fetchIndexers, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import ReleaseProfiles from './ReleaseProfiles';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.releaseProfiles,
(state) => state.settings.indexers,
createTagsSelector(),
(releaseProfiles, tagList) => {
(releaseProfiles, indexers, tagList) => {
return {
...releaseProfiles,
tagList
tagList,
isIndexersPopulated: indexers.isPopulated,
indexerList: indexers.items
};
}
);
}
const mapDispatchToProps = {
fetchIndexers,
fetchReleaseProfiles,
deleteReleaseProfile
};
@@ -31,6 +35,9 @@ class ReleaseProfilesConnector extends Component {
componentDidMount() {
this.props.fetchReleaseProfiles();
if (!this.props.isIndexersPopulated) {
this.props.fetchIndexers();
}
}
//
@@ -54,8 +61,10 @@ class ReleaseProfilesConnector extends Component {
}
ReleaseProfilesConnector.propTypes = {
isIndexersPopulated: PropTypes.bool.isRequired,
fetchReleaseProfiles: PropTypes.func.isRequired,
deleteReleaseProfile: PropTypes.func.isRequired
deleteReleaseProfile: PropTypes.func.isRequired,
fetchIndexers: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector);

View File

@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector';
@@ -54,12 +54,12 @@ class Quality extends Component {
onSavePress={this.onSavePress}
/>
<PageContentBodyConnector>
<PageContentBody>
<QualityDefinitionsConnector
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import Link from 'Components/Link/Link';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from './SettingsToolbarConnector';
import styles from './Settings.css';
@@ -12,7 +12,7 @@ function Settings() {
hasPendingChanges={false}
/>
<PageContentBodyConnector>
<PageContentBody>
<Link
className={styles.link}
to="/settings/mediamanagement"
@@ -133,7 +133,7 @@ function Settings() {
<div className={styles.summary}>
Calendar, date and color impaired options
</div>
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -9,7 +9,7 @@ function findMatchingItems(ids, items) {
});
}
function createMatchingAuthorSelector() {
function createUnorderedMatchingAuthorSelector() {
return createSelector(
(state, { authorIds }) => authorIds,
createAllAuthorSelector(),
@@ -17,6 +17,26 @@ function createMatchingAuthorSelector() {
);
}
function createMatchingAuthorSelector() {
return createSelector(
createUnorderedMatchingAuthorSelector(),
(authors) => {
return authors.sort((authorA, authorB) => {
const sortNameA = authorA.sortName;
const sortNameB = authorB.sortName;
if (sortNameA > sortNameB) {
return 1;
} else if (sortNameA < sortNameB) {
return -1;
}
return 0;
});
}
);
}
function createMatchingDelayProfilesSelector() {
return createSelector(
(state, { delayProfileIds }) => delayProfileIds,

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import TagsConnector from './TagsConnector';
@@ -11,9 +11,9 @@ function TagSettings() {
showSave={false}
/>
<PageContentBodyConnector>
<PageContentBody>
<TagsConnector />
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -7,7 +7,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
@@ -65,7 +65,7 @@ class UISettings extends Component {
onSavePress={onSavePress}
/>
<PageContentBodyConnector>
<PageContentBody>
{
isFetching &&
<LoadingIndicator />
@@ -176,7 +176,7 @@ class UISettings extends Component {
</FieldSet>
</Form>
}
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -8,7 +8,9 @@ import createTestProviderHandler, { createCancelTestProviderHandler } from 'Stor
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
import updateSectionState from 'Utilities/State/updateSectionState';
//
// Variables
@@ -21,6 +23,7 @@ const section = 'settings.indexers';
export const FETCH_INDEXERS = 'settings/indexers/fetchIndexers';
export const FETCH_INDEXER_SCHEMA = 'settings/indexers/fetchIndexerSchema';
export const SELECT_INDEXER_SCHEMA = 'settings/indexers/selectIndexerSchema';
export const CLONE_INDEXER = 'settings/indexers/cloneIndexer';
export const SET_INDEXER_VALUE = 'settings/indexers/setIndexerValue';
export const SET_INDEXER_FIELD_VALUE = 'settings/indexers/setIndexerFieldValue';
export const SAVE_INDEXER = 'settings/indexers/saveIndexer';
@@ -36,6 +39,7 @@ export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
export const fetchIndexers = createThunk(FETCH_INDEXERS);
export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA);
export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA);
export const cloneIndexer = createAction(CLONE_INDEXER);
export const saveIndexer = createThunk(SAVE_INDEXER);
export const cancelSaveIndexer = createThunk(CANCEL_SAVE_INDEXER);
@@ -113,6 +117,30 @@ export default {
return selectedSchema;
});
},
[CLONE_INDEXER]: function(state, { payload }) {
const id = payload.id;
const newState = getSectionState(state, section);
const item = newState.items.find((i) => i.id === id);
// Use selectedSchema so `createProviderSettingsSelector` works properly
const selectedSchema = { ...item };
delete selectedSchema.id;
delete selectedSchema.name;
selectedSchema.fields = selectedSchema.fields.map((field) => {
return { ...field };
});
newState.selectedSchema = selectedSchema;
// Set the name in pendingChanges
newState.pendingChanges = {
name: `${item.name} - Copy`
};
return updateSectionState(state, section, newState);
}
}

View File

@@ -114,7 +114,9 @@ export const filterPredicates = {
sizeOnDisk: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const sizeOnDisk = item.statistics ? item.statistics.sizeOnDisk : 0;
const sizeOnDisk = item.statistics && item.statistics.sizeOnDisk ?
item.statistics.sizeOnDisk :
0;
return predicate(sizeOnDisk, filterValue);
}
@@ -133,6 +135,12 @@ export const sortPredicates = {
}
return result;
},
sizeOnDisk: function(item) {
const { statistics = {} } = item;
return statistics.sizeOnDisk || 0;
}
};

View File

@@ -8,6 +8,7 @@ import { set, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
//
// Variables
@@ -30,6 +31,58 @@ export const defaultState = {
filters,
filterPredicates,
columns: [
{
name: 'status',
columnLabel: 'Status',
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'sortName',
label: 'Name',
isSortable: true,
isVisible: true
},
{
name: 'qualityProfileId',
label: 'Quality Profile',
isSortable: true,
isVisible: true
},
{
name: 'metadataProfileId',
label: 'Metadata Profile',
isSortable: true,
isVisible: false
},
{
name: 'albumFolder',
label: 'Album Folder',
isSortable: true,
isVisible: true
},
{
name: 'path',
label: 'Path',
isSortable: true,
isVisible: true
},
{
name: 'sizeOnDisk',
label: 'Size on Disk',
isSortable: true,
isVisible: false
},
{
name: 'tags',
label: 'Tags',
isSortable: false,
isVisible: true
}
],
filterBuilderProps: [
{
name: 'monitored',
@@ -65,6 +118,12 @@ export const defaultState = {
label: 'Root Folder Path',
type: filterBuilderTypes.EXACT
},
{
name: 'sizeOnDisk',
label: 'Size on Disk',
type: filterBuilderTypes.NUMBER,
valueType: filterBuilderValueTypes.BYTES
},
{
name: 'tags',
label: 'Tags',
@@ -90,6 +149,7 @@ export const SET_AUTHOR_EDITOR_SORT = 'authorEditor/setAuthorEditorSort';
export const SET_AUTHOR_EDITOR_FILTER = 'authorEditor/setAuthorEditorFilter';
export const SAVE_AUTHOR_EDITOR = 'authorEditor/saveAuthorEditor';
export const BULK_DELETE_AUTHOR = 'authorEditor/bulkDeleteAuthor';
export const SET_AUTHOR_EDITOR_TABLE_OPTION = 'authorEditor/setAuthorEditorTableOption';
//
// Action Creators
@@ -98,6 +158,7 @@ export const setAuthorEditorSort = createAction(SET_AUTHOR_EDITOR_SORT);
export const setAuthorEditorFilter = createAction(SET_AUTHOR_EDITOR_FILTER);
export const saveAuthorEditor = createThunk(SAVE_AUTHOR_EDITOR);
export const bulkDeleteAuthor = createThunk(BULK_DELETE_AUTHOR);
export const setAuthorEditorTableOption = createAction(SET_AUTHOR_EDITOR_TABLE_OPTION);
//
// Action Handlers
@@ -181,6 +242,7 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({
[SET_AUTHOR_EDITOR_TABLE_OPTION]: createSetTableOptionReducer(section),
[SET_AUTHOR_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
[SET_AUTHOR_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)

View File

@@ -180,13 +180,7 @@ export const defaultState = {
bookCount: function(item) {
const { statistics = {} } = item;
return statistics.bookCount;
},
sizeOnDisk: function(item) {
const { statistics = {} } = item;
return statistics.sizeOnDisk;
return statistics.bookCount || 0;
},
ratings: function(item) {

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