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 1. Fork Readarr
2. Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories) 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` 3. Grab the submodules `git submodule init && git submodule update`
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command. 4. Install the required Node Packages `yarn install`
5. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `netcoreapp31` 5. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
6. Debug the project in Visual Studio 6. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `netcoreapp31`
7. Open http://localhost:8787 7. Debug the project in Visual Studio
8. Open http://localhost:8787
### Contributing Code ### ### 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) - 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) [![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) [![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) [![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. ### 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 chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh ${OSNAME} Automation Test ${TESTSFOLDER}/test.sh ${OSNAME} Automation Test
displayName: Run Automation Tests 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 - task: PublishTestResults@2
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'

View File

@@ -1,8 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent'; 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 PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
@@ -10,11 +11,83 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager'; 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'; import BlacklistRowConnector from './BlacklistRowConnector';
class Blacklist extends Component { 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 // Render
@@ -28,6 +101,7 @@ class Blacklist extends Component {
items, items,
columns, columns,
totalRecords, totalRecords,
isRemoving,
isClearingBlacklistExecuting, isClearingBlacklistExecuting,
onClearBlacklistPress, onClearBlacklistPress,
...otherProps ...otherProps
@@ -36,10 +110,27 @@ class Blacklist extends Component {
const isAllPopulated = isPopulated && isAuthorPopulated; const isAllPopulated = isPopulated && isAuthorPopulated;
const isAnyFetching = isFetching || isAuthorFetching; const isAnyFetching = isFetching || isAuthorFetching;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return ( return (
<PageContent title="Blacklist"> <PageContent title="Blacklist">
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton <PageToolbarButton
label="Clear" label="Clear"
iconName={icons.CLEAR} iconName={icons.CLEAR}
@@ -61,7 +152,7 @@ class Blacklist extends Component {
</PageToolbarSection> </PageToolbarSection>
</PageToolbar> </PageToolbar>
<PageContentBodyConnector> <PageContentBody>
{ {
isAnyFetching && !isAllPopulated && isAnyFetching && !isAllPopulated &&
<LoadingIndicator /> <LoadingIndicator />
@@ -83,8 +174,12 @@ class Blacklist extends Component {
isAllPopulated && !error && !!items.length && isAllPopulated && !error && !!items.length &&
<div> <div>
<Table <Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns} columns={columns}
{...otherProps} {...otherProps}
onSelectAllChange={this.onSelectAllChange}
> >
<TableBody> <TableBody>
{ {
@@ -92,8 +187,10 @@ class Blacklist extends Component {
return ( return (
<BlacklistRowConnector <BlacklistRowConnector
key={item.id} key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns} columns={columns}
{...item} {...item}
onSelectedChange={this.onSelectedChange}
/> />
); );
}) })
@@ -108,7 +205,17 @@ class Blacklist extends Component {
/> />
</div> </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> </PageContent>
); );
} }
@@ -123,7 +230,9 @@ Blacklist.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number, totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired, isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired onClearBlacklistPress: PropTypes.func.isRequired
}; };

View File

@@ -92,6 +92,10 @@ class BlacklistConnector extends Component {
this.props.gotoBlacklistPage({ page }); this.props.gotoBlacklistPage({ page });
} }
onRemoveSelected = (ids) => {
this.props.removeBlacklistItems({ ids });
}
onSortPress = (sortKey) => { onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey }); this.props.setBlacklistSort({ sortKey });
} }
@@ -119,6 +123,7 @@ class BlacklistConnector extends Component {
onNextPagePress={this.onNextPagePress} onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress} onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect} onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress} onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange} onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress} onClearBlacklistPress={this.onClearBlacklistPress}
@@ -138,6 +143,7 @@ BlacklistConnector.propTypes = {
gotoBlacklistNextPage: PropTypes.func.isRequired, gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired, gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired, gotoBlacklistPage: PropTypes.func.isRequired,
removeBlacklistItems: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired, setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired, setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: 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 IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import BlacklistDetailsModal from './BlacklistDetailsModal'; import BlacklistDetailsModal from './BlacklistDetailsModal';
@@ -39,6 +40,7 @@ class BlacklistRow extends Component {
render() { render() {
const { const {
id,
author, author,
sourceTitle, sourceTitle,
quality, quality,
@@ -46,7 +48,9 @@ class BlacklistRow extends Component {
protocol, protocol,
indexer, indexer,
message, message,
isSelected,
columns, columns,
onSelectedChange,
onRemovePress onRemovePress
} = this.props; } = this.props;
@@ -56,6 +60,12 @@ class BlacklistRow extends Component {
return ( return (
<TableRow> <TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{ {
columns.map((column) => { columns.map((column) => {
const { const {
@@ -167,7 +177,9 @@ BlacklistRow.propTypes = {
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
indexer: PropTypes.string, indexer: PropTypes.string,
message: PropTypes.string, message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired onRemovePress: PropTypes.func.isRequired
}; };

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { removeFromBlacklist } from 'Store/Actions/blacklistActions'; import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector'; import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import BlacklistRow from './BlacklistRow'; import BlacklistRow from './BlacklistRow';
@@ -18,7 +18,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onRemovePress() { 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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu'; import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent'; 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 PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
@@ -98,7 +98,7 @@ class History extends Component {
</PageToolbarSection> </PageToolbarSection>
</PageToolbar> </PageToolbar>
<PageContentBodyConnector> <PageContentBody>
{ {
isFetchingAny && !isAllPopulated && isFetchingAny && !isAllPopulated &&
<LoadingIndicator /> <LoadingIndicator />
@@ -149,7 +149,7 @@ class History extends Component {
/> />
</div> </div>
} }
</PageContentBodyConnector> </PageContentBody>
</PageContent> </PageContent>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -138,7 +138,7 @@ class AuthorEditorFooter extends Component {
isDeleting, isDeleting,
isOrganizingAuthor, isOrganizingAuthor,
isRetaggingAuthor, isRetaggingAuthor,
showMetadataProfile, columns,
onOrganizeAuthorPress, onOrganizeAuthorPress,
onRetagAuthorPress onRetagAuthorPress
} = this.props; } = this.props;
@@ -147,6 +147,7 @@ class AuthorEditorFooter extends Component {
monitored, monitored,
qualityProfileId, qualityProfileId,
metadataProfileId, metadataProfileId,
bookFolder,
rootFolderPath, rootFolderPath,
savingTags, savingTags,
isTagsModalOpen, isTagsModalOpen,
@@ -161,6 +162,12 @@ class AuthorEditorFooter extends Component {
{ key: 'unmonitored', value: 'Unmonitored' } { key: 'unmonitored', value: 'Unmonitored' }
]; ];
const bookFolderOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true },
{ key: 'yes', value: 'Yes' },
{ key: 'no', value: 'No' }
];
return ( return (
<PageContentFooter> <PageContentFooter>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
@@ -178,56 +185,110 @@ class AuthorEditorFooter extends Component {
/> />
</div> </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 && columns.map((column) => {
<div className={styles.inputContainer}> const {
<AuthorEditorFooterLabel name,
label="Metadata Profile" isVisible
isSaving={isSaving && metadataProfileId !== NO_CHANGE} } = column;
/>
<MetadataProfileSelectInputConnector if (!isVisible) {
name="metadataProfileId" return null;
value={metadataProfileId} }
includeNoChange={true}
includeNone={true} if (name === 'qualityProfileId') {
isDisabled={!selectedCount} return (
onChange={this.onInputChange} <div
/> key={name}
</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>
);
}
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.buttonContainer}>
<div className={styles.buttonContainerContent}> <div className={styles.buttonContainerContent}>
<AuthorEditorFooterLabel <AuthorEditorFooterLabel
@@ -315,6 +376,7 @@ AuthorEditorFooter.propTypes = {
isOrganizingAuthor: PropTypes.bool.isRequired, isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired, isRetaggingAuthor: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSaveSelected: PropTypes.func.isRequired, onSaveSelected: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired, onOrganizeAuthorPress: PropTypes.func.isRequired,
onRetagAuthorPress: 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 PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import AuthorNameLink from 'Author/AuthorNameLink'; import AuthorNameLink from 'Author/AuthorNameLink';
import AuthorStatusCell from 'Author/Index/Table/AuthorStatusCell'; import AuthorStatusCell from 'Author/Index/Table/AuthorStatusCell';
import CheckInput from 'Components/Form/CheckInput';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import TagListConnector from 'Components/TagListConnector'; import TagListConnector from 'Components/TagListConnector';
import formatBytes from 'Utilities/Number/formatBytes';
import styles from './AuthorEditorRow.css';
class AuthorEditorRow extends Component { class AuthorEditorRow extends Component {
@@ -25,16 +27,20 @@ class AuthorEditorRow extends Component {
const { const {
id, id,
status, status,
titleSlug, foreignAuthorId,
authorName, authorName,
authorType, authorType,
bookFolder,
monitored, monitored,
metadataProfile, metadataProfile,
qualityProfile, qualityProfile,
path, path,
statistics,
tags, tags,
columns, columns,
isSaving,
isSelected, isSelected,
onAuthorMonitoredPress,
onSelectedChange onSelectedChange
} = this.props; } = this.props;
@@ -46,39 +52,105 @@ class AuthorEditorRow extends Component {
onSelectedChange={onSelectedChange} 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 && columns.map((column) => {
<TableRowCell> const {
{metadataProfile.name} name,
</TableRowCell> 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> </TableRow>
); );
} }
@@ -87,16 +159,21 @@ class AuthorEditorRow extends Component {
AuthorEditorRow.propTypes = { AuthorEditorRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
foreignAuthorId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired, authorName: PropTypes.string.isRequired,
authorType: PropTypes.string, authorType: PropTypes.string,
bookFolder: PropTypes.string,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
metadataProfile: PropTypes.object.isRequired, metadataProfile: PropTypes.object.isRequired,
qualityProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
onAuthorMonitoredPress: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired onSelectedChange: PropTypes.func.isRequired
}; };

View File

@@ -25,7 +25,7 @@ function OrganizeAuthorModalContent(props) {
<ModalBody> <ModalBody>
<Alert> <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 <Icon
className={styles.renameIcon} className={styles.renameIcon}
name={icons.ORGANIZE} name={icons.ORGANIZE}

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import React, { Component } from 'react';
import NoAuthor from 'Author/NoAuthor'; import NoAuthor from 'Author/NoAuthor';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; 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 PageJumpBar from 'Components/Page/PageJumpBar';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -312,7 +312,7 @@ class AuthorIndex extends Component {
</PageToolbar> </PageToolbar>
<div className={styles.pageContentBodyWrapper}> <div className={styles.pageContentBodyWrapper}>
<PageContentBodyConnector <PageContentBody
registerScroller={this.setScrollerRef} registerScroller={this.setScrollerRef}
className={styles.contentBody} className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]} innerClassName={styles[`${view}InnerContentBody`]}
@@ -351,7 +351,7 @@ class AuthorIndex extends Component {
!error && isPopulated && !items.length && !error && isPopulated && !items.length &&
<NoAuthor totalItems={totalItems} /> <NoAuthor totalItems={totalItems} />
} }
</PageContentBodyConnector> </PageContentBody>
{ {
isLoaded && !!jumpBarItems.order.length && 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 { setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import scrollPositions from 'Store/scrollPositions'; import scrollPositions from 'Store/scrollPositions';
import createAuthorClientSideCollectionItemsSelector import createAuthorClientSideCollectionItemsSelector from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import AuthorIndex from './AuthorIndex'; import AuthorIndex from './AuthorIndex';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,43 @@
import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Label from 'Components/Label';
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
import styles from './HintedSelectInputSelectedValue.css'; import styles from './HintedSelectInputSelectedValue.css';
function HintedSelectInputSelectedValue(props) { function HintedSelectInputSelectedValue(props) {
const { const {
value, value,
values,
hint, hint,
isMultiSelect,
includeHint, includeHint,
...otherProps ...otherProps
} = props; } = props;
const valuesMap = isMultiSelect && _.keyBy(values, 'key');
return ( return (
<EnhancedSelectInputSelectedValue <EnhancedSelectInputSelectedValue
className={styles.selectedValue} className={styles.selectedValue}
{...otherProps} {...otherProps}
> >
<div className={styles.valueText}> <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> </div>
{ {
@@ -31,12 +51,15 @@ function HintedSelectInputSelectedValue(props) {
} }
HintedSelectInputSelectedValue.propTypes = { 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, hint: PropTypes.string,
isMultiSelect: PropTypes.bool.isRequired,
includeHint: PropTypes.bool.isRequired includeHint: PropTypes.bool.isRequired
}; };
HintedSelectInputSelectedValue.defaultProps = { HintedSelectInputSelectedValue.defaultProps = {
isMultiSelect: false,
includeHint: true 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, className,
value, value,
keyPlaceholder, keyPlaceholder,
valuePlaceholder valuePlaceholder,
hasError,
hasWarning
} = this.props; } = this.props;
const { isFocused } = this.state; const { isFocused } = this.state;
@@ -106,7 +108,9 @@ class KeyValueListInput extends Component {
return ( return (
<div className={classNames( <div className={classNames(
className, 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 FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
function getType(type) { function getType({ type, selectOptionsProviderAction }) {
switch (type) { switch (type) {
case 'captcha': case 'captcha':
return inputTypes.CAPTCHA; return inputTypes.CAPTCHA;
@@ -25,6 +25,9 @@ function getType(type) {
case 'filePath': case 'filePath':
return inputTypes.PATH; return inputTypes.PATH;
case 'select': case 'select':
if (selectOptionsProviderAction) {
return inputTypes.DYNAMIC_SELECT;
}
return inputTypes.SELECT; return inputTypes.SELECT;
case 'tag': case 'tag':
return inputTypes.TEXT_TAG; return inputTypes.TEXT_TAG;
@@ -45,7 +48,8 @@ function getSelectValues(selectOptions) {
return _.reduce(selectOptions, (result, option) => { return _.reduce(selectOptions, (result, option) => {
result.push({ result.push({
key: option.value, key: option.value,
value: option.name value: option.name,
hint: option.hint
}); });
return result; return result;
@@ -86,7 +90,7 @@ function ProviderFieldFormGroup(props) {
<FormLabel>{label}</FormLabel> <FormLabel>{label}</FormLabel>
<FormInputGroup <FormInputGroup
type={getType(type)} type={getType(props)}
name={name} name={name}
label={label} label={label}
helpText={helpText} helpText={helpText}
@@ -106,7 +110,8 @@ function ProviderFieldFormGroup(props) {
const selectOptionsShape = { const selectOptionsShape = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.number.isRequired value: PropTypes.number.isRequired,
hint: PropTypes.string
}; };
ProviderFieldFormGroup.propTypes = { ProviderFieldFormGroup.propTypes = {
@@ -123,6 +128,7 @@ ProviderFieldFormGroup.propTypes = {
errors: PropTypes.arrayOf(PropTypes.object).isRequired, errors: PropTypes.arrayOf(PropTypes.object).isRequired,
warnings: PropTypes.arrayOf(PropTypes.object).isRequired, warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)), selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)),
selectOptionsProviderAction: PropTypes.string,
onChange: PropTypes.func.isRequired 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 { .internalInput {
flex: 1 1 0%; flex: 1 1 0%;
margin-left: 3px; margin-left: 3px;

View File

@@ -210,6 +210,8 @@ class TagInput extends Component {
const { const {
className, className,
inputContainerClassName, inputContainerClassName,
hasError,
hasWarning,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -226,7 +228,9 @@ class TagInput extends Component {
className={styles.internalInput} className={styles.internalInput}
inputContainerClassName={classNames( inputContainerClassName={classNames(
inputContainerClassName, inputContainerClassName,
isFocused && styles.isFocused isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)} )}
value={value} value={value}
suggestions={suggestions} 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 PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import MenuButton from 'Components/Menu/MenuButton'; import MenuButton from 'Components/Menu/MenuButton';
import { icons } from 'Helpers/Props';
import styles from './ToolbarMenuButton.css'; import styles from './ToolbarMenuButton.css';
function ToolbarMenuButton(props) { function ToolbarMenuButton(props) {
const { const {
iconName, iconName,
indicator,
text, text,
...otherProps ...otherProps
} = props; } = props;
@@ -22,6 +25,21 @@ function ToolbarMenuButton(props) {
size={21} 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.labelContainer}>
<div className={styles.label}> <div className={styles.label}>
{text} {text}
@@ -34,7 +52,8 @@ function ToolbarMenuButton(props) {
ToolbarMenuButton.propTypes = { ToolbarMenuButton.propTypes = {
iconName: PropTypes.object.isRequired, iconName: PropTypes.object.isRequired,
text: PropTypes.string text: PropTypes.string,
indicator: PropTypes.bool.isRequired
}; };
export default ToolbarMenuButton; export default ToolbarMenuButton;

View File

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

View File

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

View File

@@ -120,7 +120,7 @@ class SignalRConnector extends Component {
this.connection.on('receiveMessage', this.onReceiveMessage); this.connection.on('receiveMessage', this.onReceiveMessage);
this.connection.start().then(this.onConnected); this.connection.start().then(this.onStart, this.onStartFail);
} }
componentWillUnmount() { componentWillUnmount() {
@@ -286,7 +286,19 @@ class SignalRConnector extends Component {
// //
// Listeners // 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'); console.debug('[signalR] connected');
this.props.dispatchSetAppValue({ this.props.dispatchSetAppValue({

View File

@@ -2,7 +2,7 @@ import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { DndProvider } from 'react-dnd'; 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 Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,12 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; 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 Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; 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 InteractiveSearchRow from './InteractiveSearchRow';
import styles from './InteractiveSearch.css'; import styles from './InteractiveSearch.css';
@@ -87,16 +90,33 @@ function InteractiveSearch(props) {
error, error,
totalReleasesCount, totalReleasesCount,
items, items,
selectedFilterKey,
filters,
customFilters,
sortKey, sortKey,
sortDirection, sortDirection,
type,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
onSortPress, onSortPress,
onFilterSelect,
onGrabPress onGrabPress
} = props; } = props;
return ( return (
<div> <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 isFetching ? <LoadingIndicator /> : null
} }
@@ -171,12 +191,16 @@ InteractiveSearch.propTypes = {
error: PropTypes.object, error: PropTypes.object,
totalReleasesCount: PropTypes.number.isRequired, totalReleasesCount: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).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, sortKey: PropTypes.string,
sortDirection: PropTypes.string, sortDirection: PropTypes.string,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: 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 Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; import PageContentBody from 'Components/Page/PageContentBody';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import AddNewAuthorSearchResultConnector from './Author/AddNewAuthorSearchResultConnector'; import AddNewAuthorSearchResultConnector from './Author/AddNewAuthorSearchResultConnector';
import AddNewBookSearchResultConnector from './Book/AddNewBookSearchResultConnector'; import AddNewBookSearchResultConnector from './Book/AddNewBookSearchResultConnector';
import styles from './AddNewItem.css'; import styles from './AddNewItem.css';
@@ -87,7 +88,7 @@ class AddNewItem extends Component {
return ( return (
<PageContent title="Add New Item"> <PageContent title="Add New Item">
<PageContentBodyConnector> <PageContentBody>
<div className={styles.searchContainer}> <div className={styles.searchContainer}>
<div className={styles.searchIconContainer}> <div className={styles.searchIconContainer}>
<Icon <Icon
@@ -122,8 +123,13 @@ class AddNewItem extends Component {
} }
{ {
!isFetching && !!error && !isFetching && !!error ?
<div>Failed to load search results, please try again.</div> <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 /> <div />
</PageContentBodyConnector> </PageContentBody>
</PageContent> </PageContent>
); );
} }

View File

@@ -34,10 +34,20 @@
.content { .content {
flex: 0 1 100%; flex: 0 1 100%;
overflow: hidden;
}
.nameRow {
display: flex;
}
.nameContainer {
display: flex;
align-items: flex-end;
flex: 0 1 auto;
} }
.name { .name {
display: flex;
font-weight: 300; font-weight: 300;
font-size: 36px; font-size: 36px;
} }
@@ -47,6 +57,14 @@
color: $disabledColor; color: $disabledColor;
} }
.icons {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1 0 auto;
height: 55px;
}
.mbLink { .mbLink {
composes: link from '~Components/Link/Link.css'; composes: link from '~Components/Link/Link.css';
@@ -69,3 +87,10 @@
margin-top: 20px; margin-top: 20px;
text-align: justify; 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 { icons, kinds, sizes } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts'; import fonts from 'Styles/Variables/fonts';
import stripHtml from 'Utilities/String/stripHtml';
import AddNewAuthorModal from './AddNewAuthorModal'; import AddNewAuthorModal from './AddNewAuthorModal';
import styles from './AddNewAuthorSearchResult.css'; import styles from './AddNewAuthorSearchResult.css';
@@ -113,44 +112,49 @@ class AddNewAuthorSearchResult extends Component {
} }
<div className={styles.content}> <div className={styles.content}>
<div className={styles.name}> <div className={styles.nameRow}>
{authorName} <div className={styles.nameContainer}>
<div className={styles.name}>
{authorName}
{ {
!name.contains(year) && year ? !authorName.contains(year) && year ?
<span className={styles.year}> <span className={styles.year}>
({year}) ({year})
</span> : </span> :
null null
} }
{
!!disambiguation &&
<span className={styles.year}>({disambiguation})</span>
}
</div>
</div>
{ <div className={styles.icons}>
!!disambiguation && {
<span className={styles.year}>({disambiguation})</span> isExistingAuthor ?
} <Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title="Already in your library"
/> :
null
}
{ <Link
isExistingAuthor ? className={styles.mbLink}
to={`https://goodreads.com/author/show/${foreignAuthorId}`}
onPress={this.onMBLinkPress}
>
<Icon <Icon
className={styles.alreadyExistsIcon} className={styles.mbLinkIcon}
name={icons.CHECK_CIRCLE} name={icons.EXTERNAL_LINK}
size={36} size={28}
title="Already in your library" />
/> : </Link>
null </div>
}
<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>
</div> </div>
<div> <div>
@@ -186,7 +190,7 @@ class AddNewAuthorSearchResult extends Component {
<TextTruncate <TextTruncate
truncateText="…" truncateText="…"
line={Math.floor(height / (defaultFontSize * lineHeight))} line={Math.floor(height / (defaultFontSize * lineHeight))}
text={stripHtml(overview)} text={overview}
/> />
</div> </div>
</div> </div>

View File

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

View File

@@ -34,24 +34,37 @@
.content { .content {
flex: 0 1 100%; flex: 0 1 100%;
overflow: hidden;
} }
.name { .titleRow {
display: flex; display: flex;
}
.titleContainer {
display: flex;
align-items: flex-end;
flex: 0 1 auto;
}
.title {
font-weight: 300; font-weight: 300;
font-size: 36px; font-size: 36px;
} }
.authorName {
font-weight: 300;
font-size: 20px;
}
.year { .year {
margin-left: 10px; margin-left: 10px;
color: $disabledColor; color: $disabledColor;
} }
.icons {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1 0 auto;
height: 55px;
}
.mbLink { .mbLink {
composes: link from '~Components/Link/Link.css'; composes: link from '~Components/Link/Link.css';
@@ -74,3 +87,10 @@
margin-top: 20px; margin-top: 20px;
text-align: justify; 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 { icons, sizes } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts'; import fonts from 'Styles/Variables/fonts';
import stripHtml from 'Utilities/String/stripHtml';
import AddNewBookModal from './AddNewBookModal'; import AddNewBookModal from './AddNewBookModal';
import styles from './AddNewBookSearchResult.css'; import styles from './AddNewBookSearchResult.css';
@@ -112,52 +111,42 @@ class AddNewBookSearchResult extends Component {
} }
<div className={styles.content}> <div className={styles.content}>
<div className={styles.name}> <div className={styles.titleRow}>
{title} <div className={styles.titleContainer}>
<div className={styles.title}>
{title}
{ {
!!disambiguation && !!disambiguation &&
<span className={styles.year}>({disambiguation})</span> <span className={styles.year}>({disambiguation})</span>
} }
</div>
</div>
{ <div className={styles.icons}>
isExistingBook ? {
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 <Icon
className={styles.alreadyExistsIcon} className={styles.mbLinkIcon}
name={icons.CHECK_CIRCLE} name={icons.EXTERNAL_LINK}
size={20} size={28}
title="Book already in your library" />
/> : </Link>
null </div>
}
<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
}
</div> </div>
<div> <div>
@@ -186,7 +175,7 @@ class AddNewBookSearchResult extends Component {
<TextTruncate <TextTruncate
truncateText="…" truncateText="…"
line={Math.floor(height / (defaultFontSize * lineHeight))} line={Math.floor(height / (defaultFontSize * lineHeight))}
text={stripHtml(overview)} text={overview}
/> />
</div> </div>
</div> </div>

View File

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

View File

@@ -66,7 +66,7 @@ function BackupSettings(props) {
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="backupRetention" name="backupRetention"
unit="days" 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} onChange={onInputChange}
{...backupRetention} {...backupRetention}
/> />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions'; import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByName from 'Utilities/Array/sortByName';
import Indexers from './Indexers'; import Indexers from './Indexers';
@@ -15,8 +15,9 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchIndexers, dispatchFetchIndexers: fetchIndexers,
deleteIndexer dispatchDeleteIndexer: deleteIndexer,
dispatchCloneIndexer: cloneIndexer
}; };
class IndexersConnector extends Component { class IndexersConnector extends Component {
@@ -25,14 +26,14 @@ class IndexersConnector extends Component {
// Lifecycle // Lifecycle
componentDidMount() { componentDidMount() {
this.props.fetchIndexers(); this.props.dispatchFetchIndexers();
} }
// //
// Listeners // Listeners
onConfirmDeleteIndexer = (id) => { onConfirmDeleteIndexer = (id) => {
this.props.deleteIndexer({ id }); this.props.dispatchDeleteIndexer({ id });
} }
// //
@@ -49,8 +50,9 @@ class IndexersConnector extends Component {
} }
IndexersConnector.propTypes = { IndexersConnector.propTypes = {
fetchIndexers: PropTypes.func.isRequired, dispatchFetchIndexers: PropTypes.func.isRequired,
deleteIndexer: PropTypes.func.isRequired dispatchDeleteIndexer: PropTypes.func.isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector); 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 FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; 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 { inputTypes, sizes } from 'Helpers/Props';
import RemotePathMappingsConnector from 'Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector'; import RemotePathMappingsConnector from 'Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
@@ -63,7 +63,7 @@ class MediaManagement extends Component {
onSavePress={onSavePress} onSavePress={onSavePress}
/> />
<PageContentBodyConnector> <PageContentBody>
<RootFoldersConnector /> <RootFoldersConnector />
<RemotePathMappingsConnector /> <RemotePathMappingsConnector />
<NamingConnector /> <NamingConnector />
@@ -372,7 +372,7 @@ class MediaManagement extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="setPermissionsLinux" 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." helpTextWarning="If you're unsure what these settings do, do not alter them."
onChange={onInputChange} onChange={onInputChange}
{...settings.setPermissionsLinux} {...settings.setPermissionsLinux}
@@ -383,46 +383,15 @@ class MediaManagement extends Component {
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>File chmod mode</FormLabel> <FormLabel>chmod Folder</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.UMASK}
name="fileChmod" name="chmodFolder"
helpText="Octal, applied to media files when imported/renamed by Readarr" 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} onChange={onInputChange}
{...settings.fileChmod} {...settings.chmodFolder}
/>
</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}
/> />
</FormGroup> </FormGroup>
@@ -436,6 +405,7 @@ class MediaManagement extends Component {
type={inputTypes.TEXT} type={inputTypes.TEXT}
name="chownGroup" name="chownGroup"
helpText="Group name or gid. Use gid for remote file systems." 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} values={fileDateOptions}
onChange={onInputChange} onChange={onInputChange}
{...settings.chownGroup} {...settings.chownGroup}
@@ -445,7 +415,7 @@ class MediaManagement extends Component {
} }
</Form> </Form>
} }
</PageContentBodyConnector> </PageContentBody>
</PageContent> </PageContent>
); );
} }

View File

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

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import NotificationsConnector from './Notifications/NotificationsConnector'; import NotificationsConnector from './Notifications/NotificationsConnector';
@@ -11,9 +11,9 @@ function NotificationSettings() {
showSave={false} showSave={false}
/> />
<PageContentBodyConnector> <PageContentBody>
<NotificationsConnector /> <NotificationsConnector />
</PageContentBodyConnector> </PageContentBody>
</PageContent> </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 { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import DelayProfilesConnector from './Delay/DelayProfilesConnector'; import DelayProfilesConnector from './Delay/DelayProfilesConnector';
import MetadataProfilesConnector from './Metadata/MetadataProfilesConnector'; import MetadataProfilesConnector from './Metadata/MetadataProfilesConnector';
@@ -24,14 +24,14 @@ class Profiles extends Component {
showSave={false} showSave={false}
/> />
<PageContentBodyConnector> <PageContentBody>
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<QualityProfilesConnector /> <QualityProfilesConnector />
<MetadataProfilesConnector /> <MetadataProfilesConnector />
<DelayProfilesConnector /> <DelayProfilesConnector />
<ReleaseProfilesConnector /> <ReleaseProfilesConnector />
</DndProvider> </DndProvider>
</PageContentBodyConnector> </PageContentBody>
</PageContent> </PageContent>
); );
} }

View File

@@ -30,11 +30,13 @@ function EditReleaseProfileModalContent(props) {
const { const {
id, id,
enabled,
required, required,
ignored, ignored,
preferred, preferred,
includePreferredWhenRenaming, includePreferredWhenRenaming,
tags tags,
indexerId
} = item; } = item;
return ( return (
@@ -45,6 +47,18 @@ function EditReleaseProfileModalContent(props) {
<ModalBody> <ModalBody>
<Form {...otherProps}> <Form {...otherProps}>
<FormGroup>
<FormLabel>Enable Profile</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enabled"
helpText="Check to enable release profile"
{...enabled}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel>Must Contain</FormLabel> <FormLabel>Must Contain</FormLabel>
@@ -99,9 +113,23 @@ function EditReleaseProfileModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="includePreferredWhenRenaming" 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} {...includePreferredWhenRenaming}
onChange={onInputChange} 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> </FormGroup>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ function findMatchingItems(ids, items) {
}); });
} }
function createMatchingAuthorSelector() { function createUnorderedMatchingAuthorSelector() {
return createSelector( return createSelector(
(state, { authorIds }) => authorIds, (state, { authorIds }) => authorIds,
createAllAuthorSelector(), 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() { function createMatchingDelayProfilesSelector() {
return createSelector( return createSelector(
(state, { delayProfileIds }) => delayProfileIds, (state, { delayProfileIds }) => delayProfileIds,

View File

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

View File

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

View File

@@ -8,7 +8,9 @@ import createTestProviderHandler, { createCancelTestProviderHandler } from 'Stor
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState';
import selectProviderSchema from 'Utilities/State/selectProviderSchema'; import selectProviderSchema from 'Utilities/State/selectProviderSchema';
import updateSectionState from 'Utilities/State/updateSectionState';
// //
// Variables // Variables
@@ -21,6 +23,7 @@ const section = 'settings.indexers';
export const FETCH_INDEXERS = 'settings/indexers/fetchIndexers'; export const FETCH_INDEXERS = 'settings/indexers/fetchIndexers';
export const FETCH_INDEXER_SCHEMA = 'settings/indexers/fetchIndexerSchema'; export const FETCH_INDEXER_SCHEMA = 'settings/indexers/fetchIndexerSchema';
export const SELECT_INDEXER_SCHEMA = 'settings/indexers/selectIndexerSchema'; 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_VALUE = 'settings/indexers/setIndexerValue';
export const SET_INDEXER_FIELD_VALUE = 'settings/indexers/setIndexerFieldValue'; export const SET_INDEXER_FIELD_VALUE = 'settings/indexers/setIndexerFieldValue';
export const SAVE_INDEXER = 'settings/indexers/saveIndexer'; 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 fetchIndexers = createThunk(FETCH_INDEXERS);
export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA); export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA);
export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA); export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA);
export const cloneIndexer = createAction(CLONE_INDEXER);
export const saveIndexer = createThunk(SAVE_INDEXER); export const saveIndexer = createThunk(SAVE_INDEXER);
export const cancelSaveIndexer = createThunk(CANCEL_SAVE_INDEXER); export const cancelSaveIndexer = createThunk(CANCEL_SAVE_INDEXER);
@@ -113,6 +117,30 @@ export default {
return selectedSchema; 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) { sizeOnDisk: function(item, filterValue, type) {
const predicate = filterTypePredicates[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); return predicate(sizeOnDisk, filterValue);
} }
@@ -133,6 +135,12 @@ export const sortPredicates = {
} }
return result; 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 createHandleActions from './Creators/createHandleActions';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
// //
// Variables // Variables
@@ -30,6 +31,58 @@ export const defaultState = {
filters, filters,
filterPredicates, 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: [ filterBuilderProps: [
{ {
name: 'monitored', name: 'monitored',
@@ -65,6 +118,12 @@ export const defaultState = {
label: 'Root Folder Path', label: 'Root Folder Path',
type: filterBuilderTypes.EXACT type: filterBuilderTypes.EXACT
}, },
{
name: 'sizeOnDisk',
label: 'Size on Disk',
type: filterBuilderTypes.NUMBER,
valueType: filterBuilderValueTypes.BYTES
},
{ {
name: 'tags', name: 'tags',
label: '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 SET_AUTHOR_EDITOR_FILTER = 'authorEditor/setAuthorEditorFilter';
export const SAVE_AUTHOR_EDITOR = 'authorEditor/saveAuthorEditor'; export const SAVE_AUTHOR_EDITOR = 'authorEditor/saveAuthorEditor';
export const BULK_DELETE_AUTHOR = 'authorEditor/bulkDeleteAuthor'; export const BULK_DELETE_AUTHOR = 'authorEditor/bulkDeleteAuthor';
export const SET_AUTHOR_EDITOR_TABLE_OPTION = 'authorEditor/setAuthorEditorTableOption';
// //
// Action Creators // Action Creators
@@ -98,6 +158,7 @@ export const setAuthorEditorSort = createAction(SET_AUTHOR_EDITOR_SORT);
export const setAuthorEditorFilter = createAction(SET_AUTHOR_EDITOR_FILTER); export const setAuthorEditorFilter = createAction(SET_AUTHOR_EDITOR_FILTER);
export const saveAuthorEditor = createThunk(SAVE_AUTHOR_EDITOR); export const saveAuthorEditor = createThunk(SAVE_AUTHOR_EDITOR);
export const bulkDeleteAuthor = createThunk(BULK_DELETE_AUTHOR); export const bulkDeleteAuthor = createThunk(BULK_DELETE_AUTHOR);
export const setAuthorEditorTableOption = createAction(SET_AUTHOR_EDITOR_TABLE_OPTION);
// //
// Action Handlers // Action Handlers
@@ -181,6 +242,7 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({ export const reducers = createHandleActions({
[SET_AUTHOR_EDITOR_TABLE_OPTION]: createSetTableOptionReducer(section),
[SET_AUTHOR_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section), [SET_AUTHOR_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
[SET_AUTHOR_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section) [SET_AUTHOR_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)

View File

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

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