diff --git a/frontend/src/Activity/Queue/Status/QueueStatusConnector.js b/frontend/src/Activity/Queue/Status/QueueStatusConnector.js index c8419a8f8..e56b39187 100644 --- a/frontend/src/Activity/Queue/Status/QueueStatusConnector.js +++ b/frontend/src/Activity/Queue/Status/QueueStatusConnector.js @@ -8,7 +8,7 @@ import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; function createMapStateToProps() { return createSelector( (state) => state.app, - (state) => state.queue.queueStatus, + (state) => state.queue.status, (app, status) => { return { isConnected: app.isConnected, diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js index fda164301..f87164a1f 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js @@ -80,7 +80,7 @@ class ImportArtistTable extends Component { // Props doesn't have a selected artist or // the selected artist is an existing artist. - if ((selectedArtist && !prevItem.selectedArtist) || (isExistingArtist && !prevItem.selectedArtist)) { + if ((!selectedArtist && prevItem.selectedArtist) || (isExistingArtist && !prevItem.selectedArtist)) { onSelectedChange({ id, value: false }); return; diff --git a/frontend/src/Album/episodeEntities.js b/frontend/src/Album/episodeEntities.js index 175b6fc54..fe21d4ed0 100644 --- a/frontend/src/Album/episodeEntities.js +++ b/frontend/src/Album/episodeEntities.js @@ -1,6 +1,6 @@ export const CALENDAR = 'calendar'; export const EPISODES = 'episodes'; -export const INTERACTIVE_IMPORT = 'interactiveImport.interactiveImportAlbums'; +export const INTERACTIVE_IMPORT = 'interactiveImport.episodes'; export const WANTED_CUTOFF_UNMET = 'wanted.cutoffUnmet'; export const WANTED_MISSING = 'wanted.missing'; diff --git a/frontend/src/AlbumStudio/AlbumStudioRowConnector.js b/frontend/src/AlbumStudio/AlbumStudioRowConnector.js index fa54b9064..12dfc12ea 100644 --- a/frontend/src/AlbumStudio/AlbumStudioRowConnector.js +++ b/frontend/src/AlbumStudio/AlbumStudioRowConnector.js @@ -4,7 +4,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createArtistSelector from 'Store/Selectors/createArtistSelector'; -import { toggleArtistMonitored, toggleSeasonMonitored } from 'Store/Actions/artistActions'; +import { toggleArtistMonitored } from 'Store/Actions/artistActions'; import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions'; import AlbumStudioRow from './AlbumStudioRow'; @@ -32,7 +32,6 @@ function createMapStateToProps() { const mapDispatchToProps = { toggleArtistMonitored, - toggleSeasonMonitored, toggleEpisodeMonitored }; @@ -78,7 +77,6 @@ AlbumStudioRowConnector.propTypes = { artistId: PropTypes.number.isRequired, monitored: PropTypes.bool.isRequired, toggleArtistMonitored: PropTypes.func.isRequired, - toggleSeasonMonitored: PropTypes.func.isRequired, toggleEpisodeMonitored: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Details/ArtistDetailsSeason.js b/frontend/src/Artist/Details/ArtistDetailsSeason.js index ccffc29fe..8c7e386f7 100644 --- a/frontend/src/Artist/Details/ArtistDetailsSeason.js +++ b/frontend/src/Artist/Details/ArtistDetailsSeason.js @@ -125,7 +125,6 @@ class ArtistDetailsSeason extends Component { artistMonitored, isSmallScreen, onTableOptionChange, - onMonitorSeasonPress, onSearchPress } = this.props; @@ -309,7 +308,6 @@ ArtistDetailsSeason.propTypes = { artistMonitored: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired, onTableOptionChange: PropTypes.func.isRequired, - onMonitorSeasonPress: PropTypes.func.isRequired, onExpandPress: PropTypes.func.isRequired, onMonitorAlbumPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired diff --git a/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js b/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js index 49dfb78e4..df705b405 100644 --- a/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js +++ b/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js @@ -7,7 +7,6 @@ import { findCommand } from 'Utilities/Command'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createArtistSelector from 'Store/Selectors/createArtistSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; -import { toggleSeasonMonitored } from 'Store/Actions/artistActions'; import { toggleEpisodesMonitored, setEpisodesTableOption } from 'Store/Actions/episodeActions'; import { executeCommand } from 'Store/Actions/commandActions'; import * as commandNames from 'Commands/commandNames'; @@ -42,7 +41,6 @@ function createMapStateToProps() { } const mapDispatchToProps = { - toggleSeasonMonitored, toggleEpisodesMonitored, setEpisodesTableOption, executeCommand @@ -57,17 +55,6 @@ class ArtistDetailsSeasonConnector extends Component { this.props.setEpisodesTableOption(payload); } - onMonitorSeasonPress = (monitored) => { - const { - artistId - } = this.props; - - this.props.toggleSeasonMonitored({ - artistId, - monitored - }); - } - onSearchPress = () => { const { artistId @@ -104,7 +91,6 @@ class ArtistDetailsSeasonConnector extends Component { ArtistDetailsSeasonConnector.propTypes = { artistId: PropTypes.number.isRequired, - toggleSeasonMonitored: PropTypes.func.isRequired, toggleEpisodesMonitored: PropTypes.func.isRequired, setEpisodesTableOption: PropTypes.func.isRequired, executeCommand: PropTypes.func.isRequired diff --git a/frontend/src/Artist/Index/ArtistIndex.js b/frontend/src/Artist/Index/ArtistIndex.js index 827fe2499..1acd5b3aa 100644 --- a/frontend/src/Artist/Index/ArtistIndex.js +++ b/frontend/src/Artist/Index/ArtistIndex.js @@ -214,6 +214,7 @@ class ArtistIndex extends Component { const ViewComponent = getViewComponent(view); const isLoaded = !error && isPopulated && !!items.length && contentBody; + const hasNoArtist = !items.length; return ( @@ -224,6 +225,7 @@ class ArtistIndex extends Component { iconName={icons.REFRESH} spinningName={icons.REFRESH} isSpinning={isRefreshingArtist} + isDisabled={hasNoArtist} onPress={onRefreshArtistPress} /> @@ -231,6 +233,7 @@ class ArtistIndex extends Component { label="RSS Sync" iconName={icons.RSS} isSpinning={isRssSyncExecuting} + isDisabled={hasNoArtist} onPress={onRssSyncPress} /> @@ -246,6 +249,7 @@ class ArtistIndex extends Component { } @@ -255,6 +259,7 @@ class ArtistIndex extends Component { } @@ -264,6 +269,7 @@ class ArtistIndex extends Component { } @@ -276,18 +282,21 @@ class ArtistIndex extends Component { diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js index 5b78ce44d..6b5c9b9e1 100644 --- a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js +++ b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js @@ -9,11 +9,15 @@ function ArtistIndexFilterMenu(props) { const { filterKey, filterValue, + isDisabled, onFilterSelect } = props; return ( - + + + @@ -150,7 +149,7 @@ class ArtistIndexOverview extends Component { /> -
+
0; + let PageComponent = 'div'; + + if (isMeasured) { + PageComponent = hasArtist ? CalendarConnector : NoArtist; + } + return ( @@ -74,7 +83,10 @@ class CalendarPage extends Component { - + - { - this.state.width > 0 ? - : -
- } + - + { + hasArtist && + + } state.calendar, + createArtistCountSelector(), createUISettingsSelector(), - (calendar, uiSettings) => { + (calendar, artistCount, uiSettings) => { return { unmonitored: calendar.unmonitored, showUpcoming: calendar.showUpcoming, - colorImpairedMode: uiSettings.enableColorImpairedMode + colorImpairedMode: uiSettings.enableColorImpairedMode, + hasArtist: !!artistCount }; } ); diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css index 4662cc581..7fa7f72cb 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.css +++ b/frontend/src/Components/Form/EnhancedSelectInput.css @@ -55,11 +55,17 @@ height: auto !important; } -.optionsInnerModalBody { - composes: innerModalBody from 'Components/Modal/ModalBody.css'; +.optionsModalBody { + composes: modalBody from 'Components/Modal/ModalBody.css'; - padding: 0; - width: 100%; + display: flex; + justify-content: center; + flex-direction: column; + padding: 10px 0; +} + +.optionsModalScroller { + composes: scroller from 'Components/Scroller/Scroller.css'; border: 1px solid $inputBorderColor; border-radius: 4px; background-color: $white; diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js index 6310b4854..f3d923895 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.js +++ b/frontend/src/Components/Form/EnhancedSelectInput.js @@ -7,11 +7,12 @@ import TetherComponent from 'react-tether'; import classNames from 'classnames'; import isMobileUtil from 'Utilities/isMobile'; import * as keyCodes from 'Utilities/Constants/keyCodes'; -import { icons } from 'Helpers/Props'; +import { icons, scrollDirections } from 'Helpers/Props'; import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; import Modal from 'Components/Modal/Modal'; import ModalBody from 'Components/Modal/ModalBody'; +import Scroller from 'Components/Scroller/Scroller'; import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; import EnhancedSelectInputOption from './EnhancedSelectInputOption'; import styles from './EnhancedSelectInput.css'; @@ -346,24 +347,28 @@ class EnhancedSelectInput extends Component { onModalClose={this.onOptionsModalClose} > - { - values.map((v, index) => { - return ( - - {v.value} - - ); - }) - } + + { + values.map((v, index) => { + return ( + + {v.value} + + ); + }) + } + } diff --git a/frontend/src/Components/Menu/FilterMenu.js b/frontend/src/Components/Menu/FilterMenu.js index 6a76db432..03fff8820 100644 --- a/frontend/src/Components/Menu/FilterMenu.js +++ b/frontend/src/Components/Menu/FilterMenu.js @@ -9,6 +9,7 @@ function FilterMenu(props) { const { className, children, + isDisabled, ...otherProps } = props; @@ -20,6 +21,7 @@ function FilterMenu(props) { {children} @@ -28,11 +30,13 @@ function FilterMenu(props) { FilterMenu.propTypes = { className: PropTypes.string, - children: PropTypes.node.isRequired + children: PropTypes.node.isRequired, + isDisabled: PropTypes.bool.isRequired }; FilterMenu.defaultProps = { - className: styles.filterMenu + className: styles.filterMenu, + isDisabled: false }; export default FilterMenu; diff --git a/frontend/src/Components/Menu/MenuButton.css b/frontend/src/Components/Menu/MenuButton.css index 04a7439cd..184545a5f 100644 --- a/frontend/src/Components/Menu/MenuButton.css +++ b/frontend/src/Components/Menu/MenuButton.css @@ -13,3 +13,7 @@ color: $toobarButtonHoverColor; } } + +.isDisabled { + color: $disabledColor; +} diff --git a/frontend/src/Components/Menu/MenuButton.js b/frontend/src/Components/Menu/MenuButton.js index d89a52d1d..477334a1d 100644 --- a/frontend/src/Components/Menu/MenuButton.js +++ b/frontend/src/Components/Menu/MenuButton.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import classNames from 'classnames'; import Link from 'Components/Link/Link'; import styles from './MenuButton.css'; @@ -12,13 +13,18 @@ class MenuButton extends Component { const { className, children, + isDisabled, onPress, ...otherProps } = this.props; return ( @@ -31,11 +37,13 @@ class MenuButton extends Component { MenuButton.propTypes = { className: PropTypes.string, children: PropTypes.node.isRequired, + isDisabled: PropTypes.bool.isRequired, onPress: PropTypes.func }; MenuButton.defaultProps = { - className: styles.menuButton + className: styles.menuButton, + isDisabled: false }; export default MenuButton; diff --git a/frontend/src/Components/Menu/SortMenu.js b/frontend/src/Components/Menu/SortMenu.js index 7a2e931ea..a9a6a184e 100644 --- a/frontend/src/Components/Menu/SortMenu.js +++ b/frontend/src/Components/Menu/SortMenu.js @@ -8,6 +8,7 @@ function SortMenu(props) { const { className, children, + isDisabled, ...otherProps } = props; @@ -19,6 +20,7 @@ function SortMenu(props) { {children} @@ -27,7 +29,12 @@ function SortMenu(props) { SortMenu.propTypes = { className: PropTypes.string, - children: PropTypes.node.isRequired + children: PropTypes.node.isRequired, + isDisabled: PropTypes.bool.isRequired +}; + +SortMenu.defaultProps = { + isDisabled: false }; export default SortMenu; diff --git a/frontend/src/Components/Menu/ViewMenu.js b/frontend/src/Components/Menu/ViewMenu.js index 9c39bb3e2..60c77e003 100644 --- a/frontend/src/Components/Menu/ViewMenu.js +++ b/frontend/src/Components/Menu/ViewMenu.js @@ -7,6 +7,7 @@ import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; function ViewMenu(props) { const { children, + isDisabled, ...otherProps } = props; @@ -17,6 +18,7 @@ function ViewMenu(props) { {children} @@ -24,7 +26,12 @@ function ViewMenu(props) { } ViewMenu.propTypes = { - children: PropTypes.node.isRequired + children: PropTypes.node.isRequired, + isDisabled: PropTypes.bool.isRequired +}; + +ViewMenu.defaultProps = { + isDisabled: false }; export default ViewMenu; diff --git a/frontend/src/Components/Page/Header/ArtistSearchResult.css b/frontend/src/Components/Page/Header/ArtistSearchResult.css index 87988a35b..5c6f58eed 100644 --- a/frontend/src/Components/Page/Header/ArtistSearchResult.css +++ b/frontend/src/Components/Page/Header/ArtistSearchResult.css @@ -6,7 +6,7 @@ .poster { width: 35px; - height: 50px; + height: 35px; } .titles { diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index 1093ef8d9..1d07456d8 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -72,7 +72,7 @@ class SignalRConnector extends Component { this.signalRconnectionOptions = { transport: ['webSockets', 'longPolling'] }; this.signalRconnection = null; - this.retryInterval = 5; + this.retryInterval = 1; this.retryTimeoutId = null; this.disconnectedTime = null; } @@ -107,7 +107,7 @@ class SignalRConnector extends Component { this.retryTimeoutId = setTimeout(() => { this.signalRconnection.start(this.signalRconnectionOptions); - this.retryInterval = Math.min(this.retryInterval + 5, 30); + this.retryInterval = Math.min(this.retryInterval + 1, 10); }, this.retryInterval * 1000); } @@ -262,7 +262,7 @@ class SignalRConnector extends Component { } handleQueueStatus = (body) => { - this.props.update({ section: 'queueStatus', data: body.resource }); + this.props.update({ section: 'queue.status', data: body.resource }); } handleVersion = (body) => { diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js index 8694a5485..b9fd5b001 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js @@ -97,5 +97,5 @@ export default connectSection( mapDispatchToProps, undefined, undefined, - { section: 'interactiveImport.interactiveImportAlbums' } + { section: 'interactiveImport.albums' } )(SelectAlbumModalContentConnector); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js index 982b675b8..b5e5520fb 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js @@ -6,7 +6,7 @@ import { cancelTestDownloadClient, cancelSaveDownloadClient } from 'Store/Action import EditDownloadClientModal from './EditDownloadClientModal'; function createMapDispatchToProps(dispatch, props) { - const section = 'downloadClients'; + const section = 'settings.downloadClients'; return { dispatchClearPendingChanges() { diff --git a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js index cee9a544e..2c5c78c7e 100644 --- a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js +++ b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js @@ -88,5 +88,5 @@ export default connectSection( mapDispatchToProps, undefined, { withRef: true }, - { section: 'downloadClientOptions' } + { section: 'settings.downloadClientOptions' } )(DownloadClientOptionsConnector); diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js index bd8bca75f..94172429d 100644 --- a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js @@ -18,7 +18,7 @@ class EditRemotePathMappingModalConnector extends Component { // Listeners onModalClose = () => { - this.props.clearPendingChanges({ section: 'remotePathMappings' }); + this.props.clearPendingChanges({ section: 'settings.remotePathMappings' }); this.props.onModalClose(); } diff --git a/frontend/src/Settings/General/GeneralSettingsConnector.js b/frontend/src/Settings/General/GeneralSettingsConnector.js index fd0a11743..de4a03632 100644 --- a/frontend/src/Settings/General/GeneralSettingsConnector.js +++ b/frontend/src/Settings/General/GeneralSettingsConnector.js @@ -113,5 +113,5 @@ export default connectSection( mapDispatchToProps, undefined, undefined, - { section: 'general' } + { section: 'settings.general' } )(GeneralSettingsConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js index c308f004f..ec0b7586e 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js @@ -6,7 +6,7 @@ import { cancelTestIndexer, cancelSaveIndexer } from 'Store/Actions/settingsActi import EditIndexerModal from './EditIndexerModal'; function createMapDispatchToProps(dispatch, props) { - const section = 'indexers'; + const section = 'settings.indexers'; return { dispatchClearPendingChanges() { diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js b/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js index e6d39edf9..4215a3257 100644 --- a/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js +++ b/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js @@ -88,5 +88,5 @@ export default connectSection( mapDispatchToProps, undefined, { withRef: true }, - { section: 'indexerOptions' } + { section: 'settings.indexerOptions' } )(IndexerOptionsConnector); diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js index 4483f7894..0089d153e 100644 --- a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js @@ -14,7 +14,7 @@ class EditRestrictionModalConnector extends Component { // Listeners onModalClose = () => { - this.props.clearPendingChanges({ section: 'restrictions' }); + this.props.clearPendingChanges({ section: 'settings.restrictions' }); this.props.onModalClose(); } diff --git a/frontend/src/Settings/MediaManagement/MediaManagementConnector.js b/frontend/src/Settings/MediaManagement/MediaManagementConnector.js index 5f7ed7141..61a0468cc 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagementConnector.js +++ b/frontend/src/Settings/MediaManagement/MediaManagementConnector.js @@ -87,5 +87,5 @@ export default connectSection( mapDispatchToProps, undefined, undefined, - { section: 'mediaManagement' } + { section: 'settings.mediaManagement' } )(MediaManagementConnector); diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js index 04d200eb1..f66c3b5cc 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js @@ -98,5 +98,5 @@ export default connectSection( mapDispatchToProps, undefined, undefined, - { section: 'naming' } + { section: 'settings.naming' } )(NamingConnector); diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js index 4f80d5f14..cb461520f 100644 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js @@ -5,7 +5,7 @@ import { clearPendingChanges } from 'Store/Actions/baseActions'; import EditMetadataModal from './EditMetadataModal'; function createMapDispatchToProps(dispatch, props) { - const section = 'metadata'; + const section = 'settings.metadata'; return { dispatchClearPendingChanges() { diff --git a/frontend/src/Settings/Metadata/MetadataProvider/MetadataProviderConnector.js b/frontend/src/Settings/Metadata/MetadataProvider/MetadataProviderConnector.js index 153a929e9..319f4f02c 100644 --- a/frontend/src/Settings/Metadata/MetadataProvider/MetadataProviderConnector.js +++ b/frontend/src/Settings/Metadata/MetadataProvider/MetadataProviderConnector.js @@ -84,9 +84,9 @@ MetadataProviderConnector.propTypes = { }; export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - { withRef: true }, - { section: 'metadataProvider' } - )(MetadataProviderConnector); + createMapStateToProps, + mapDispatchToProps, + undefined, + { withRef: true }, + { section: 'settings.metadataProvider' } +)(MetadataProviderConnector); diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js index 77566f9e1..e1452d142 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js @@ -6,7 +6,7 @@ import { cancelTestNotification, cancelSaveNotification } from 'Store/Actions/se import EditNotificationModal from './EditNotificationModal'; function createMapDispatchToProps(dispatch, props) { - const section = 'notifications'; + const section = 'settings.notifications'; return { dispatchClearPendingChanges() { diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js index 1f696c846..a1e8d2dcd 100644 --- a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js @@ -18,7 +18,7 @@ class EditDelayProfileModalConnector extends Component { // Listeners onModalClose = () => { - this.props.clearPendingChanges({ section: 'delayProfiles' }); + this.props.clearPendingChanges({ section: 'settings.delayProfiles' }); this.props.onModalClose(); } diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js index 44866e2a3..8e112c9e1 100644 --- a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js @@ -18,7 +18,7 @@ class EditLanguageProfileModalConnector extends Component { // Listeners onModalClose = () => { - this.props.clearPendingChanges({ section: 'languageProfiles' }); + this.props.clearPendingChanges({ section: 'settings.languageProfiles' }); this.props.onModalClose(); } diff --git a/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalConnector.js b/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalConnector.js index 987630c98..edc1f1a73 100644 --- a/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalConnector.js +++ b/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalConnector.js @@ -18,7 +18,7 @@ class EditMetadataProfileModalConnector extends Component { // Listeners onModalClose = () => { - this.props.clearPendingChanges({ section: 'metadataProfiles' }); + this.props.clearPendingChanges({ section: 'settings.metadataProfiles' }); this.props.onModalClose(); } diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js index 5ec77950f..942949cac 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js @@ -18,7 +18,7 @@ class EditQualityProfileModalConnector extends Component { // Listeners onModalClose = () => { - this.props.clearPendingChanges({ section: 'qualityProfiles' }); + this.props.clearPendingChanges({ section: 'settings.qualityProfiles' }); this.props.onModalClose(); } diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js b/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js index be83cc069..9404dfd9f 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js @@ -19,7 +19,7 @@ const mapDispatchToProps = { class QualityDefinitionConnector extends Component { componentWillUnmount() { - this.props.clearPendingChanges({ section: 'qualityDefinitions' }); + this.props.clearPendingChanges({ section: 'settings.qualityDefinitions' }); } // diff --git a/frontend/src/Settings/UI/UISettingsConnector.js b/frontend/src/Settings/UI/UISettingsConnector.js index 70c9bdd14..c5ca4a051 100644 --- a/frontend/src/Settings/UI/UISettingsConnector.js +++ b/frontend/src/Settings/UI/UISettingsConnector.js @@ -78,5 +78,5 @@ export default connectSection( mapDispatchToProps, undefined, undefined, - { section: 'ui' } + { section: 'settings.ui' } )(UISettingsConnector); diff --git a/frontend/src/Store/Reducers/Creators/createClearReducer.js b/frontend/src/Store/Actions/Creators/Reducers/createClearReducer.js similarity index 100% rename from frontend/src/Store/Reducers/Creators/createClearReducer.js rename to frontend/src/Store/Actions/Creators/Reducers/createClearReducer.js diff --git a/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionFilterReducer.js b/frontend/src/Store/Actions/Creators/Reducers/createSetClientSideCollectionFilterReducer.js similarity index 100% rename from frontend/src/Store/Reducers/Creators/createSetClientSideCollectionFilterReducer.js rename to frontend/src/Store/Actions/Creators/Reducers/createSetClientSideCollectionFilterReducer.js diff --git a/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionSortReducer.js b/frontend/src/Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer.js similarity index 100% rename from frontend/src/Store/Reducers/Creators/createSetClientSideCollectionSortReducer.js rename to frontend/src/Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer.js diff --git a/frontend/src/Store/Reducers/Creators/createSetProviderFieldValueReducer.js b/frontend/src/Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer.js similarity index 100% rename from frontend/src/Store/Reducers/Creators/createSetProviderFieldValueReducer.js rename to frontend/src/Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer.js diff --git a/frontend/src/Store/Reducers/Creators/createSetSettingValueReducer.js b/frontend/src/Store/Actions/Creators/Reducers/createSetSettingValueReducer.js similarity index 100% rename from frontend/src/Store/Reducers/Creators/createSetSettingValueReducer.js rename to frontend/src/Store/Actions/Creators/Reducers/createSetSettingValueReducer.js diff --git a/frontend/src/Store/Reducers/Creators/createSetTableOptionReducer.js b/frontend/src/Store/Actions/Creators/Reducers/createSetTableOptionReducer.js similarity index 100% rename from frontend/src/Store/Reducers/Creators/createSetTableOptionReducer.js rename to frontend/src/Store/Actions/Creators/Reducers/createSetTableOptionReducer.js diff --git a/frontend/src/Store/Actions/Creators/createBatchToggleAlbumMonitoredHandler.js b/frontend/src/Store/Actions/Creators/createBatchToggleAlbumMonitoredHandler.js index 6d93633a8..d94f2cc5d 100644 --- a/frontend/src/Store/Actions/Creators/createBatchToggleAlbumMonitoredHandler.js +++ b/frontend/src/Store/Actions/Creators/createBatchToggleAlbumMonitoredHandler.js @@ -1,7 +1,8 @@ import $ from 'jquery'; import updateAlbums from 'Utilities/Album/updateAlbums'; +import getSectionState from 'Utilities/State/getSectionState'; -function createBatchToggleAlbumMonitoredHandler(section, getFromState) { +function createBatchToggleAlbumMonitoredHandler(section) { return function(payload) { return function(dispatch, getState) { const { @@ -9,7 +10,7 @@ function createBatchToggleAlbumMonitoredHandler(section, getFromState) { monitored } = payload; - const state = getFromState(getState()); + const state = getSectionState(getState(), section, true); updateAlbums(dispatch, section, state.items, albumIds, { isSaving: true diff --git a/frontend/src/Store/Actions/Creators/createFetchHandler.js b/frontend/src/Store/Actions/Creators/createFetchHandler.js index e0f572dbc..c9cd058bd 100644 --- a/frontend/src/Store/Actions/Creators/createFetchHandler.js +++ b/frontend/src/Store/Actions/Creators/createFetchHandler.js @@ -2,47 +2,43 @@ import { batchActions } from 'redux-batched-actions'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import { set, update, updateItem } from '../baseActions'; -function createFetchHandler(section, url) { - return function(payload = {}) { - return function(dispatch, getState) { - dispatch(set({ section, isFetching: true })); +export default function createFetchHandler(section, url) { + return function(getState, payload, dispatch) { + dispatch(set({ section, isFetching: true })); - const { - id, - ...otherPayload - } = payload; + const { + id, + ...otherPayload + } = payload; - const { request, abortRequest } = createAjaxRequest({ - url: id == null ? url : `${url}/${id}`, - data: otherPayload, - traditional: true - }); + const { request, abortRequest } = createAjaxRequest({ + url: id == null ? url : `${url}/${id}`, + data: otherPayload, + traditional: true + }); - request.done((data) => { - dispatch(batchActions([ - id == null ? update({ section, data }) : updateItem({ section, ...data }), + request.done((data) => { + dispatch(batchActions([ + id == null ? update({ section, data }) : updateItem({ section, ...data }), - set({ - section, - isFetching: false, - isPopulated: true, - error: null - }) - ])); - }); - - request.fail((xhr) => { - dispatch(set({ + set({ section, isFetching: false, - isPopulated: false, - error: xhr.aborted ? null : xhr - })); - }); + isPopulated: true, + error: null + }) + ])); + }); - return abortRequest; - }; + request.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr.aborted ? null : xhr + })); + }); + + return abortRequest; }; } - -export default createFetchHandler; diff --git a/frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js b/frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js index e58811ee4..5a360305b 100644 --- a/frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js +++ b/frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js @@ -2,33 +2,31 @@ import $ from 'jquery'; import { set } from '../baseActions'; function createFetchSchemaHandler(section, url) { - return function(payload) { - return function(dispatch, getState) { - dispatch(set({ section, isFetchingSchema: true })); + return function(getState, payload, dispatch) { + dispatch(set({ section, isFetchingSchema: true })); - const promise = $.ajax({ - url - }); + const promise = $.ajax({ + url + }); - promise.done((data) => { - dispatch(set({ - section, - isFetchingSchema: false, - schemaPopulated: true, - schemaError: null, - schema: data - })); - }); + promise.done((data) => { + dispatch(set({ + section, + isFetchingSchema: false, + schemaPopulated: true, + schemaError: null, + schema: data + })); + }); - promise.fail((xhr) => { - dispatch(set({ - section, - isFetchingSchema: false, - schemaPopulated: true, - schemaError: xhr - })); - }); - }; + promise.fail((xhr) => { + dispatch(set({ + section, + isFetchingSchema: false, + schemaPopulated: true, + schemaError: xhr + })); + }); }; } diff --git a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js index 4f8d779a8..d4c07e8cb 100644 --- a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js +++ b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js @@ -1,53 +1,51 @@ import _ from 'lodash'; import $ from 'jquery'; import { batchActions } from 'redux-batched-actions'; +import getSectionState from 'Utilities/State/getSectionState'; import { set, updateServerSideCollection } from '../baseActions'; -function createFetchServerSideCollectionHandler(section, url, getFromState) { - return function(payload = {}) { - return function(dispatch, getState) { - dispatch(set({ section, isFetching: true })); +function createFetchServerSideCollectionHandler(section, url) { + return function(getState, payload, dispatch) { + dispatch(set({ section, isFetching: true })); - const state = getFromState(getState()); - const sectionState = state.hasOwnProperty(section) ? state[section] : state; - const page = payload.page || sectionState.page || 1; + const sectionState = getSectionState(getState(), section, true); + const page = payload.page || sectionState.page || 1; - const data = Object.assign({ page }, - _.pick(sectionState, [ - 'pageSize', - 'sortDirection', - 'sortKey', - 'filterKey', - 'filterValue' - ])); + const data = Object.assign({ page }, + _.pick(sectionState, [ + 'pageSize', + 'sortDirection', + 'sortKey', + 'filterKey', + 'filterValue' + ])); - const promise = $.ajax({ - url, - data - }); + const promise = $.ajax({ + url, + data + }); - promise.done((response) => { - dispatch(batchActions([ - updateServerSideCollection({ section, data: response }), + promise.done((response) => { + dispatch(batchActions([ + updateServerSideCollection({ section, data: response }), - set({ - section, - isFetching: false, - isPopulated: true, - error: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ + set({ section, isFetching: false, - isPopulated: false, - error: xhr - })); - }); - }; + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); }; } diff --git a/frontend/src/Store/Actions/Creators/createHandleActions.js b/frontend/src/Store/Actions/Creators/createHandleActions.js new file mode 100644 index 000000000..c3315ce94 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createHandleActions.js @@ -0,0 +1,143 @@ +import _ from 'lodash'; +import { handleActions } from 'redux-actions'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import { + SET, + UPDATE, + UPDATE_ITEM, + UPDATE_SERVER_SIDE_COLLECTION, + CLEAR_PENDING_CHANGES, + REMOVE_ITEM +} from 'Store/Actions/baseActions'; + +const blacklistedProperties = [ + 'section', + 'id' +]; + +export default function createHandleActions(handlers, defaultState, section) { + return handleActions({ + + [SET]: function(state, { payload }) { + const payloadSection = payload.section; + const [baseSection] = payloadSection.split('.'); + + if (section === baseSection) { + const newState = Object.assign(getSectionState(state, payloadSection), + _.omit(payload, blacklistedProperties)); + + return updateSectionState(state, payloadSection, newState); + } + + return state; + }, + + [UPDATE]: function(state, { payload }) { + const payloadSection = payload.section; + const [baseSection] = payloadSection.split('.'); + + if (section === baseSection) { + const newState = getSectionState(state, payloadSection); + + if (_.isArray(payload.data)) { + newState.items = payload.data; + } else { + newState.item = payload.data; + } + + return updateSectionState(state, payloadSection, newState); + } + + return state; + }, + + [UPDATE_ITEM]: function(state, { payload }) { + const { + section: payloadSection, + updateOnly = false, + ...otherProps + } = payload; + + const [baseSection] = payloadSection.split('.'); + + if (section === baseSection) { + const newState = getSectionState(state, payloadSection); + const items = newState.items; + const index = _.findIndex(items, { id: payload.id }); + + newState.items = [...items]; + + // TODO: Move adding to it's own reducer + if (index >= 0) { + const item = items[index]; + + newState.items.splice(index, 1, { ...item, ...otherProps }); + } else if (!updateOnly) { + newState.items.push({ ...otherProps }); + } + + return updateSectionState(state, payloadSection, newState); + } + + return state; + }, + + [CLEAR_PENDING_CHANGES]: function(state, { payload }) { + const payloadSection = payload.section; + const [baseSection] = payloadSection.split('.'); + + if (section === baseSection) { + const newState = getSectionState(state, payloadSection); + newState.pendingChanges = {}; + + if (newState.hasOwnProperty('saveError')) { + newState.saveError = null; + } + + return updateSectionState(state, payloadSection, newState); + } + + return state; + }, + + [REMOVE_ITEM]: function(state, { payload }) { + const payloadSection = payload.section; + const [baseSection] = payloadSection.split('.'); + + if (section === baseSection) { + const newState = getSectionState(state, payloadSection); + + newState.items = [...newState.items]; + _.remove(newState.items, { id: payload.id }); + + return updateSectionState(state, payloadSection, newState); + } + + return state; + }, + + [UPDATE_SERVER_SIDE_COLLECTION]: function(state, { payload }) { + const payloadSection = payload.section; + const [baseSection] = payloadSection.split('.'); + + if (section === baseSection) { + const data = payload.data; + const newState = getSectionState(state, payloadSection); + + const serverState = _.omit(data, ['records']); + const calculatedState = { + totalPages: Math.max(Math.ceil(data.totalRecords / data.pageSize), 1), + items: data.records + }; + + return updateSectionState(state, payloadSection, Object.assign(newState, serverState, calculatedState)); + } + + return state; + }, + + ...handlers + + }, defaultState); +} diff --git a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js index f09a05948..c554187e0 100644 --- a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js +++ b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js @@ -3,44 +3,42 @@ import { batchActions } from 'redux-batched-actions'; import { set, removeItem } from '../baseActions'; function createRemoveItemHandler(section, url) { - return function(payload) { - return function(dispatch, getState) { - const { - id, - ...queryParms - } = payload; + return function(getState, payload, dispatch) { + const { + id, + ...queryParms + } = payload; - dispatch(set({ section, isDeleting: true })); + dispatch(set({ section, isDeleting: true })); - const ajaxOptions = { - url: `${url}/${id}?${$.param(queryParms, true)}`, - method: 'DELETE' - }; + const ajaxOptions = { + url: `${url}/${id}?${$.param(queryParms, true)}`, + method: 'DELETE' + }; - const promise = $.ajax(ajaxOptions); + const promise = $.ajax(ajaxOptions); - promise.done((data) => { - dispatch(batchActions([ - removeItem({ section, id }), + promise.done((data) => { + dispatch(batchActions([ + removeItem({ section, id }), - set({ - section, - isDeleting: false, - deleteError: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ + set({ section, isDeleting: false, - deleteError: xhr - })); - }); + deleteError: null + }) + ])); + }); - return promise; - }; + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + + return promise; }; } diff --git a/frontend/src/Store/Actions/Creators/createSaveHandler.js b/frontend/src/Store/Actions/Creators/createSaveHandler.js index 76048192e..8711e4acd 100644 --- a/frontend/src/Store/Actions/Creators/createSaveHandler.js +++ b/frontend/src/Store/Actions/Creators/createSaveHandler.js @@ -1,43 +1,42 @@ import $ from 'jquery'; import { batchActions } from 'redux-batched-actions'; +import getSectionState from 'Utilities/State/getSectionState'; import { set, update } from '../baseActions'; -function createSaveHandler(section, url, getFromState) { - return function(payload) { - return function(dispatch, getState) { - dispatch(set({ section, isSaving: true })); +function createSaveHandler(section, url) { + return function(getState, payload, dispatch) { + dispatch(set({ section, isSaving: true })); - const state = getFromState(getState()); - const saveData = Object.assign({}, state.item, state.pendingChanges); + const state = getSectionState(getState(), section, true); + const saveData = Object.assign({}, state.item, state.pendingChanges); - const promise = $.ajax({ - url, - method: 'PUT', - dataType: 'json', - data: JSON.stringify(saveData) - }); + const promise = $.ajax({ + url, + method: 'PUT', + dataType: 'json', + data: JSON.stringify(saveData) + }); - promise.done((data) => { - dispatch(batchActions([ - update({ section, data }), + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), - set({ - section, - isSaving: false, - saveError: null, - pendingChanges: {} - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ + set({ section, isSaving: false, - saveError: xhr - })); - }); - }; + saveError: null, + pendingChanges: {} + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); }; } diff --git a/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js index 7d9f88ee0..97f81cecb 100644 --- a/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js +++ b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js @@ -6,62 +6,58 @@ import { set, updateItem } from '../baseActions'; const abortCurrentRequests = {}; export function createCancelSaveProviderHandler(section) { - return function(payload) { - return function(dispatch, getState) { - if (abortCurrentRequests[section]) { - abortCurrentRequests[section](); - abortCurrentRequests[section] = null; - } - }; + return function(getState, payload, dispatch) { + if (abortCurrentRequests[section]) { + abortCurrentRequests[section](); + abortCurrentRequests[section] = null; + } }; } -function createSaveProviderHandler(section, url, getFromState) { - return function(payload) { - return function(dispatch, getState) { - dispatch(set({ section, isSaving: true })); +function createSaveProviderHandler(section, url) { + return function(getState, payload, dispatch) { + dispatch(set({ section, isSaving: true })); - const id = payload.id; - const saveData = getProviderState(payload, getState, getFromState); + const id = payload.id; + const saveData = getProviderState(payload, getState, section); - const ajaxOptions = { - url, - method: 'POST', - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(saveData) - }; + const ajaxOptions = { + url, + method: 'POST', + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(saveData) + }; - if (id) { - ajaxOptions.url = `${url}/${id}`; - ajaxOptions.method = 'PUT'; - } + if (id) { + ajaxOptions.url = `${url}/${id}`; + ajaxOptions.method = 'PUT'; + } - const { request, abortRequest } = createAjaxRequest(ajaxOptions); - - abortCurrentRequests[section] = abortRequest; + const { request, abortRequest } = createAjaxRequest(ajaxOptions); - request.done((data) => { - dispatch(batchActions([ - updateItem({ section, ...data }), + abortCurrentRequests[section] = abortRequest; - set({ - section, - isSaving: false, - saveError: null, - pendingChanges: {} - }) - ])); - }); + request.done((data) => { + dispatch(batchActions([ + updateItem({ section, ...data }), - request.fail((xhr) => { - dispatch(set({ + set({ section, isSaving: false, - saveError: xhr.aborted ? null : xhr - })); - }); - }; + saveError: null, + pendingChanges: {} + }) + ])); + }); + + request.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr.aborted ? null : xhr + })); + }); }; } diff --git a/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js b/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js index 91cef5d5e..d036cb1ae 100644 --- a/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js +++ b/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js @@ -5,45 +5,45 @@ import createSetServerSideCollectionPageHandler from './createSetServerSideColle import createSetServerSideCollectionSortHandler from './createSetServerSideCollectionSortHandler'; import createSetServerSideCollectionFilterHandler from './createSetServerSideCollectionFilterHandler'; -function createServerSideCollectionHandlers(section, url, getFromState, handlers) { +function createServerSideCollectionHandlers(section, url, fetchThunk, handlers) { const actionHandlers = {}; const fetchHandlerType = handlers[serverSideCollectionHandlers.FETCH]; - const fetchHandler = createFetchServerSideCollectionHandler(section, url, getFromState); + const fetchHandler = createFetchServerSideCollectionHandler(section, url); actionHandlers[fetchHandlerType] = fetchHandler; if (handlers.hasOwnProperty(serverSideCollectionHandlers.FIRST_PAGE)) { const handlerType = handlers[serverSideCollectionHandlers.FIRST_PAGE]; - actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.FIRST, getFromState, fetchHandler); + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.FIRST, fetchThunk); } if (handlers.hasOwnProperty(serverSideCollectionHandlers.PREVIOUS_PAGE)) { const handlerType = handlers[serverSideCollectionHandlers.PREVIOUS_PAGE]; - actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.PREVIOUS, getFromState, fetchHandler); + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.PREVIOUS, fetchThunk); } if (handlers.hasOwnProperty(serverSideCollectionHandlers.NEXT_PAGE)) { const handlerType = handlers[serverSideCollectionHandlers.NEXT_PAGE]; - actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.NEXT, getFromState, fetchHandler); + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.NEXT, fetchThunk); } if (handlers.hasOwnProperty(serverSideCollectionHandlers.LAST_PAGE)) { const handlerType = handlers[serverSideCollectionHandlers.LAST_PAGE]; - actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.LAST, getFromState, fetchHandler); + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.LAST, fetchThunk); } if (handlers.hasOwnProperty(serverSideCollectionHandlers.EXACT_PAGE)) { const handlerType = handlers[serverSideCollectionHandlers.EXACT_PAGE]; - actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.EXACT, getFromState, fetchHandler); + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.EXACT, fetchThunk); } if (handlers.hasOwnProperty(serverSideCollectionHandlers.SORT)) { const handlerType = handlers[serverSideCollectionHandlers.SORT]; - actionHandlers[handlerType] = createSetServerSideCollectionSortHandler(section, getFromState, fetchHandler); + actionHandlers[handlerType] = createSetServerSideCollectionSortHandler(section, fetchThunk); } if (handlers.hasOwnProperty(serverSideCollectionHandlers.FILTER)) { const handlerType = handlers[serverSideCollectionHandlers.FILTER]; - actionHandlers[handlerType] = createSetServerSideCollectionFilterHandler(section, getFromState, fetchHandler); + actionHandlers[handlerType] = createSetServerSideCollectionFilterHandler(section, fetchThunk); } return actionHandlers; diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js index 0aaa342db..d7e476444 100644 --- a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js @@ -1,11 +1,9 @@ import { set } from '../baseActions'; -function createSetServerSideCollectionFilterHandler(section, getFromState, fetchHandler) { - return function(payload) { - return function(dispatch, getState) { - dispatch(set({ section, ...payload })); - dispatch(fetchHandler({ page: 1 })); - }; +function createSetServerSideCollectionFilterHandler(section, fetchHandler) { + return function(getState, payload, dispatch) { + dispatch(set({ section, ...payload })); + dispatch(fetchHandler({ page: 1 })); }; } diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js index 88682f118..12b21bb0d 100644 --- a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js @@ -1,36 +1,34 @@ import pages from 'Utilities/pages'; +import getSectionState from 'Utilities/State/getSectionState'; -function createSetServerSideCollectionPageHandler(section, page, getFromState, fetchHandler) { - return function(payload) { - return function(dispatch, getState) { - const state = getFromState(getState()); - const sectionState = state.hasOwnProperty(section) ? state[section] : state; - const currentPage = sectionState.page || 1; - let nextPage = 0; +function createSetServerSideCollectionPageHandler(section, page, fetchHandler) { + return function(getState, payload, dispatch) { + const sectionState = getSectionState(getState(), section, true); + const currentPage = sectionState.page || 1; + let nextPage = 0; - switch (page) { - case pages.FIRST: - nextPage = 1; - break; - case pages.PREVIOUS: - nextPage = currentPage - 1; - break; - case pages.NEXT: - nextPage = currentPage + 1; - break; - case pages.LAST: - nextPage = sectionState.totalPages; - break; - default: - nextPage = payload.page; - } + switch (page) { + case pages.FIRST: + nextPage = 1; + break; + case pages.PREVIOUS: + nextPage = currentPage - 1; + break; + case pages.NEXT: + nextPage = currentPage + 1; + break; + case pages.LAST: + nextPage = sectionState.totalPages; + break; + default: + nextPage = payload.page; + } - // If we prefer to update the page immediately we should - // set the page and not pass a page to the fetch handler. + // If we prefer to update the page immediately we should + // set the page and not pass a page to the fetch handler. - // dispatch(set({ section, page: nextPage })); - dispatch(fetchHandler({ page: nextPage })); - }; + // dispatch(set({ section, page: nextPage })); + dispatch(fetchHandler({ page: nextPage })); }; } diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js index 6ee0ac4b2..fbd66e83e 100644 --- a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js @@ -1,27 +1,25 @@ +import getSectionState from 'Utilities/State/getSectionState'; import { sortDirections } from 'Helpers/Props'; import { set } from '../baseActions'; -function createSetServerSideCollectionSortHandler(section, getFromState, fetchHandler) { - return function(payload) { - return function(dispatch, getState) { - const state = getFromState(getState()); - const sectionState = state.hasOwnProperty(section) ? state[section] : state; - const sortKey = payload.sortKey || sectionState.sortKey; - let sortDirection = payload.sortDirection; +function createSetServerSideCollectionSortHandler(section, fetchHandler) { + return function(getState, payload, dispatch) { + const sectionState = getSectionState(getState(), section, true); + const sortKey = payload.sortKey || sectionState.sortKey; + let sortDirection = payload.sortDirection; - if (!sortDirection) { - if (payload.sortKey === sectionState.sortKey) { - sortDirection = sectionState.sortDirection === sortDirections.ASCENDING ? - sortDirections.DESCENDING : - sortDirections.ASCENDING; - } else { - sortDirection = sectionState.sortDirection; - } + if (!sortDirection) { + if (payload.sortKey === sectionState.sortKey) { + sortDirection = sectionState.sortDirection === sortDirections.ASCENDING ? + sortDirections.DESCENDING : + sortDirections.ASCENDING; + } else { + sortDirection = sectionState.sortDirection; } + } - dispatch(set({ section, sortKey, sortDirection })); - dispatch(fetchHandler()); - }; + dispatch(set({ section, sortKey, sortDirection })); + dispatch(fetchHandler()); }; } diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js index 352eec001..ca26883fb 100644 --- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js +++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js @@ -5,51 +5,47 @@ import { set } from '../baseActions'; const abortCurrentRequests = {}; export function createCancelTestProviderHandler(section) { - return function(payload) { - return function(dispatch, getState) { - if (abortCurrentRequests[section]) { - abortCurrentRequests[section](); - abortCurrentRequests[section] = null; - } - }; + return function(getState, payload, dispatch) { + if (abortCurrentRequests[section]) { + abortCurrentRequests[section](); + abortCurrentRequests[section] = null; + } }; } -function createTestProviderHandler(section, url, getFromState) { - return function(payload) { - return function(dispatch, getState) { - dispatch(set({ section, isTesting: true })); +function createTestProviderHandler(section, url) { + return function(getState, payload, dispatch) { + dispatch(set({ section, isTesting: true })); - const testData = getProviderState(payload, getState, getFromState); + const testData = getProviderState(payload, getState, section); - const ajaxOptions = { - url: `${url}/test`, - method: 'POST', - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(testData) - }; - - const { request, abortRequest } = createAjaxRequest(ajaxOptions); - - abortCurrentRequests[section] = abortRequest; - - request.done((data) => { - dispatch(set({ - section, - isTesting: false, - saveError: null - })); - }); - - request.fail((xhr) => { - dispatch(set({ - section, - isTesting: false, - saveError: xhr.aborted ? null : xhr - })); - }); + const ajaxOptions = { + url: `${url}/test`, + method: 'POST', + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(testData) }; + + const { request, abortRequest } = createAjaxRequest(ajaxOptions); + + abortCurrentRequests[section] = abortRequest; + + request.done((data) => { + dispatch(set({ + section, + isTesting: false, + saveError: null + })); + }); + + request.fail((xhr) => { + dispatch(set({ + section, + isTesting: false, + saveError: xhr.aborted ? null : xhr + })); + }); }; } diff --git a/frontend/src/Store/Actions/Creators/createToggleAlbumMonitoredHandler.js b/frontend/src/Store/Actions/Creators/createToggleAlbumMonitoredHandler.js index 94581aec9..79e6326ca 100644 --- a/frontend/src/Store/Actions/Creators/createToggleAlbumMonitoredHandler.js +++ b/frontend/src/Store/Actions/Creators/createToggleAlbumMonitoredHandler.js @@ -1,7 +1,8 @@ import $ from 'jquery'; import updateAlbums from 'Utilities/Album/updateAlbums'; +import getSectionState from 'Utilities/State/getSectionState'; -function createToggleAlbumMonitoredHandler(section, getFromState) { +function createToggleAlbumMonitoredHandler(section) { return function(payload) { return function(dispatch, getState) { const { @@ -9,8 +10,8 @@ function createToggleAlbumMonitoredHandler(section, getFromState) { monitored } = payload; - const state = getFromState(getState()); - + const state = getSectionState(getState(), section, true); + updateAlbums(dispatch, section, state.items, [albumId], { isSaving: true }); diff --git a/frontend/src/Store/Actions/Settings/delayProfiles.js b/frontend/src/Store/Actions/Settings/delayProfiles.js new file mode 100644 index 000000000..68232e510 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/delayProfiles.js @@ -0,0 +1,103 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; +import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; +import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; +import { update } from 'Store/Actions/baseActions'; + +// +// Variables + +const section = 'settings.delayProfiles'; + +// +// Actions Types + +export const FETCH_DELAY_PROFILES = 'settings/delayProfiles/fetchDelayProfiles'; +export const FETCH_DELAY_PROFILE_SCHEMA = 'settings/delayProfiles/fetchDelayProfileSchema'; +export const SAVE_DELAY_PROFILE = 'settings/delayProfiles/saveDelayProfile'; +export const DELETE_DELAY_PROFILE = 'settings/delayProfiles/deleteDelayProfile'; +export const REORDER_DELAY_PROFILE = 'settings/delayProfiles/reorderDelayProfile'; +export const SET_DELAY_PROFILE_VALUE = 'settings/delayProfiles/setDelayProfileValue'; + +// +// Action Creators + +export const fetchDelayProfiles = createThunk(FETCH_DELAY_PROFILES); +export const fetchDelayProfileSchema = createThunk(FETCH_DELAY_PROFILE_SCHEMA); +export const saveDelayProfile = createThunk(SAVE_DELAY_PROFILE); +export const deleteDelayProfile = createThunk(DELETE_DELAY_PROFILE); +export const reorderDelayProfile = createThunk(REORDER_DELAY_PROFILE); + +export const setDelayProfileValue = createAction(SET_DELAY_PROFILE_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isSaving: false, + saveError: null, + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_DELAY_PROFILES]: createFetchHandler(section, '/delayprofile'), + [FETCH_DELAY_PROFILE_SCHEMA]: createFetchSchemaHandler(section, '/delayprofile/schema'), + + [SAVE_DELAY_PROFILE]: createSaveProviderHandler(section, '/delayprofile'), + [DELETE_DELAY_PROFILE]: createRemoveItemHandler(section, '/delayprofile'), + + [REORDER_DELAY_PROFILE]: (getState, payload, dispatch) => { + const { id, moveIndex } = payload; + const moveOrder = moveIndex + 1; + const delayProfiles = getState().settings.delayProfiles.items; + const moving = _.find(delayProfiles, { id }); + + // Don't move if the order hasn't changed + if (moving.order === moveOrder) { + return; + } + + const after = moveIndex > 0 ? _.find(delayProfiles, { order: moveIndex }) : null; + const afterQueryParam = after ? `after=${after.id}` : ''; + + const promise = $.ajax({ + method: 'PUT', + url: `/delayprofile/reorder/${id}?${afterQueryParam}` + }); + + promise.done((data) => { + dispatch(update({ section, data })); + }); + } + }, + + // + // Reducers + + reducers: { + [SET_DELAY_PROFILE_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/downloadClientOptions.js b/frontend/src/Store/Actions/Settings/downloadClientOptions.js new file mode 100644 index 000000000..6d4a3954d --- /dev/null +++ b/frontend/src/Store/Actions/Settings/downloadClientOptions.js @@ -0,0 +1,64 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveHandler from 'Store/Actions/Creators/createSaveHandler'; + +// +// Variables + +const section = 'settings.downloadClientOptions'; + +// +// Actions Types + +export const FETCH_DOWNLOAD_CLIENT_OPTIONS = 'FETCH_DOWNLOAD_CLIENT_OPTIONS'; +export const SET_DOWNLOAD_CLIENT_OPTIONS_VALUE = 'SET_DOWNLOAD_CLIENT_OPTIONS_VALUE'; +export const SAVE_DOWNLOAD_CLIENT_OPTIONS = 'SAVE_DOWNLOAD_CLIENT_OPTIONS'; + +// +// Action Creators + +export const fetchDownloadClientOptions = createThunk(FETCH_DOWNLOAD_CLIENT_OPTIONS); +export const saveDownloadClientOptions = createThunk(SAVE_DOWNLOAD_CLIENT_OPTIONS); +export const setDownloadClientOptionsValue = createAction(SET_DOWNLOAD_CLIENT_OPTIONS_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_DOWNLOAD_CLIENT_OPTIONS]: createFetchHandler(section, '/config/downloadclient'), + [SAVE_DOWNLOAD_CLIENT_OPTIONS]: createSaveHandler(section, '/config/downloadclient') + }, + + // + // Reducers + + reducers: { + [SET_DOWNLOAD_CLIENT_OPTIONS_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js new file mode 100644 index 000000000..c8b8659e6 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/downloadClients.js @@ -0,0 +1,112 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import selectProviderSchema from 'Utilities/State/selectProviderSchema'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; +import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; +import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; +import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; + +// +// Variables + +const section = 'settings.downloadClients'; + +// +// Actions Types + +export const FETCH_DOWNLOAD_CLIENTS = 'settings/downloadClients/fetchDownloadClients'; +export const FETCH_DOWNLOAD_CLIENT_SCHEMA = 'settings/downloadClients/fetchDownloadClientSchema'; +export const SELECT_DOWNLOAD_CLIENT_SCHEMA = 'settings/downloadClients/selectDownloadClientSchema'; +export const SET_DOWNLOAD_CLIENT_VALUE = 'settings/downloadClients/setDownloadClientValue'; +export const SET_DOWNLOAD_CLIENT_FIELD_VALUE = 'settings/downloadClients/setDownloadClientFieldValue'; +export const SAVE_DOWNLOAD_CLIENT = 'settings/downloadClients/saveDownloadClient'; +export const CANCEL_SAVE_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelSaveDownloadClient'; +export const DELETE_DOWNLOAD_CLIENT = 'settings/downloadClients/deleteDownloadClient'; +export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient'; +export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient'; + +// +// Action Creators + +export const fetchDownloadClients = createThunk(FETCH_DOWNLOAD_CLIENTS); +export const fetchDownloadClientSchema = createThunk(FETCH_DOWNLOAD_CLIENT_SCHEMA); +export const selectDownloadClientSchema = createAction(SELECT_DOWNLOAD_CLIENT_SCHEMA); + +export const saveDownloadClient = createThunk(SAVE_DOWNLOAD_CLIENT); +export const cancelSaveDownloadClient = createThunk(CANCEL_SAVE_DOWNLOAD_CLIENT); +export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT); +export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT); +export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT); + +export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +export const setDownloadClientFieldValue = createAction(SET_DOWNLOAD_CLIENT_FIELD_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: [], + selectedSchema: {}, + isSaving: false, + saveError: null, + isTesting: false, + items: [], + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'), + [FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'), + + [SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler(section, '/downloadclient'), + [CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section), + [DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'), + [TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'), + [CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section) + }, + + // + // Reducers + + reducers: { + [SET_DOWNLOAD_CLIENT_VALUE]: createSetSettingValueReducer(section), + [SET_DOWNLOAD_CLIENT_FIELD_VALUE]: createSetProviderFieldValueReducer(section), + + [SELECT_DOWNLOAD_CLIENT_SCHEMA]: (state, { payload }) => { + return selectProviderSchema(state, section, payload, (selectedSchema) => { + selectedSchema.enable = true; + + return selectedSchema; + }); + } + } + +}; diff --git a/frontend/src/Store/Actions/Settings/general.js b/frontend/src/Store/Actions/Settings/general.js new file mode 100644 index 000000000..f5e8c277e --- /dev/null +++ b/frontend/src/Store/Actions/Settings/general.js @@ -0,0 +1,64 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveHandler from 'Store/Actions/Creators/createSaveHandler'; + +// +// Variables + +const section = 'settings.general'; + +// +// Actions Types + +export const FETCH_GENERAL_SETTINGS = 'settings/general/fetchGeneralSettings'; +export const SET_GENERAL_SETTINGS_VALUE = 'settings/general/setGeneralSettingsValue'; +export const SAVE_GENERAL_SETTINGS = 'settings/general/saveGeneralSettings'; + +// +// Action Creators + +export const fetchGeneralSettings = createThunk(FETCH_GENERAL_SETTINGS); +export const saveGeneralSettings = createThunk(SAVE_GENERAL_SETTINGS); +export const setGeneralSettingsValue = createAction(SET_GENERAL_SETTINGS_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_GENERAL_SETTINGS]: createFetchHandler(section, '/config/host'), + [SAVE_GENERAL_SETTINGS]: createSaveHandler(section, '/config/host') + }, + + // + // Reducers + + reducers: { + [SET_GENERAL_SETTINGS_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/indexerOptions.js b/frontend/src/Store/Actions/Settings/indexerOptions.js new file mode 100644 index 000000000..53fb21651 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/indexerOptions.js @@ -0,0 +1,64 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveHandler from 'Store/Actions/Creators/createSaveHandler'; + +// +// Variables + +const section = 'settings.indexerOptions'; + +// +// Actions Types + +export const FETCH_INDEXER_OPTIONS = 'settings/indexerOptions/fetchIndexerOptions'; +export const SAVE_INDEXER_OPTIONS = 'settings/indexerOptions/saveIndexerOptions'; +export const SET_INDEXER_OPTIONS_VALUE = 'settings/indexerOptions/setIndexerOptionsValue'; + +// +// Action Creators + +export const fetchIndexerOptions = createThunk(FETCH_INDEXER_OPTIONS); +export const saveIndexerOptions = createThunk(SAVE_INDEXER_OPTIONS); +export const setIndexerOptionsValue = createAction(SET_INDEXER_OPTIONS_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_INDEXER_OPTIONS]: createFetchHandler(section, '/config/indexer'), + [SAVE_INDEXER_OPTIONS]: createSaveHandler(section, '/config/indexer') + }, + + // + // Reducers + + reducers: { + [SET_INDEXER_OPTIONS_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js new file mode 100644 index 000000000..47d271b2b --- /dev/null +++ b/frontend/src/Store/Actions/Settings/indexers.js @@ -0,0 +1,113 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import selectProviderSchema from 'Utilities/State/selectProviderSchema'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; +import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; +import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; +import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; + +// +// Variables + +const section = 'settings.indexers'; + +// +// Actions Types + +export const FETCH_INDEXERS = 'settings/indexers/fetchIndexers'; +export const FETCH_INDEXER_SCHEMA = 'settings/indexers/fetchIndexerSchema'; +export const SELECT_INDEXER_SCHEMA = 'settings/indexers/selectIndexerSchema'; +export const SET_INDEXER_VALUE = 'settings/indexers/setIndexerValue'; +export const SET_INDEXER_FIELD_VALUE = 'settings/indexers/setIndexerFieldValue'; +export const SAVE_INDEXER = 'settings/indexers/saveIndexer'; +export const CANCEL_SAVE_INDEXER = 'settings/indexers/cancelSaveIndexer'; +export const DELETE_INDEXER = 'settings/indexers/deleteIndexer'; +export const TEST_INDEXER = 'settings/indexers/testIndexer'; +export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer'; + +// +// Action Creators + +export const fetchIndexers = createThunk(FETCH_INDEXERS); +export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA); +export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA); + +export const saveIndexer = createThunk(SAVE_INDEXER); +export const cancelSaveIndexer = createThunk(CANCEL_SAVE_INDEXER); +export const deleteIndexer = createThunk(DELETE_INDEXER); +export const testIndexer = createThunk(TEST_INDEXER); +export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER); + +export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +export const setIndexerFieldValue = createAction(SET_INDEXER_FIELD_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: [], + selectedSchema: {}, + isSaving: false, + saveError: null, + isTesting: false, + items: [], + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_INDEXERS]: createFetchHandler(section, '/indexer'), + [FETCH_INDEXER_SCHEMA]: createFetchSchemaHandler(section, '/indexer/schema'), + + [SAVE_INDEXER]: createSaveProviderHandler(section, '/indexer'), + [CANCEL_SAVE_INDEXER]: createCancelSaveProviderHandler(section), + [DELETE_INDEXER]: createRemoveItemHandler(section, '/indexer'), + [TEST_INDEXER]: createTestProviderHandler(section, '/indexer'), + [CANCEL_TEST_INDEXER]: createCancelTestProviderHandler(section) + }, + + // + // Reducers + + reducers: { + [SET_INDEXER_VALUE]: createSetSettingValueReducer(section), + [SET_INDEXER_FIELD_VALUE]: createSetProviderFieldValueReducer(section), + + [SELECT_INDEXER_SCHEMA]: (state, { payload }) => { + return selectProviderSchema(state, section, payload, (selectedSchema) => { + selectedSchema.enableRss = selectedSchema.supportsRss; + selectedSchema.enableSearch = selectedSchema.supportsSearch; + + return selectedSchema; + }); + } + } + +}; diff --git a/frontend/src/Store/Actions/Settings/languageProfiles.js b/frontend/src/Store/Actions/Settings/languageProfiles.js new file mode 100644 index 000000000..8e1f62ba6 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/languageProfiles.js @@ -0,0 +1,79 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; +import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; +import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; + +// +// Variables + +const section = 'settings.languageProfiles'; + +// +// Actions Types + +export const FETCH_LANGUAGE_PROFILES = 'settings/languageProfiles/fetchLanguageProfiles'; +export const FETCH_LANGUAGE_PROFILE_SCHEMA = 'settings/languageProfiles/fetchLanguageProfileSchema'; +export const SAVE_LANGUAGE_PROFILE = 'settings/languageProfiles/saveLanguageProfile'; +export const DELETE_LANGUAGE_PROFILE = 'settings/languageProfiles/deleteLanguageProfile'; +export const SET_LANGUAGE_PROFILE_VALUE = 'settings/languageProfiles/setLanguageProfileValue'; + +// +// Action Creators + +export const fetchLanguageProfiles = createThunk(FETCH_LANGUAGE_PROFILES); +export const fetchLanguageProfileSchema = createThunk(FETCH_LANGUAGE_PROFILE_SCHEMA); +export const saveLanguageProfile = createThunk(SAVE_LANGUAGE_PROFILE); +export const deleteLanguageProfile = createThunk(DELETE_LANGUAGE_PROFILE); + +export const setLanguageProfileValue = createAction(SET_LANGUAGE_PROFILE_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: {}, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_LANGUAGE_PROFILES]: createFetchHandler(section, '/languageprofile'), + [FETCH_LANGUAGE_PROFILE_SCHEMA]: createFetchSchemaHandler(section, '/languageprofile/schema'), + [SAVE_LANGUAGE_PROFILE]: createSaveProviderHandler(section, '/languageprofile'), + [DELETE_LANGUAGE_PROFILE]: createRemoveItemHandler(section, '/languageprofile') + }, + + // + // Reducers + + reducers: { + [SET_LANGUAGE_PROFILE_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/mediaManagement.js b/frontend/src/Store/Actions/Settings/mediaManagement.js new file mode 100644 index 000000000..4ae9eba0c --- /dev/null +++ b/frontend/src/Store/Actions/Settings/mediaManagement.js @@ -0,0 +1,64 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveHandler from 'Store/Actions/Creators/createSaveHandler'; + +// +// Variables + +const section = 'settings.mediaManagement'; + +// +// Actions Types + +export const FETCH_MEDIA_MANAGEMENT_SETTINGS = 'settings/mediaManagement/fetchMediaManagementSettings'; +export const SAVE_MEDIA_MANAGEMENT_SETTINGS = 'settings/mediaManagement/saveMediaManagementSettings'; +export const SET_MEDIA_MANAGEMENT_SETTINGS_VALUE = 'settings/mediaManagement/setMediaManagementSettingsValue'; + +// +// Action Creators + +export const fetchMediaManagementSettings = createThunk(FETCH_MEDIA_MANAGEMENT_SETTINGS); +export const saveMediaManagementSettings = createThunk(SAVE_MEDIA_MANAGEMENT_SETTINGS); +export const setMediaManagementSettingsValue = createAction(SET_MEDIA_MANAGEMENT_SETTINGS_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_MEDIA_MANAGEMENT_SETTINGS]: createFetchHandler(section, '/config/mediamanagement'), + [SAVE_MEDIA_MANAGEMENT_SETTINGS]: createSaveHandler(section, '/config/mediamanagement') + }, + + // + // Reducers + + reducers: { + [SET_MEDIA_MANAGEMENT_SETTINGS_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/metadata.js b/frontend/src/Store/Actions/Settings/metadata.js new file mode 100644 index 000000000..ed5e0aa86 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/metadata.js @@ -0,0 +1,75 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; + +// +// Variables + +const section = 'settings.metadata'; + +// +// Actions Types + +export const FETCH_METADATA = 'settings/metadata/fetchMetadata'; +export const SET_METADATA_VALUE = 'settings/metadata/setMetadataValue'; +export const SET_METADATA_FIELD_VALUE = 'settings/metadata/setMetadataFieldValue'; +export const SAVE_METADATA = 'settings/metadata/saveMetadata'; + +// +// Action Creators + +export const fetchMetadata = createThunk(FETCH_METADATA); +export const saveMetadata = createThunk(SAVE_METADATA); + +export const setMetadataValue = createAction(SET_METADATA_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +export const setMetadataFieldValue = createAction(SET_METADATA_FIELD_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_METADATA]: createFetchHandler(section, '/metadata'), + [SAVE_METADATA]: createSaveProviderHandler(section, '/metadata') + }, + + // + // Reducers + + reducers: { + [SET_METADATA_VALUE]: createSetSettingValueReducer(section), + [SET_METADATA_FIELD_VALUE]: createSetProviderFieldValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/metadataProfiles.js b/frontend/src/Store/Actions/Settings/metadataProfiles.js new file mode 100644 index 000000000..3bf46fa27 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/metadataProfiles.js @@ -0,0 +1,79 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; +import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; +import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; + +// +// Variables + +const section = 'settings.metadataProfiles'; + +// +// Actions Types + +export const FETCH_METADATA_PROFILES = 'settings/metadataProfiles/fetchMetadataProfiles'; +export const FETCH_METADATA_PROFILE_SCHEMA = 'settings/metadataProfiles/fetchMetadataProfileSchema'; +export const SAVE_METADATA_PROFILE = 'settings/metadataProfiles/saveMetadataProfile'; +export const DELETE_METADATA_PROFILE = 'settings/metadataProfiles/deleteMetadataProfile'; +export const SET_METADATA_PROFILE_VALUE = 'settings/metadataProfiles/setMetadataProfileValue'; + +// +// Action Creators + +export const fetchMetadataProfiles = createThunk(FETCH_METADATA_PROFILES); +export const fetchMetadataProfileSchema = createThunk(FETCH_METADATA_PROFILE_SCHEMA); +export const saveMetadataProfile = createThunk(SAVE_METADATA_PROFILE); +export const deleteMetadataProfile = createThunk(DELETE_METADATA_PROFILE); + +export const setMetadataProfileValue = createAction(SET_METADATA_PROFILE_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: {}, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_METADATA_PROFILES]: createFetchHandler(section, '/metadataprofile'), + [FETCH_METADATA_PROFILE_SCHEMA]: createFetchSchemaHandler(section, '/metadataprofile/schema'), + [SAVE_METADATA_PROFILE]: createSaveProviderHandler(section, '/metadataprofile'), + [DELETE_METADATA_PROFILE]: createRemoveItemHandler(section, '/metadataprofile') + }, + + // + // Reducers + + reducers: { + [SET_METADATA_PROFILE_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/metadataProvider.js b/frontend/src/Store/Actions/Settings/metadataProvider.js new file mode 100644 index 000000000..32ebd88a0 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/metadataProvider.js @@ -0,0 +1,64 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveHandler from 'Store/Actions/Creators/createSaveHandler'; + +// +// Variables + +const section = 'settings.metadataProvider'; + +// +// Actions Types + +export const FETCH_METADATA_PROVIDER = 'settings/metadataProvider/fetchMetadataProvider'; +export const SET_METADATA_PROVIDER_VALUE = 'settings/metadataProvider/setMetadataProviderValue'; +export const SAVE_METADATA_PROVIDER = 'settings/metadataProvider/saveMetadataProvider'; + +// +// Action Creators + +export const fetchMetadataProvider = createThunk(FETCH_METADATA_PROVIDER); +export const saveMetadataProvider = createThunk(SAVE_METADATA_PROVIDER); +export const setMetadataProviderValue = createAction(SET_METADATA_PROVIDER_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_METADATA_PROVIDER]: createFetchHandler(section, '/config/metadataProvider'), + [SAVE_METADATA_PROVIDER]: createSaveHandler(section, '/config/metadataProvider') + }, + + // + // Reducers + + reducers: { + [SET_METADATA_PROVIDER_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/naming.js b/frontend/src/Store/Actions/Settings/naming.js new file mode 100644 index 000000000..27add8309 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/naming.js @@ -0,0 +1,64 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveHandler from 'Store/Actions/Creators/createSaveHandler'; + +// +// Variables + +const section = 'settings.naming'; + +// +// Actions Types + +export const FETCH_NAMING_SETTINGS = 'settings/naming/fetchNamingSettings'; +export const SAVE_NAMING_SETTINGS = 'settings/naming/saveNamingSettings'; +export const SET_NAMING_SETTINGS_VALUE = 'settings/naming/setNamingSettingsValue'; + +// +// Action Creators + +export const fetchNamingSettings = createThunk(FETCH_NAMING_SETTINGS); +export const saveNamingSettings = createThunk(SAVE_NAMING_SETTINGS); +export const setNamingSettingsValue = createAction(SET_NAMING_SETTINGS_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_NAMING_SETTINGS]: createFetchHandler(section, '/config/naming'), + [SAVE_NAMING_SETTINGS]: createSaveHandler(section, '/config/naming') + }, + + // + // Reducers + + reducers: { + [SET_NAMING_SETTINGS_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/namingExamples.js b/frontend/src/Store/Actions/Settings/namingExamples.js new file mode 100644 index 000000000..e3f2ae01c --- /dev/null +++ b/frontend/src/Store/Actions/Settings/namingExamples.js @@ -0,0 +1,79 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { createThunk } from 'Store/thunks'; +import { set, update } from 'Store/Actions/baseActions'; + +// +// Variables + +const section = 'settings.namingExamples'; + +// +// Actions Types + +export const FETCH_NAMING_EXAMPLES = 'settings/namingExamples/fetchNamingExamples'; + +// +// Action Creators + +export const fetchNamingExamples = createThunk(FETCH_NAMING_EXAMPLES); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + item: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_NAMING_EXAMPLES]: function(getState, payload, dispatch) { + dispatch(set({ section, isFetching: true })); + + const naming = getState().settings.naming; + + const promise = $.ajax({ + url: '/config/naming/examples', + data: Object.assign({}, naming.item, naming.pendingChanges) + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + } + }, + + // + // Reducers + + reducers: {} + +}; diff --git a/frontend/src/Store/Actions/Settings/notifications.js b/frontend/src/Store/Actions/Settings/notifications.js new file mode 100644 index 000000000..96a0792bb --- /dev/null +++ b/frontend/src/Store/Actions/Settings/notifications.js @@ -0,0 +1,115 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import selectProviderSchema from 'Utilities/State/selectProviderSchema'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; +import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; +import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; +import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; + +// +// Variables + +const section = 'settings.notifications'; + +// +// Actions Types + +export const FETCH_NOTIFICATIONS = 'settings/notifications/fetchNotifications'; +export const FETCH_NOTIFICATION_SCHEMA = 'settings/notifications/fetchNotificationSchema'; +export const SELECT_NOTIFICATION_SCHEMA = 'settings/notifications/selectNotificationSchema'; +export const SET_NOTIFICATION_VALUE = 'settings/notifications/setNotificationValue'; +export const SET_NOTIFICATION_FIELD_VALUE = 'settings/notifications/setNotificationFieldValue'; +export const SAVE_NOTIFICATION = 'settings/notifications/saveNotification'; +export const CANCEL_SAVE_NOTIFICATION = 'settings/notifications/cancelSaveNotification'; +export const DELETE_NOTIFICATION = 'settings/notifications/deleteNotification'; +export const TEST_NOTIFICATION = 'settings/notifications/testNotification'; +export const CANCEL_TEST_NOTIFICATION = 'settings/notifications/cancelTestNotification'; + +// +// Action Creators + +export const fetchNotifications = createThunk(FETCH_NOTIFICATIONS); +export const fetchNotificationSchema = createThunk(FETCH_NOTIFICATION_SCHEMA); +export const selectNotificationSchema = createAction(SELECT_NOTIFICATION_SCHEMA); + +export const saveNotification = createThunk(SAVE_NOTIFICATION); +export const cancelSaveNotification = createThunk(CANCEL_SAVE_NOTIFICATION); +export const deleteNotification = createThunk(DELETE_NOTIFICATION); +export const testNotification = createThunk(TEST_NOTIFICATION); +export const cancelTestNotification = createThunk(CANCEL_TEST_NOTIFICATION); + +export const setNotificationValue = createAction(SET_NOTIFICATION_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +export const setNotificationFieldValue = createAction(SET_NOTIFICATION_FIELD_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: [], + selectedSchema: {}, + isSaving: false, + saveError: null, + isTesting: false, + items: [], + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_NOTIFICATIONS]: createFetchHandler(section, '/notification'), + [FETCH_NOTIFICATION_SCHEMA]: createFetchSchemaHandler(section, '/notification/schema'), + + [SAVE_NOTIFICATION]: createSaveProviderHandler(section, '/notification'), + [CANCEL_SAVE_NOTIFICATION]: createCancelSaveProviderHandler(section), + [DELETE_NOTIFICATION]: createRemoveItemHandler(section, '/notification'), + [TEST_NOTIFICATION]: createTestProviderHandler(section, '/notification'), + [CANCEL_TEST_NOTIFICATION]: createCancelTestProviderHandler(section) + }, + + // + // Reducers + + reducers: { + [SET_NOTIFICATION_VALUE]: createSetSettingValueReducer(section), + [SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section), + + [SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => { + return selectProviderSchema(state, section, payload, (selectedSchema) => { + selectedSchema.onGrab = selectedSchema.supportsOnGrab; + selectedSchema.onDownload = selectedSchema.supportsOnDownload; + selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade; + selectedSchema.onRename = selectedSchema.supportsOnRename; + + return selectedSchema; + }); + } + } + +}; diff --git a/frontend/src/Store/Actions/Settings/qualityDefinitions.js b/frontend/src/Store/Actions/Settings/qualityDefinitions.js new file mode 100644 index 000000000..8a900e1d1 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/qualityDefinitions.js @@ -0,0 +1,116 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { createAction } from 'redux-actions'; +import { batchActions } from 'redux-batched-actions'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import { createThunk } from 'Store/thunks'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveHandler from 'Store/Actions/Creators/createSaveHandler'; +import { clearPendingChanges, update } from 'Store/Actions/baseActions'; + +// +// Variables + +const section = 'settings.qualityDefinitions'; + +// +// Actions Types + +export const FETCH_QUALITY_DEFINITIONS = 'settings/qualityDefinitions/fetchQualityDefinitions'; +export const SAVE_QUALITY_DEFINITIONS = 'settings/qualityDefinitions/saveQualityDefinitions'; +export const SET_QUALITY_DEFINITION_VALUE = 'settings/qualityDefinitions/setQualityDefinitionValue'; + +// +// Action Creators + +export const fetchQualityDefinitions = createThunk(FETCH_QUALITY_DEFINITIONS); +export const saveQualityDefinitions = createThunk(SAVE_QUALITY_DEFINITIONS); + +export const setQualityDefinitionValue = createAction(SET_QUALITY_DEFINITION_VALUE); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isSaving: false, + saveError: null, + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_QUALITY_DEFINITIONS]: createFetchHandler(section, '/qualitydefinition'), + [SAVE_QUALITY_DEFINITIONS]: createSaveHandler(section, '/qualitydefinition'), + + [SAVE_QUALITY_DEFINITIONS]: function(getState, payload, dispatch) { + const qualityDefinitions = getState().settings.qualityDefinitions; + + const upatedDefinitions = Object.keys(qualityDefinitions.pendingChanges).map((key) => { + const id = parseInt(key); + const pendingChanges = qualityDefinitions.pendingChanges[id] || {}; + const item = _.find(qualityDefinitions.items, { id }); + + return Object.assign({}, item, pendingChanges); + }); + + // If there is nothing to save don't bother isSaving + if (!upatedDefinitions || !upatedDefinitions.length) { + return; + } + + const promise = $.ajax({ + method: 'PUT', + url: '/qualityDefinition/update', + data: JSON.stringify(upatedDefinitions) + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + clearPendingChanges({ section }) + ])); + }); + } + }, + + // + // Reducers + + reducers: { + [SET_QUALITY_DEFINITION_VALUE]: function(state, { payload }) { + const { id, name, value } = payload; + const newState = getSectionState(state, section); + newState.pendingChanges = _.cloneDeep(newState.pendingChanges); + + const pendingState = newState.pendingChanges[id] || {}; + const currentValue = _.find(newState.items, { id })[name]; + + if (currentValue === value) { + delete pendingState[name]; + } else { + pendingState[name] = value; + } + + if (_.isEmpty(pendingState)) { + delete newState.pendingChanges[id]; + } else { + newState.pendingChanges[id] = pendingState; + } + + return updateSectionState(state, section, newState); + } + } + +}; diff --git a/frontend/src/Store/Actions/Settings/qualityProfiles.js b/frontend/src/Store/Actions/Settings/qualityProfiles.js new file mode 100644 index 000000000..d12833623 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/qualityProfiles.js @@ -0,0 +1,79 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; +import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; +import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; + +// +// Variables + +const section = 'settings.qualityProfiles'; + +// +// Actions Types + +export const FETCH_QUALITY_PROFILES = 'settings/qualityProfiles/fetchQualityProfiles'; +export const FETCH_QUALITY_PROFILE_SCHEMA = 'settings/qualityProfiles/fetchQualityProfileSchema'; +export const SAVE_QUALITY_PROFILE = 'settings/qualityProfiles/saveQualityProfile'; +export const DELETE_QUALITY_PROFILE = 'settings/qualityProfiles/deleteQualityProfile'; +export const SET_QUALITY_PROFILE_VALUE = 'settings/qualityProfiles/setQualityProfileValue'; + +// +// Action Creators + +export const fetchQualityProfiles = createThunk(FETCH_QUALITY_PROFILES); +export const fetchQualityProfileSchema = createThunk(FETCH_QUALITY_PROFILE_SCHEMA); +export const saveQualityProfile = createThunk(SAVE_QUALITY_PROFILE); +export const deleteQualityProfile = createThunk(DELETE_QUALITY_PROFILE); + +export const setQualityProfileValue = createAction(SET_QUALITY_PROFILE_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: {}, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_QUALITY_PROFILES]: createFetchHandler(section, '/qualityprofile'), + [FETCH_QUALITY_PROFILE_SCHEMA]: createFetchSchemaHandler(section, '/qualityprofile/schema'), + [SAVE_QUALITY_PROFILE]: createSaveProviderHandler(section, '/qualityprofile'), + [DELETE_QUALITY_PROFILE]: createRemoveItemHandler(section, '/qualityprofile') + }, + + // + // Reducers + + reducers: { + [SET_QUALITY_PROFILE_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/remotePathMappings.js b/frontend/src/Store/Actions/Settings/remotePathMappings.js new file mode 100644 index 000000000..3cfcc7f1f --- /dev/null +++ b/frontend/src/Store/Actions/Settings/remotePathMappings.js @@ -0,0 +1,69 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; +import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; + +// +// Variables + +const section = 'settings.remotePathMappings'; + +// +// Actions Types + +export const FETCH_REMOTE_PATH_MAPPINGS = 'settings/remotePathMappings/fetchRemotePathMappings'; +export const SAVE_REMOTE_PATH_MAPPING = 'settings/remotePathMappings/saveRemotePathMapping'; +export const DELETE_REMOTE_PATH_MAPPING = 'settings/remotePathMappings/deleteRemotePathMapping'; +export const SET_REMOTE_PATH_MAPPING_VALUE = 'settings/remotePathMappings/setRemotePathMappingValue'; + +// +// Action Creators + +export const fetchRemotePathMappings = createThunk(FETCH_REMOTE_PATH_MAPPINGS); +export const saveRemotePathMapping = createThunk(SAVE_REMOTE_PATH_MAPPING); +export const deleteRemotePathMapping = createThunk(DELETE_REMOTE_PATH_MAPPING); + +export const setRemotePathMappingValue = createAction(SET_REMOTE_PATH_MAPPING_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isSaving: false, + saveError: null, + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_REMOTE_PATH_MAPPINGS]: createFetchHandler(section, '/remotepathmapping'), + [SAVE_REMOTE_PATH_MAPPING]: createSaveProviderHandler(section, '/remotepathmapping'), + [DELETE_REMOTE_PATH_MAPPING]: createRemoveItemHandler(section, '/remotepathmapping') + }, + + // + // Reducers + + reducers: { + [SET_REMOTE_PATH_MAPPING_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/restrictions.js b/frontend/src/Store/Actions/Settings/restrictions.js new file mode 100644 index 000000000..190b5124e --- /dev/null +++ b/frontend/src/Store/Actions/Settings/restrictions.js @@ -0,0 +1,71 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; +import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; + +// +// Variables + +const section = 'settings.restrictions'; + +// +// Actions Types + +export const FETCH_RESTRICTIONS = 'settings/restrictions/fetchRestrictions'; +export const SAVE_RESTRICTION = 'settings/restrictions/saveRestriction'; +export const DELETE_RESTRICTION = 'settings/restrictions/deleteRestriction'; +export const SET_RESTRICTION_VALUE = 'settings/restrictions/setRestrictionValue'; + +// +// Action Creators + +export const fetchRestrictions = createThunk(FETCH_RESTRICTIONS); +export const saveRestriction = createThunk(SAVE_RESTRICTION); +export const deleteRestriction = createThunk(DELETE_RESTRICTION); + +export const setRestrictionValue = createAction(SET_RESTRICTION_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_RESTRICTIONS]: createFetchHandler(section, '/restriction'), + + [SAVE_RESTRICTION]: createSaveProviderHandler(section, '/restriction'), + + [DELETE_RESTRICTION]: createRemoveItemHandler(section, '/restriction') + }, + + // + // Reducers + + reducers: { + [SET_RESTRICTION_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/Settings/ui.js b/frontend/src/Store/Actions/Settings/ui.js new file mode 100644 index 000000000..97d7223fd --- /dev/null +++ b/frontend/src/Store/Actions/Settings/ui.js @@ -0,0 +1,64 @@ +import { createAction } from 'redux-actions'; +import { createThunk } from 'Store/thunks'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import createSaveHandler from 'Store/Actions/Creators/createSaveHandler'; + +// +// Variables + +const section = 'settings.ui'; + +// +// Actions Types + +export const FETCH_UI_SETTINGS = 'settings/ui/fetchUiSettings'; +export const SET_UI_SETTINGS_VALUE = 'SET_UI_SETTINGS_VALUE'; +export const SAVE_UI_SETTINGS = 'SAVE_UI_SETTINGS'; + +// +// Action Creators + +export const fetchUISettings = createThunk(FETCH_UI_SETTINGS); +export const saveUISettings = createThunk(SAVE_UI_SETTINGS); +export const setUISettingsValue = createAction(SET_UI_SETTINGS_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_UI_SETTINGS]: createFetchHandler(section, '/config/ui'), + [SAVE_UI_SETTINGS]: createSaveHandler(section, '/config/ui') + }, + + // + // Reducers + + reducers: { + [SET_UI_SETTINGS_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/actionTypes.js b/frontend/src/Store/Actions/actionTypes.js index 65c04674d..a0c784a7d 100644 --- a/frontend/src/Store/Actions/actionTypes.js +++ b/frontend/src/Store/Actions/actionTypes.js @@ -1,18 +1,3 @@ -// -// BASE - -export const SET = 'SET'; - -export const UPDATE = 'UPDATE'; -export const UPDATE_ITEM = 'UPDATE_ITEM'; -export const UPDATE_SERVER_SIDE_COLLECTION = 'UPDATE_SERVER_SIDE_COLLECTION'; - -export const SET_SETTING_VALUE = 'SET_SETTING_VALUE'; -export const CLEAR_PENDING_CHANGES = 'CLEAR_PENDING_CHANGES'; -export const SAVE_SETTINGS = 'SAVE_SETTINGS'; - -export const REMOVE_ITEM = 'REMOVE_ITEM'; - // // App @@ -23,401 +8,14 @@ export const SET_VERSION = 'SET_VERSION'; export const SET_APP_VALUE = 'SET_APP_VALUE'; export const SET_IS_SIDEBAR_VISIBLE = 'SET_IS_SIDEBAR_VISIBLE'; -// -// Add Artist - -export const LOOKUP_ARTIST = 'LOOKUP_ARTIST'; -export const ADD_ARTIST = 'ADD_ARTIST'; -export const SET_ADD_ARTIST_VALUE = 'SET_ADD_ARTIST_VALUE'; -export const CLEAR_ADD_ARTIST = 'CLEAR_ADD_ARTIST'; -export const SET_ADD_ARTIST_DEFAULT = 'SET_ADD_ARTIST_DEFAULT'; - -// -// Import Artist - -export const QUEUE_LOOKUP_ARTIST = 'QUEUE_LOOKUP_ARTIST'; -export const START_LOOKUP_ARTIST = 'START_LOOKUP_ARTIST'; -export const CLEAR_IMPORT_ARTIST = 'CLEAR_IMPORT_ARTIST'; -export const SET_IMPORT_ARTIST_VALUE = 'SET_IMPORT_ARTIST_VALUE'; -export const IMPORT_ARTIST = 'IMPORT_ARTIST'; - -// -// Artist - -export const FETCH_ARTIST = 'FETCH_ARTIST'; -export const SET_ARTIST_VALUE = 'SET_ARTIST_VALUE'; -export const SAVE_ARTIST = 'SAVE_ARTIST'; -export const DELETE_ARTIST = 'DELETE_ARTIST'; - -export const SET_ARTIST_SORT = 'SET_ARTIST_SORT'; -export const SET_ARTIST_FILTER = 'SET_ARTIST_FILTER'; -export const SET_ARTIST_VIEW = 'SET_ARTIST_VIEW'; -export const SET_ARTIST_TABLE_OPTION = 'SET_ARTIST_TABLE_OPTION'; -export const SET_ARTIST_POSTER_OPTION = 'SET_ARTIST_POSTER_OPTION'; -export const SET_ARTIST_BANNER_OPTION = 'SET_ARTIST_BANNER_OPTION'; -export const SET_ARTIST_OVERVIEW_OPTION = 'SET_ARTIST_OVERVIEW_OPTION'; - -export const TOGGLE_ARTIST_MONITORED = 'TOGGLE_ARTIST_MONITORED'; -export const TOGGLE_ALBUM_MONITORED = 'TOGGLE_ALBUM_MONITORED'; - -// -// Artist Editor - -export const SET_ARTIST_EDITOR_SORT = 'SET_ARTIST_EDITOR_SORT'; -export const SET_ARTIST_EDITOR_FILTER = 'SET_ARTIST_EDITOR_FILTER'; -export const SAVE_ARTIST_EDITOR = 'SAVE_ARTIST_EDITOR'; -export const BULK_DELETE_ARTIST = 'BULK_DELETE_ARTIST'; - -// -// Season Pass - -export const SET_ALBUM_STUDIO_SORT = 'SET_ALBUM_STUDIO_SORT'; -export const SET_ALBUM_STUDIO_FILTER = 'SET_ALBUM_STUDIO_FILTER'; -export const SAVE_ALBUM_STUDIO = 'SAVE_ALBUM_STUDIO'; - -// -// Episodes - -export const FETCH_EPISODES = 'FETCH_EPISODES'; -export const SET_EPISODES_SORT = 'SET_EPISODES_SORT'; -export const SET_EPISODES_TABLE_OPTION = 'SET_EPISODES_TABLE_OPTION'; -export const CLEAR_EPISODES = 'CLEAR_EPISODES'; -export const TOGGLE_EPISODE_MONITORED = 'TOGGLE_EPISODE_MONITORED'; -export const TOGGLE_EPISODES_MONITORED = 'TOGGLE_EPISODES_MONITORED'; - -// -// Tracks - -export const FETCH_TRACKS = 'FETCH_TRACKS'; -export const SET_TRACKS_SORT = 'SET_TRACKS_SORT'; -export const SET_TRACKS_TABLE_OPTION = 'SET_TRACKS_TABLE_OPTION'; -export const CLEAR_TRACKS = 'CLEAR_TRACKS'; - -// -// Episode Files - -export const FETCH_TRACK_FILES = 'FETCH_TRACK_FILES'; -export const CLEAR_TRACK_FILES = 'CLEAR_TRACK_FILES'; -export const DELETE_TRACK_FILE = 'DELETE_TRACK_FILE'; -export const DELETE_TRACK_FILES = 'DELETE_TRACK_FILES'; -export const UPDATE_TRACK_FILES = 'UPDATE_TRACK_FILES'; - -// -// Album History - -export const FETCH_ALBUM_HISTORY = 'FETCH_ALBUM_HISTORY'; -export const CLEAR_ALBUM_HISTORY = 'CLEAR_ALBUM_HISTORY'; -export const ALBUM_HISTORY_MARK_AS_FAILED = 'ALBUM_HISTORY_MARK_AS_FAILED'; - -// -// Releases - -export const FETCH_RELEASES = 'FETCH_RELEASES'; -export const CANCEL_FETCH_RELEASES = 'CANCEL_FETCH_RELEASES'; -export const SET_RELEASES_SORT = 'SET_RELEASES_SORT'; -export const CLEAR_RELEASES = 'CLEAR_RELEASES'; -export const GRAB_RELEASE = 'GRAB_RELEASE'; -export const UPDATE_RELEASE = 'UPDATE_RELEASE'; - -// -// Calendar - -export const FETCH_CALENDAR = 'FETCH_CALENDAR'; -export const SET_CALENDAR_DAYS_COUNT = 'SET_CALENDAR_DAYS_COUNT'; -export const SET_CALENDAR_INCLUDE_UNMONITORED = 'SET_CALENDAR_INCLUDE_UNMONITORED'; -export const SET_CALENDAR_VIEW = 'SET_CALENDAR_VIEW'; -export const GOTO_CALENDAR_TODAY = 'GOTO_CALENDAR_TODAY'; -export const GOTO_CALENDAR_PREVIOUS_RANGE = 'GOTO_CALENDAR_PREVIOUS_RANGE'; -export const GOTO_CALENDAR_NEXT_RANGE = 'GOTO_CALENDAR_NEXT_RANGE'; -export const CLEAR_CALENDAR = 'CLEAR_CALENDAR'; - -// -// History - -export const FETCH_HISTORY = 'FETCH_HISTORY'; -export const GOTO_FIRST_HISTORY_PAGE = 'GOTO_FIRST_HISTORY_PAGE'; -export const GOTO_PREVIOUS_HISTORY_PAGE = 'GOTO_PREVIOUS_HISTORY_PAGE'; -export const GOTO_NEXT_HISTORY_PAGE = 'GOTO_NEXT_HISTORY_PAGE'; -export const GOTO_LAST_HISTORY_PAGE = 'GOTO_LAST_HISTORY_PAGE'; -export const GOTO_HISTORY_PAGE = 'GOTO_HISTORY_PAGE'; -export const SET_HISTORY_SORT = 'SET_HISTORY_SORT'; -export const SET_HISTORY_FILTER = 'SET_HISTORY_FILTER'; -export const SET_HISTORY_TABLE_OPTION = 'SET_HISTORY_TABLE_OPTION'; -export const CLEAR_HISTORY = 'CLEAR_HISTORY'; - -export const MARK_AS_FAILED = 'MARK_AS_FAILED'; - -// -// Queue - -export const FETCH_QUEUE_STATUS = 'FETCH_QUEUE_STATUS'; - -export const FETCH_QUEUE_DETAILS = 'FETCH_QUEUE_DETAILS'; -export const CLEAR_QUEUE_DETAILS = 'CLEAR_QUEUE_DETAILS'; - -export const FETCH_QUEUE = 'FETCH_QUEUE'; -export const GOTO_FIRST_QUEUE_PAGE = 'GOTO_FIRST_QUEUE_PAGE'; -export const GOTO_PREVIOUS_QUEUE_PAGE = 'GOTO_PREVIOUS_QUEUE_PAGE'; -export const GOTO_NEXT_QUEUE_PAGE = 'GOTO_NEXT_QUEUE_PAGE'; -export const GOTO_LAST_QUEUE_PAGE = 'GOTO_LAST_QUEUE_PAGE'; -export const GOTO_QUEUE_PAGE = 'GOTO_QUEUE_PAGE'; -export const SET_QUEUE_SORT = 'SET_QUEUE_SORT'; -export const SET_QUEUE_TABLE_OPTION = 'SET_QUEUE_TABLE_OPTION'; -export const CLEAR_QUEUE = 'CLEAR_QUEUE'; - -export const GRAB_QUEUE_ITEM = 'GRAB_QUEUE_ITEM'; -export const GRAB_QUEUE_ITEMS = 'GRAB_QUEUE_ITEMS'; -export const REMOVE_QUEUE_ITEM = 'REMOVE_QUEUE_ITEM'; -export const REMOVE_QUEUE_ITEMS = 'REMOVE_QUEUE_ITEMS'; - -// -// Blacklist - -export const FETCH_BLACKLIST = 'FETCH_BLACKLIST'; -export const GOTO_FIRST_BLACKLIST_PAGE = 'GOTO_FIRST_BLACKLIST_PAGE'; -export const GOTO_PREVIOUS_BLACKLIST_PAGE = 'GOTO_PREVIOUS_BLACKLIST_PAGE'; -export const GOTO_NEXT_BLACKLIST_PAGE = 'GOTO_NEXT_BLACKLIST_PAGE'; -export const GOTO_LAST_BLACKLIST_PAGE = 'GOTO_LAST_BLACKLIST_PAGE'; -export const GOTO_BLACKLIST_PAGE = 'GOTO_BLACKLIST_PAGE'; -export const SET_BLACKLIST_SORT = 'SET_BLACKLIST_SORT'; -export const SET_BLACKLIST_TABLE_OPTION = 'SET_BLACKLIST_TABLE_OPTION'; - -// -// Wanted - -export const FETCH_MISSING = 'FETCH_MISSING'; -export const GOTO_FIRST_MISSING_PAGE = 'GOTO_FIRST_MISSING_PAGE'; -export const GOTO_PREVIOUS_MISSING_PAGE = 'GOTO_PREVIOUS_MISSING_PAGE'; -export const GOTO_NEXT_MISSING_PAGE = 'GOTO_NEXT_MISSING_PAGE'; -export const GOTO_LAST_MISSING_PAGE = 'GOTO_LAST_MISSING_PAGE'; -export const GOTO_MISSING_PAGE = 'GOTO_MISSING_PAGE'; -export const SET_MISSING_SORT = 'SET_MISSING_SORT'; -export const SET_MISSING_FILTER = 'SET_MISSING_FILTER'; -export const SET_MISSING_TABLE_OPTION = 'SET_MISSING_TABLE_OPTION'; -export const CLEAR_MISSING = 'CLEAR_MISSING'; - -export const BATCH_TOGGLE_MISSING_ALBUMS = 'BATCH_TOGGLE_MISSING_ALBUMS'; - -export const FETCH_CUTOFF_UNMET = 'FETCH_CUTOFF_UNMET'; -export const GOTO_FIRST_CUTOFF_UNMET_PAGE = 'GOTO_FIRST_CUTOFF_UNMET_PAGE'; -export const GOTO_PREVIOUS_CUTOFF_UNMET_PAGE = 'GOTO_PREVIOUS_CUTOFF_UNMET_PAGE'; -export const GOTO_NEXT_CUTOFF_UNMET_PAGE = 'GOTO_NEXT_CUTOFF_UNMET_PAGE'; -export const GOTO_LAST_CUTOFF_UNMET_PAGE = 'GOTO_LAST_CUTOFF_UNMET_PAGE'; -export const GOTO_CUTOFF_UNMET_PAGE = 'GOTO_CUTOFF_UNMET_PAGE'; -export const SET_CUTOFF_UNMET_SORT = 'SET_CUTOFF_UNMET_SORT'; -export const SET_CUTOFF_UNMET_FILTER = 'SET_CUTOFF_UNMET_FILTER'; -export const SET_CUTOFF_UNMET_TABLE_OPTION = 'SET_CUTOFF_UNMET_TABLE_OPTION'; -export const CLEAR_CUTOFF_UNMET = 'CLEAR_CUTOFF_UNMET'; - -export const BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS = 'BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS'; - // // Settings -export const TOGGLE_ADVANCED_SETTINGS = 'TOGGLE_ADVANCED_SETTINGS'; - -export const FETCH_UI_SETTINGS = 'FETCH_UI_SETTINGS'; -export const SET_UI_SETTINGS_VALUE = 'SET_UI_SETTINGS_VALUE'; -export const SAVE_UI_SETTINGS = 'SAVE_UI_SETTINGS'; - -export const FETCH_MEDIA_MANAGEMENT_SETTINGS = 'FETCH_MEDIA_MANAGEMENT_SETTINGS'; -export const SET_MEDIA_MANAGEMENT_SETTINGS_VALUE = 'SET_MEDIA_MANAGEMENT_SETTINGS_VALUE'; -export const SAVE_MEDIA_MANAGEMENT_SETTINGS = 'SAVE_MEDIA_MANAGEMENT_SETTINGS'; - -export const FETCH_NAMING_SETTINGS = 'FETCH_NAMING_SETTINGS'; -export const SET_NAMING_SETTINGS_VALUE = 'SET_NAMING_SETTINGS_VALUE'; -export const SAVE_NAMING_SETTINGS = 'SAVE_NAMING_SETTINGS'; -export const FETCH_NAMING_EXAMPLES = 'FETCH_NAMING_EXAMPLES'; - -export const FETCH_QUALITY_PROFILES = 'FETCH_QUALITY_PROFILES'; -export const FETCH_QUALITY_PROFILE_SCHEMA = 'FETCH_QUALITY_PROFILE_SCHEMA'; -export const SET_QUALITY_PROFILE_VALUE = 'SET_QUALITY_PROFILE_VALUE'; -export const SAVE_QUALITY_PROFILE = 'SAVE_QUALITY_PROFILE'; -export const DELETE_QUALITY_PROFILE = 'DELETE_QUALITY_PROFILE'; - -export const FETCH_LANGUAGE_PROFILES = 'FETCH_LANGUAGE_PROFILES'; -export const FETCH_LANGUAGE_PROFILE_SCHEMA = 'FETCH_LANGUAGE_PROFILE_SCHEMA'; -export const SET_LANGUAGE_PROFILE_VALUE = 'SET_LANGUAGE_PROFILE_VALUE'; -export const SAVE_LANGUAGE_PROFILE = 'SAVE_LANGUAGE_PROFILE'; -export const DELETE_LANGUAGE_PROFILE = 'DELETE_LANGUAGE_PROFILE'; - -export const FETCH_METADATA_PROFILES = 'FETCH_METADATA_PROFILES'; -export const FETCH_METADATA_PROFILE_SCHEMA = 'FETCH_METADATA_PROFILE_SCHEMA'; -export const SET_METADATA_PROFILE_VALUE = 'SET_METADATA_PROFILE_VALUE'; -export const SAVE_METADATA_PROFILE = 'SAVE_METADATA_PROFILE'; -export const DELETE_METADATA_PROFILE = 'DELETE_METADATA_PROFILE'; - -export const FETCH_DELAY_PROFILES = 'FETCH_DELAY_PROFILES'; -export const SET_DELAY_PROFILE_VALUE = 'SET_DELAY_PROFILE_VALUE'; -export const SAVE_DELAY_PROFILE = 'SAVE_DELAY_PROFILE'; -export const DELETE_DELAY_PROFILE = 'DELETE_DELAY_PROFILE'; -export const REORDER_DELAY_PROFILE = 'REORDER_DELAY_PROFILE'; - -export const FETCH_QUALITY_DEFINITIONS = 'FETCH_QUALITY_DEFINITIONS'; -export const SET_QUALITY_DEFINITION_VALUE = 'SET_QUALITY_DEFINITION_VALUE'; -export const SAVE_QUALITY_DEFINITIONS = 'SAVE_QUALITY_DEFINITIONS'; - -export const FETCH_INDEXERS = 'FETCH_INDEXERS'; -export const FETCH_INDEXER_SCHEMA = 'FETCH_INDEXER_SCHEMA'; -export const SELECT_INDEXER_SCHEMA = 'SELECT_INDEXER_SCHEMA'; -export const SET_INDEXER_VALUE = 'SET_INDEXER_VALUE'; -export const SET_INDEXER_FIELD_VALUE = 'SET_INDEXER_FIELD_VALUE'; -export const SAVE_INDEXER = 'SAVE_INDEXER'; -export const CANCEL_SAVE_INDEXER = 'CANCEL_SAVE_INDEXER'; -export const DELETE_INDEXER = 'DELETE_INDEXER'; -export const TEST_INDEXER = 'TEST_INDEXER'; -export const CANCEL_TEST_INDEXER = 'CANCEL_TEST_INDEXER'; - -export const FETCH_INDEXER_OPTIONS = 'FETCH_INDEXER_OPTIONS'; -export const SET_INDEXER_OPTIONS_VALUE = 'SET_INDEXER_OPTIONS_VALUE'; -export const SAVE_INDEXER_OPTIONS = 'SAVE_INDEXER_OPTIONS'; - -export const FETCH_RESTRICTIONS = 'FETCH_RESTRICTIONS'; -export const SET_RESTRICTION_VALUE = 'SET_RESTRICTION_VALUE'; -export const SAVE_RESTRICTION = 'SAVE_RESTRICTION'; -export const DELETE_RESTRICTION = 'DELETE_RESTRICTION'; - -export const FETCH_DOWNLOAD_CLIENTS = 'FETCH_DOWNLOAD_CLIENTS'; -export const FETCH_DOWNLOAD_CLIENT_SCHEMA = 'FETCH_DOWNLOAD_CLIENT_SCHEMA'; -export const SELECT_DOWNLOAD_CLIENT_SCHEMA = 'SELECT_DOWNLOAD_CLIENT_SCHEMA'; -export const SET_DOWNLOAD_CLIENT_VALUE = 'SET_DOWNLOAD_CLIENT_VALUE'; -export const SET_DOWNLOAD_CLIENT_FIELD_VALUE = 'SET_DOWNLOAD_CLIENT_FIELD_VALUE'; -export const SAVE_DOWNLOAD_CLIENT = 'SAVE_DOWNLOAD_CLIENT'; -export const CANCEL_SAVE_DOWNLOAD_CLIENT = 'CANCEL_SAVE_DOWNLOAD_CLIENT'; -export const DELETE_DOWNLOAD_CLIENT = 'DELETE_DOWNLOAD_CLIENT'; -export const TEST_DOWNLOAD_CLIENT = 'TEST_DOWNLOAD_CLIENT'; -export const CANCEL_TEST_DOWNLOAD_CLIENT = 'CANCEL_TEST_DOWNLOAD_CLIENT'; - -export const FETCH_DOWNLOAD_CLIENT_OPTIONS = 'FETCH_DOWNLOAD_CLIENT_OPTIONS'; -export const SET_DOWNLOAD_CLIENT_OPTIONS_VALUE = 'SET_DOWNLOAD_CLIENT_OPTIONS_VALUE'; -export const SAVE_DOWNLOAD_CLIENT_OPTIONS = 'SAVE_DOWNLOAD_CLIENT_OPTIONS'; - -export const FETCH_REMOTE_PATH_MAPPINGS = 'FETCH_REMOTE_PATH_MAPPINGS'; -export const SET_REMOTE_PATH_MAPPING_VALUE = 'SET_REMOTE_PATH_MAPPING_VALUE'; -export const SAVE_REMOTE_PATH_MAPPING = 'SAVE_REMOTE_PATH_MAPPING'; -export const DELETE_REMOTE_PATH_MAPPING = 'DELETE_REMOTE_PATH_MAPPING'; - -export const FETCH_NOTIFICATIONS = 'FETCH_NOTIFICATIONS'; -export const FETCH_NOTIFICATION_SCHEMA = 'FETCH_NOTIFICATION_SCHEMA'; -export const SELECT_NOTIFICATION_SCHEMA = 'SELECT_NOTIFICATION_SCHEMA'; -export const SET_NOTIFICATION_VALUE = 'SET_NOTIFICATION_VALUE'; -export const SET_NOTIFICATION_FIELD_VALUE = 'SET_NOTIFICATION_FIELD_VALUE'; -export const SAVE_NOTIFICATION = 'SAVE_NOTIFICATION'; -export const CANCEL_SAVE_NOTIFICATION = 'CANCEL_SAVE_NOTIFICATION'; -export const DELETE_NOTIFICATION = 'DELETE_NOTIFICATION'; -export const CANCEL_TEST_NOTIFICATION = 'CANCEL_TEST_NOTIFICATION'; - -export const FETCH_METADATA = 'FETCH_METADATA'; -export const SET_METADATA_VALUE = 'SET_METADATA_VALUE'; -export const SET_METADATA_FIELD_VALUE = 'SET_METADATA_FIELD_VALUE'; -export const SAVE_METADATA = 'SAVE_METADATA'; - -export const FETCH_METADATA_PROVIDER = 'FETCH_METADATA_PROVIDER'; -export const SET_METADATA_PROVIDER_VALUE = 'SET_METADATA_PROVIDER_VALUE'; -export const SAVE_METADATA_PROVIDER = 'SAVE_METADATA_PROVIDER'; - -// -// System - -export const FETCH_STATUS = 'FETCH_STATUS'; -export const FETCH_HEALTH = 'FETCH_HEALTH'; -export const FETCH_DISK_SPACE = 'FETCH_DISK_SPACE'; - -export const FETCH_TASK = 'FETCH_TASK'; -export const FETCH_TASKS = 'FETCH_TASKS'; -export const FETCH_BACKUPS = 'FETCH_BACKUPS'; -export const FETCH_UPDATES = 'FETCH_UPDATES'; - -export const FETCH_LOGS = 'FETCH_LOGS'; -export const GOTO_FIRST_LOGS_PAGE = 'GOTO_FIRST_LOGS_PAGE'; -export const GOTO_PREVIOUS_LOGS_PAGE = 'GOTO_PREVIOUS_LOGS_PAGE'; -export const GOTO_NEXT_LOGS_PAGE = 'GOTO_NEXT_LOGS_PAGE'; -export const GOTO_LAST_LOGS_PAGE = 'GOTO_LAST_LOGS_PAGE'; -export const GOTO_LOGS_PAGE = 'GOTO_LOGS_PAGE'; -export const SET_LOGS_SORT = 'SET_LOGS_SORT'; -export const SET_LOGS_FILTER = 'SET_LOGS_FILTER'; -export const SET_LOGS_TABLE_OPTION = 'SET_LOGS_TABLE_OPTION'; - -export const FETCH_LOG_FILES = 'FETCH_LOG_FILES'; -export const FETCH_UPDATE_LOG_FILES = 'FETCH_UPDATE_LOG_FILES'; - -export const FETCH_GENERAL_SETTINGS = 'FETCH_GENERAL_SETTINGS'; -export const SET_GENERAL_SETTINGS_VALUE = 'SET_GENERAL_SETTINGS_VALUE'; -export const SAVE_GENERAL_SETTINGS = 'SAVE_GENERAL_SETTINGS'; - -export const RESTART = 'RESTART'; -export const SHUTDOWN = 'SHUTDOWN'; - -// -// Commands - -export const FETCH_COMMANDS = 'FETCH_COMMANDS'; -export const EXECUTE_COMMAND = 'EXECUTE_COMMAND'; -export const ADD_COMMAND = 'ADD_COMMAND'; -export const UPDATE_COMMAND = 'UPDATE_COMMAND'; -export const FINISH_COMMAND = 'FINISH_COMMAND'; -export const REMOVE_COMMAND = 'REMOVE_COMMAND'; -export const REGISTER_FINISH_COMMAND_HANDLER = 'REGISTER_FINISH_COMMAND_HANDLER'; -export const UNREGISTER_FINISH_COMMAND_HANDLER = 'UNREGISTER_FINISH_COMMAND_HANDLER'; - -// -// Paths - -export const FETCH_PATHS = 'FETCH_PATHS'; -export const UPDATE_PATHS = 'UPDATE_PATHS'; -export const CLEAR_PATHS = 'CLEAR_PATHS'; +export const FETCH_GENERAL_SETTINGS = 'settings/general/fetchGeneralSettings'; +export const SET_GENERAL_SETTINGS_VALUE = 'settings/general/setGeneralSettingsValue'; +export const SAVE_GENERAL_SETTINGS = 'settings/general/saveGeneralSettings'; // // Languages export const FETCH_LANGUAGES = 'FETCH_LANGUAGES'; - -// -// Tags - -export const FETCH_TAGS = 'FETCH_TAGS'; -export const ADD_TAG = 'ADD_TAG'; - -// -// Captcha - -export const REFRESH_CAPTCHA = 'REFRESH_CAPTCHA'; -export const GET_CAPTCHA_COOKIE = 'GET_CAPTCHA_COOKIE'; -export const SET_CAPTCHA_VALUE = 'SET_CAPTCHA_VALUE'; -export const RESET_CAPTCHA = 'RESET_CAPTCHA'; - -// -// OAuth - -export const START_OAUTH = 'START_OAUTH'; -export const GET_OAUTH_TOKEN = 'GET_OAUTH_TOKEN'; -export const SET_OAUTH_VALUE = 'SET_OAUTH_VALUE'; -export const RESET_OAUTH = 'RESET_OAUTH'; - -// -// Interactive Import - -export const FETCH_INTERACTIVE_IMPORT_ITEMS = 'FETCH_INTERACTIVE_IMPORT_ITEMS'; -export const UPDATE_INTERACTIVE_IMPORT_ITEM = 'UPDATE_INTERACTIVE_IMPORT_ITEM'; -export const SET_INTERACTIVE_IMPORT_SORT = 'SET_INTERACTIVE_IMPORT_SORT'; -export const CLEAR_INTERACTIVE_IMPORT = 'CLEAR_INTERACTIVE_IMPORT'; -export const ADD_RECENT_FOLDER = 'ADD_RECENT_FOLDER'; -export const REMOVE_RECENT_FOLDER = 'REMOVE_RECENT_FOLDER'; -export const SET_INTERACTIVE_IMPORT_MODE = 'SET_INTERACTIVE_IMPORT_MODE'; - -export const FETCH_INTERACTIVE_IMPORT_ALBUMS = 'FETCH_INTERACTIVE_IMPORT_ALBUMS'; -export const SET_INTERACTIVE_IMPORT_ALBUMS_SORT = 'SET_INTERACTIVE_IMPORT_ALBUMS_SORT'; -export const CLEAR_INTERACTIVE_IMPORT_ALBUMS = 'CLEAR_INTERACTIVE_IMPORT_ALBUMS'; - -// -// Root Folders - -export const FETCH_ROOT_FOLDERS = 'FETCH_ROOT_FOLDERS'; -export const ADD_ROOT_FOLDER = 'ADD_ROOT_FOLDER'; -export const DELETE_ROOT_FOLDER = 'DELETE_ROOT_FOLDER'; - -// -// Organize Preview - -export const FETCH_ORGANIZE_PREVIEW = 'FETCH_ORGANIZE_PREVIEW'; -export const CLEAR_ORGANIZE_PREVIEW = 'CLEAR_ORGANIZE_PREVIEW'; diff --git a/frontend/src/Store/Actions/addArtistActionHandlers.js b/frontend/src/Store/Actions/addArtistActionHandlers.js deleted file mode 100644 index 00842ee8f..000000000 --- a/frontend/src/Store/Actions/addArtistActionHandlers.js +++ /dev/null @@ -1,94 +0,0 @@ -import _ from 'lodash'; -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import createAjaxRequest from 'Utilities/createAjaxRequest'; -import getNewArtist from 'Utilities/Artist/getNewArtist'; -import * as types from './actionTypes'; -import { set, update, updateItem } from './baseActions'; - -let abortCurrentRequest = null; -const section = 'addArtist'; - -const addArtistActionHandlers = { - [types.LOOKUP_ARTIST]: function(payload) { - return function(dispatch, getState) { - dispatch(set({ section, isFetching: true })); - - if (abortCurrentRequest) { - abortCurrentRequest(); - } - - const { request, abortRequest } = createAjaxRequest({ - url: '/artist/lookup', - data: { - term: payload.term - } - }); - - abortCurrentRequest = abortRequest; - - request.done((data) => { - dispatch(batchActions([ - update({ section, data }), - - set({ - section, - isFetching: false, - isPopulated: true, - error: null - }) - ])); - }); - - request.fail((xhr) => { - dispatch(set({ - section, - isFetching: false, - isPopulated: false, - error: xhr.aborted ? null : xhr - })); - }); - }; - }, - - [types.ADD_ARTIST]: function(payload) { - return function(dispatch, getState) { - dispatch(set({ section, isAdding: true })); - - const foreignArtistId = payload.foreignArtistId; - const items = getState().addArtist.items; - const newSeries = getNewArtist(_.cloneDeep(_.find(items, { foreignArtistId })), payload); - - const promise = $.ajax({ - url: '/artist', - method: 'POST', - contentType: 'application/json', - data: JSON.stringify(newSeries) - }); - - promise.done((data) => { - dispatch(batchActions([ - updateItem({ section: 'artist', ...data }), - - set({ - section, - isAdding: false, - isAdded: true, - addError: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isAdding: false, - isAdded: false, - addError: xhr - })); - }); - }; - } -}; - -export default addArtistActionHandlers; diff --git a/frontend/src/Store/Actions/addArtistActions.js b/frontend/src/Store/Actions/addArtistActions.js index e0ee67420..500709fc6 100644 --- a/frontend/src/Store/Actions/addArtistActions.js +++ b/frontend/src/Store/Actions/addArtistActions.js @@ -1,15 +1,180 @@ +import _ from 'lodash'; +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import addArtistActionHandlers from './addArtistActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import { createThunk, handleThunks } from 'Store/thunks'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import createAjaxRequest from 'Utilities/createAjaxRequest'; +import getNewArtist from 'Utilities/Artist/getNewArtist'; +import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer'; +import createHandleActions from './Creators/createHandleActions'; +import { set, update, updateItem } from './baseActions'; -export const lookupArtist = addArtistActionHandlers[types.LOOKUP_ARTIST]; -export const addArtist = addArtistActionHandlers[types.ADD_ARTIST]; -export const clearAddArtist = createAction(types.CLEAR_ADD_ARTIST); -export const setAddArtistDefault = createAction(types.SET_ADD_ARTIST_DEFAULT); +// +// Variables -export const setAddArtistValue = createAction(types.SET_ADD_ARTIST_VALUE, (payload) => { +export const section = 'addArtist'; +let abortCurrentRequest = null; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isAdding: false, + isAdded: false, + addError: null, + items: [], + + defaults: { + rootFolderPath: '', + monitor: 'allAlbums', + qualityProfileId: 0, + languageProfileId: 0, + metadataProfileId: 0, + albumFolder: true, + tags: [] + } +}; + +export const persistState = [ + 'addArtist.defaults' +]; + +// +// Actions Types + +export const LOOKUP_ARTIST = 'addArtist/lookupArtist'; +export const ADD_ARTIST = 'addArtist/addArtist'; +export const SET_ADD_ARTIST_VALUE = 'addArtist/setAddArtistValue'; +export const CLEAR_ADD_ARTIST = 'addArtist/clearAddArtist'; +export const SET_ADD_ARTIST_DEFAULT = 'addArtist/setAddArtistDefault'; + +// +// Action Creators + +export const lookupArtist = createThunk(LOOKUP_ARTIST); +export const addArtist = createThunk(ADD_ARTIST); +export const clearAddArtist = createAction(CLEAR_ADD_ARTIST); +export const setAddArtistDefault = createAction(SET_ADD_ARTIST_DEFAULT); + +export const setAddArtistValue = createAction(SET_ADD_ARTIST_VALUE, (payload) => { return { - section: 'addArtist', + section, ...payload }; }); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [LOOKUP_ARTIST]: function(getState, payload, dispatch) { + dispatch(set({ section, isFetching: true })); + + if (abortCurrentRequest) { + abortCurrentRequest(); + } + + const { request, abortRequest } = createAjaxRequest({ + url: '/artist/lookup', + data: { + term: payload.term + } + }); + + abortCurrentRequest = abortRequest; + + request.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + request.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr.aborted ? null : xhr + })); + }); + }, + + [ADD_ARTIST]: function(getState, payload, dispatch) { + dispatch(set({ section, isAdding: true })); + + const foreignArtistId = payload.foreignArtistId; + const items = getState().addArtist.items; + const newArtist = getNewArtist(_.cloneDeep(_.find(items, { foreignArtistId })), payload); + + const promise = $.ajax({ + url: '/artist', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(newArtist) + }); + + promise.done((data) => { + dispatch(batchActions([ + updateItem({ section: 'artist', ...data }), + + set({ + section, + isAdding: false, + isAdded: true, + addError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isAdding: false, + isAdded: false, + addError: xhr + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_ADD_ARTIST_VALUE]: createSetSettingValueReducer(section), + + [SET_ADD_ARTIST_DEFAULT]: function(state, { payload }) { + const newState = getSectionState(state, section); + + newState.defaults = { + ...newState.defaults, + ...payload + }; + + return updateSectionState(state, section, newState); + }, + + [CLEAR_ADD_ARTIST]: function(state) { + const { + defaults, + ...otherDefaultState + } = defaultState; + + return Object.assign({}, state, otherDefaultState); + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/albumHistoryActionHandlers.js b/frontend/src/Store/Actions/albumHistoryActionHandlers.js deleted file mode 100644 index 8d1952fa7..000000000 --- a/frontend/src/Store/Actions/albumHistoryActionHandlers.js +++ /dev/null @@ -1,75 +0,0 @@ -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import { sortDirections } from 'Helpers/Props'; -import * as types from './actionTypes'; -import { set, update } from './baseActions'; -import { fetchAlbumHistory } from './albumHistoryActions'; - -const albumHistoryActionHandlers = { - [types.FETCH_ALBUM_HISTORY]: function(payload) { - const section = 'albumHistory'; - - return function(dispatch, getState) { - dispatch(set({ section, isFetching: true })); - - const queryParams = { - pageSize: 1000, - page: 1, - filterKey: 'albumId', - filterValue: payload.albumId, - sortKey: 'date', - sortDirection: sortDirections.DESCENDING - }; - - const promise = $.ajax({ - url: '/history', - data: queryParams - }); - - promise.done((data) => { - dispatch(batchActions([ - update({ section, data: data.records }), - - set({ - section, - isFetching: false, - isPopulated: true, - error: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isFetching: false, - isPopulated: false, - error: xhr - })); - }); - }; - }, - - [types.ALBUM_HISTORY_MARK_AS_FAILED]: function(payload) { - return function(dispatch, getState) { - const { - historyId, - albumId - } = payload; - - const promise = $.ajax({ - url: '/history/failed', - method: 'POST', - data: { - id: historyId - } - }); - - promise.done(() => { - dispatch(fetchAlbumHistory({ albumId })); - }); - }; - } -}; - -export default albumHistoryActionHandlers; diff --git a/frontend/src/Store/Actions/albumHistoryActions.js b/frontend/src/Store/Actions/albumHistoryActions.js index 3e15c4b1f..fb60bc8ce 100644 --- a/frontend/src/Store/Actions/albumHistoryActions.js +++ b/frontend/src/Store/Actions/albumHistoryActions.js @@ -1,7 +1,113 @@ +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import albumHistoryActionHandlers from './albumHistoryActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import { createThunk, handleThunks } from 'Store/thunks'; +import { sortDirections } from 'Helpers/Props'; +import createHandleActions from './Creators/createHandleActions'; +import { set, update } from './baseActions'; + +// +// Variables + +export const section = 'albumHistory'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [] +}; + +// +// Actions Types + +export const FETCH_ALBUM_HISTORY = 'albumHistory/fetchAlbumHistory'; +export const CLEAR_ALBUM_HISTORY = 'albumHistory/clearAlbumHistory'; +export const ALBUM_HISTORY_MARK_AS_FAILED = 'albumHistory/albumHistoryMarkAsFailed'; + +// +// Action Creators + +export const fetchAlbumHistory = createThunk(FETCH_ALBUM_HISTORY); +export const clearAlbumHistory = createAction(CLEAR_ALBUM_HISTORY); +export const albumHistoryMarkAsFailed = createThunk(ALBUM_HISTORY_MARK_AS_FAILED); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [FETCH_ALBUM_HISTORY]: function(getState, payload, dispatch) { + dispatch(set({ section, isFetching: true })); + + const queryParams = { + pageSize: 1000, + page: 1, + filterKey: 'albumId', + filterValue: payload.albumId, + sortKey: 'date', + sortDirection: sortDirections.DESCENDING + }; + + const promise = $.ajax({ + url: '/history', + data: queryParams + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data: data.records }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }, + + [ALBUM_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) { + const { + historyId, + albumId + } = payload; + + const promise = $.ajax({ + url: '/history/failed', + method: 'POST', + data: { + id: historyId + } + }); + + promise.done(() => { + dispatch(fetchAlbumHistory({ albumId })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [CLEAR_ALBUM_HISTORY]: (state) => { + return Object.assign({}, state, defaultState); + } + +}, defaultState, section); -export const fetchAlbumHistory = albumHistoryActionHandlers[types.FETCH_ALBUM_HISTORY]; -export const clearAlbumHistory = createAction(types.CLEAR_ALBUM_HISTORY); -export const albumHistoryMarkAsFailed = albumHistoryActionHandlers[types.ALBUM_HISTORY_MARK_AS_FAILED]; diff --git a/frontend/src/Store/Actions/albumStudioActionHandlers.js b/frontend/src/Store/Actions/albumStudioActionHandlers.js deleted file mode 100644 index 0b26da551..000000000 --- a/frontend/src/Store/Actions/albumStudioActionHandlers.js +++ /dev/null @@ -1,83 +0,0 @@ -import _ from 'lodash'; -import $ from 'jquery'; -import getMonitoringOptions from 'Utilities/Artist/getMonitoringOptions'; -import * as types from './actionTypes'; -import { set } from './baseActions'; -import { fetchArtist } from './artistActions'; - -const section = 'albumStudio'; - -const albumStudioActionHandlers = { - [types.SAVE_ALBUM_STUDIO]: function(payload) { - return function(dispatch, getState) { - const { - artistIds, - monitored, - monitor - } = payload; - - let monitoringOptions = null; - const artist = []; - const allArtists = getState().artist.items; - - artistIds.forEach((id) => { - const s = _.find(allArtists, { id }); - const artistToUpdate = { id }; - - if (payload.hasOwnProperty('monitored')) { - artistToUpdate.monitored = monitored; - } - - if (monitor) { - const { - albums, - options: artistMonitoringOptions - } = getMonitoringOptions(_.cloneDeep(s.albums), monitor); - - if (!monitoringOptions) { - monitoringOptions = artistMonitoringOptions; - } - - artistToUpdate.albums = albums; - } - - artist.push(artistToUpdate); - }); - - dispatch(set({ - section, - isSaving: true - })); - - const promise = $.ajax({ - url: '/albumStudio', - method: 'POST', - data: JSON.stringify({ - artist, - monitoringOptions - }), - dataType: 'json' - }); - - promise.done((data) => { - dispatch(fetchArtist()); - - dispatch(set({ - section, - isSaving: false, - saveError: null - })); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isSaving: false, - saveError: xhr - })); - }); - }; - } -}; - -export default albumStudioActionHandlers; diff --git a/frontend/src/Store/Actions/albumStudioActions.js b/frontend/src/Store/Actions/albumStudioActions.js index 7d90bbcc8..ed9265ace 100644 --- a/frontend/src/Store/Actions/albumStudioActions.js +++ b/frontend/src/Store/Actions/albumStudioActions.js @@ -1,7 +1,139 @@ +import _ from 'lodash'; +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import albumStudioActionHandlers from './albumStudioActionHandlers'; +import getMonitoringOptions from 'Utilities/Artist/getMonitoringOptions'; +import { filterTypes, sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; +import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; +import createHandleActions from './Creators/createHandleActions'; +import { set } from './baseActions'; +import { fetchArtist } from './artistActions'; + +// +// Variables + +export const section = 'albumStudio'; + +// +// State + +export const defaultState = { + isSaving: false, + saveError: null, + sortKey: 'sortName', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortName', + secondarySortDirection: sortDirections.ASCENDING, + filterKey: null, + filterValue: null, + filterType: filterTypes.EQUAL +}; + +export const persistState = [ + 'albumStudio.sortKey', + 'albumStudio.sortDirection', + 'albumStudio.filterKey', + 'albumStudio.filterValue', + 'albumStudio.filterType' +]; + +// +// Actions Types + +export const SET_ALBUM_STUDIO_SORT = 'albumStudio/setAlbumStudioSort'; +export const SET_ALBUM_STUDIO_FILTER = 'albumStudio/setAlbumStudioFilter'; +export const SAVE_ALBUM_STUDIO = 'albumStudio/saveAlbumStudio'; + +// +// Action Creators + +export const setAlbumStudioSort = createAction(SET_ALBUM_STUDIO_SORT); +export const setAlbumStudioFilter = createAction(SET_ALBUM_STUDIO_FILTER); +export const saveAlbumStudio = createThunk(SAVE_ALBUM_STUDIO); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [SAVE_ALBUM_STUDIO]: function(getState, payload, dispatch) { + const { + artistIds, + monitored, + monitor + } = payload; + + let monitoringOptions = null; + const artist = []; + const allArtists = getState().artist.items; + + artistIds.forEach((id) => { + const s = _.find(allArtists, { id }); + const artistToUpdate = { id }; + + if (payload.hasOwnProperty('monitored')) { + artistToUpdate.monitored = monitored; + } + + if (monitor) { + const { + albums, + options: artistMonitoringOptions + } = getMonitoringOptions(_.cloneDeep(s.albums), monitor); + + if (!monitoringOptions) { + monitoringOptions = artistMonitoringOptions; + } + + artistToUpdate.albums = albums; + } + + artist.push(artistToUpdate); + }); + + dispatch(set({ + section, + isSaving: true + })); + + const promise = $.ajax({ + url: '/albumStudio', + method: 'POST', + data: JSON.stringify({ + artist, + monitoringOptions + }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(fetchArtist()); + + dispatch(set({ + section, + isSaving: false, + saveError: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_ALBUM_STUDIO_SORT]: createSetClientSideCollectionSortReducer(section), + [SET_ALBUM_STUDIO_FILTER]: createSetClientSideCollectionFilterReducer(section) + +}, defaultState, section); -export const setAlbumStudioSort = createAction(types.SET_ALBUM_STUDIO_SORT); -export const setAlbumStudioFilter = createAction(types.SET_ALBUM_STUDIO_FILTER); -export const saveAlbumStudio = albumStudioActionHandlers[types.SAVE_ALBUM_STUDIO]; diff --git a/frontend/src/Store/Actions/appActions.js b/frontend/src/Store/Actions/appActions.js index 3d0191a21..c845fe3da 100644 --- a/frontend/src/Store/Actions/appActions.js +++ b/frontend/src/Store/Actions/appActions.js @@ -1,27 +1,134 @@ +import _ from 'lodash'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import createHandleActions from './Creators/createHandleActions'; -export const saveDimensions = createAction(types.SAVE_DIMENSIONS); -export const setVersion = createAction(types.SET_VERSION); -export const setIsSidebarVisible = createAction(types.SET_IS_SIDEBAR_VISIBLE); - -export const setAppValue = createAction(types.SET_APP_VALUE, (payload) => { - return { - section: 'app', - ...payload +function getDimensions(width, height) { + const dimensions = { + width, + height, + isExtraSmallScreen: width <= 480, + isSmallScreen: width <= 768, + isMediumScreen: width <= 992, + isLargeScreen: width <= 1200 }; -}); -export const showMessage = createAction(types.SHOW_MESSAGE, (payload) => { - return { - section: 'messages', - ...payload - }; -}); + return dimensions; +} + +// +// Variables + +export const section = 'app'; +const messagesSection = 'app.messages'; + +// +// State + +export const defaultState = { + dimensions: getDimensions(window.innerWidth, window.innerHeight), + messages: { + items: [] + }, + version: window.Sonarr.version, + isUpdated: false, + isConnected: true, + isReconnecting: false, + isDisconnected: false, + isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen +}; + +// +// Action Types + +export const SHOW_MESSAGE = 'app/showMessage'; +export const HIDE_MESSAGE = 'app/hideMessage'; +export const SAVE_DIMENSIONS = 'app/saveDimensions'; +export const SET_VERSION = 'app/setVersion'; +export const SET_APP_VALUE = 'app/setAppValue'; +export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible'; + +// +// Action Creators + +export const saveDimensions = createAction(SAVE_DIMENSIONS); +export const setVersion = createAction(SET_VERSION); +export const setIsSidebarVisible = createAction(SET_IS_SIDEBAR_VISIBLE); +export const setAppValue = createAction(SET_APP_VALUE); +export const showMessage = createAction(SHOW_MESSAGE); +export const hideMessage = createAction(HIDE_MESSAGE); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SAVE_DIMENSIONS]: function(state, { payload }) { + const { + width, + height + } = payload; + + const dimensions = getDimensions(width, height); + + return Object.assign({}, state, { dimensions }); + }, + + [SHOW_MESSAGE]: function(state, { payload }) { + const newState = getSectionState(state, messagesSection); + const items = newState.items; + const index = _.findIndex(items, { id: payload.id }); + + newState.items = [...items]; + + if (index >= 0) { + const item = items[index]; + + newState.items.splice(index, 1, { ...item, ...payload }); + } else { + newState.items.push({ ...payload }); + } + + return updateSectionState(state, messagesSection, newState); + }, + + [HIDE_MESSAGE]: function(state, { payload }) { + const newState = getSectionState(state, messagesSection); + + newState.items = [...newState.items]; + _.remove(newState.items, { id: payload.id }); + + return updateSectionState(state, messagesSection, newState); + }, + + [SET_APP_VALUE]: function(state, { payload }) { + const newState = Object.assign(getSectionState(state, section), payload); + + return updateSectionState(state, section, newState); + }, + + [SET_VERSION]: function(state, { payload }) { + const version = payload.version; + + const newState = { + version + }; + + if (state.version !== version) { + newState.isUpdated = true; + } + + return Object.assign({}, state, newState); + }, + + [SET_IS_SIDEBAR_VISIBLE]: function(state, { payload }) { + const newState = { + isSidebarVisible: payload.isSidebarVisible + }; + + return Object.assign({}, state, newState); + } + +}, defaultState, section); -export const hideMessage = createAction(types.HIDE_MESSAGE, (payload) => { - return { - section: 'messages', - ...payload - }; -}); diff --git a/frontend/src/Store/Actions/artistActionHandlers.js b/frontend/src/Store/Actions/artistActionHandlers.js deleted file mode 100644 index d455c40af..000000000 --- a/frontend/src/Store/Actions/artistActionHandlers.js +++ /dev/null @@ -1,132 +0,0 @@ -import _ from 'lodash'; -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import * as types from './actionTypes'; -import createFetchHandler from './Creators/createFetchHandler'; -import createSaveProviderHandler from './Creators/createSaveProviderHandler'; -import createRemoveItemHandler from './Creators/createRemoveItemHandler'; -import { updateItem } from './baseActions'; - -const section = 'artist'; - -const artistActionHandlers = { - [types.FETCH_ARTIST]: createFetchHandler(section, '/artist'), - - [types.SAVE_ARTIST]: createSaveProviderHandler( - section, - '/artist', - (state) => state.artist), - - [types.DELETE_ARTIST]: createRemoveItemHandler( - section, - '/artist', - (state) => state.artist), - - [types.TOGGLE_ARTIST_MONITORED]: function(payload) { - return function(dispatch, getState) { - const { - artistId: id, - monitored - } = payload; - - const artist = _.find(getState().artist.items, { id }); - - dispatch(updateItem({ - id, - section, - isSaving: true - })); - - const promise = $.ajax({ - url: `/artist/${id}`, - method: 'PUT', - data: JSON.stringify({ - ...artist, - monitored - }), - dataType: 'json' - }); - - promise.done((data) => { - dispatch(updateItem({ - id, - section, - isSaving: false, - monitored - })); - }); - - promise.fail((xhr) => { - dispatch(updateItem({ - id, - section, - isSaving: false - })); - }); - }; - }, - - [types.TOGGLE_ALBUM_MONITORED]: function(payload) { - return function(dispatch, getState) { - const { - artistId: id, - seasonNumber, - monitored - } = payload; - - const artist = _.find(getState().artist.items, { id }); - const seasons = _.cloneDeep(artist.seasons); - const season = _.find(seasons, { seasonNumber }); - - season.isSaving = true; - - dispatch(updateItem({ - id, - section, - seasons - })); - - season.monitored = monitored; - - const promise = $.ajax({ - url: `/artist/${id}`, - method: 'PUT', - data: JSON.stringify({ - ...artist, - seasons - }), - dataType: 'json' - }); - - promise.done((data) => { - const episodes = _.filter(getState().episodes.items, { artistId: id, seasonNumber }); - - dispatch(batchActions([ - updateItem({ - id, - section, - ...data - }), - - ...episodes.map((episode) => { - return updateItem({ - id: episode.id, - section: 'episodes', - monitored - }); - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(updateItem({ - id, - section, - seasons: artist.seasons - })); - }); - }; - } -}; - -export default artistActionHandlers; diff --git a/frontend/src/Store/Actions/artistActions.js b/frontend/src/Store/Actions/artistActions.js index bf805763b..28656a543 100644 --- a/frontend/src/Store/Actions/artistActions.js +++ b/frontend/src/Store/Actions/artistActions.js @@ -1,16 +1,187 @@ +import _ from 'lodash'; +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import artistActionHandlers from './artistActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer'; +import createFetchHandler from './Creators/createFetchHandler'; +import createSaveProviderHandler from './Creators/createSaveProviderHandler'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import createHandleActions from './Creators/createHandleActions'; +import { updateItem } from './baseActions'; -export const fetchArtist = artistActionHandlers[types.FETCH_ARTIST]; -export const saveArtist = artistActionHandlers[types.SAVE_ARTIST]; -export const deleteArtist = artistActionHandlers[types.DELETE_ARTIST]; -export const toggleArtistMonitored = artistActionHandlers[types.TOGGLE_ARTIST_MONITORED]; -export const toggleSeasonMonitored = artistActionHandlers[types.TOGGLE_ALBUM_MONITORED]; +// +// Variables -export const setArtistValue = createAction(types.SET_ARTIST_VALUE, (payload) => { +export const section = 'artist'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [], + sortKey: 'sortName', + sortDirection: sortDirections.ASCENDING, + pendingChanges: {} +}; + +// +// Actions Types + +export const FETCH_ARTIST = 'artist/fetchArtist'; +export const SET_ARTIST_VALUE = 'artist/setArtistValue'; +export const SAVE_ARTIST = 'artist/saveArtist'; +export const DELETE_ARTIST = 'artist/deleteArtist'; + +export const TOGGLE_ARTIST_MONITORED = 'artist/toggleArtistMonitored'; +export const TOGGLE_ALBUM_MONITORED = 'artist/toggleAlbumMonitored'; + +// +// Action Creators + +export const fetchArtist = createThunk(FETCH_ARTIST); +export const saveArtist = createThunk(SAVE_ARTIST); +export const deleteArtist = createThunk(DELETE_ARTIST); +export const toggleArtistMonitored = createThunk(TOGGLE_ARTIST_MONITORED); +export const toggleAlbumMonitored = createThunk(TOGGLE_ALBUM_MONITORED); + +export const setArtistValue = createAction(SET_ARTIST_VALUE, (payload) => { return { section: 'artist', ...payload }; }); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [FETCH_ARTIST]: createFetchHandler(section, '/artist'), + + [SAVE_ARTIST]: createSaveProviderHandler( + section, '/artist'), + + [DELETE_ARTIST]: createRemoveItemHandler( + section, + '/artist' + ), + + [TOGGLE_ARTIST_MONITORED]: (getState, payload, dispatch) => { + const { + artistId: id, + monitored + } = payload; + + const artist = _.find(getState().artist.items, { id }); + + dispatch(updateItem({ + id, + section, + isSaving: true + })); + + const promise = $.ajax({ + url: `/artist/${id}`, + method: 'PUT', + data: JSON.stringify({ + ...artist, + monitored + }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(updateItem({ + id, + section, + isSaving: false, + monitored + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + id, + section, + isSaving: false + })); + }); + }, + + [TOGGLE_ALBUM_MONITORED]: (getState, payload, dispatch) => { + const { + artistId: id, + seasonNumber, + monitored + } = payload; + + const artist = _.find(getState().artist.items, { id }); + const seasons = _.cloneDeep(artist.seasons); + const season = _.find(seasons, { seasonNumber }); + + season.isSaving = true; + + dispatch(updateItem({ + id, + section, + seasons + })); + + season.monitored = monitored; + + const promise = $.ajax({ + url: `/artist/${id}`, + method: 'PUT', + data: JSON.stringify({ + ...artist, + seasons + }), + dataType: 'json' + }); + + promise.done((data) => { + const episodes = _.filter(getState().episodes.items, { artistId: id, seasonNumber }); + + dispatch(batchActions([ + updateItem({ + id, + section, + ...data + }), + + ...episodes.map((episode) => { + return updateItem({ + id: episode.id, + section: 'episodes', + monitored + }); + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + id, + section, + seasons: artist.seasons + })); + }); + } + +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_ARTIST_VALUE]: createSetSettingValueReducer(section) + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/artistEditorActionHandlers.js b/frontend/src/Store/Actions/artistEditorActionHandlers.js deleted file mode 100644 index 81bc2ecdc..000000000 --- a/frontend/src/Store/Actions/artistEditorActionHandlers.js +++ /dev/null @@ -1,86 +0,0 @@ -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import * as types from './actionTypes'; -import { set, updateItem } from './baseActions'; - -const section = 'artistEditor'; - -const artistEditorActionHandlers = { - [types.SAVE_ARTIST_EDITOR]: function(payload) { - return function(dispatch, getState) { - dispatch(set({ - section, - isSaving: true - })); - - const promise = $.ajax({ - url: '/artist/editor', - method: 'PUT', - data: JSON.stringify(payload), - dataType: 'json' - }); - - promise.done((data) => { - dispatch(batchActions([ - ...data.map((artist) => { - return updateItem({ - id: artist.id, - section: 'artist', - ...artist - }); - }), - - set({ - section, - isSaving: false, - saveError: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isSaving: false, - saveError: xhr - })); - }); - }; - }, - - [types.BULK_DELETE_ARTIST]: function(payload) { - return function(dispatch, getState) { - dispatch(set({ - section, - isDeleting: true - })); - - const promise = $.ajax({ - url: '/artist/editor', - method: 'DELETE', - data: JSON.stringify(payload), - dataType: 'json' - }); - - promise.done(() => { - // SignaR will take care of removing the serires from the collection - - dispatch(set({ - section, - isDeleting: false, - deleteError: null - })); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isDeleting: false, - deleteError: xhr - })); - }); - }; - } -}; - -export default artistEditorActionHandlers; diff --git a/frontend/src/Store/Actions/artistEditorActions.js b/frontend/src/Store/Actions/artistEditorActions.js index 5dbaf4f26..4da674087 100644 --- a/frontend/src/Store/Actions/artistEditorActions.js +++ b/frontend/src/Store/Actions/artistEditorActions.js @@ -1,8 +1,142 @@ +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import artistEditorActionHandlers from './artistEditorActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import { filterTypes, sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; +import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; +import createHandleActions from './Creators/createHandleActions'; +import { set, updateItem } from './baseActions'; -export const setArtistEditorSort = createAction(types.SET_ARTIST_EDITOR_SORT); -export const setArtistEditorFilter = createAction(types.SET_ARTIST_EDITOR_FILTER); -export const saveArtistEditor = artistEditorActionHandlers[types.SAVE_ARTIST_EDITOR]; -export const bulkDeleteArtist = artistEditorActionHandlers[types.BULK_DELETE_ARTIST]; +// +// Variables + +export const section = 'artistEditor'; + +// +// State + +export const defaultState = { + isSaving: false, + saveError: null, + isDeleting: false, + deleteError: null, + sortKey: 'sortName', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortName', + secondarySortDirection: sortDirections.ASCENDING, + filterKey: null, + filterValue: null, + filterType: filterTypes.EQUAL +}; + +export const persistState = [ + 'artistEditor.sortKey', + 'artistEditor.sortDirection', + 'artistEditor.filterKey', + 'artistEditor.filterValue', + 'artistEditor.filterType' +]; + +// +// Actions Types + +export const SET_ARTIST_EDITOR_SORT = 'artistEditor/setArtistEditorSort'; +export const SET_ARTIST_EDITOR_FILTER = 'artistEditor/setArtistEditorFilter'; +export const SAVE_ARTIST_EDITOR = 'artistEditor/saveArtistEditor'; +export const BULK_DELETE_ARTIST = 'artistEditor/bulkDeleteArtist'; + +// +// Action Creators + +export const setArtistEditorSort = createAction(SET_ARTIST_EDITOR_SORT); +export const setArtistEditorFilter = createAction(SET_ARTIST_EDITOR_FILTER); +export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR); +export const bulkDeleteArtist = createThunk(BULK_DELETE_ARTIST); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [SAVE_ARTIST_EDITOR]: function(getState, payload, dispatch) { + dispatch(set({ + section, + isSaving: true + })); + + const promise = $.ajax({ + url: '/artist/editor', + method: 'PUT', + data: JSON.stringify(payload), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(batchActions([ + ...data.map((artist) => { + return updateItem({ + id: artist.id, + section: 'artist', + ...artist + }); + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }, + + [BULK_DELETE_ARTIST]: function(getState, payload, dispatch) { + dispatch(set({ + section, + isDeleting: true + })); + + const promise = $.ajax({ + url: '/artist/editor', + method: 'DELETE', + data: JSON.stringify(payload), + dataType: 'json' + }); + + promise.done(() => { + // SignaR will take care of removing the serires from the collection + + dispatch(set({ + section, + isDeleting: false, + deleteError: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section), + [SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section) + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js index 2e15d6c0b..e3d085b66 100644 --- a/frontend/src/Store/Actions/artistIndexActions.js +++ b/frontend/src/Store/Actions/artistIndexActions.js @@ -1,10 +1,279 @@ +import moment from 'moment'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; +import { filterTypes, sortDirections } from 'Helpers/Props'; +import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; +import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; +import createHandleActions from './Creators/createHandleActions'; -export const setArtistSort = createAction(types.SET_ARTIST_SORT); -export const setArtistFilter = createAction(types.SET_ARTIST_FILTER); -export const setArtistView = createAction(types.SET_ARTIST_VIEW); -export const setArtistTableOption = createAction(types.SET_ARTIST_TABLE_OPTION); -export const setArtistPosterOption = createAction(types.SET_ARTIST_POSTER_OPTION); -export const setArtistBannerOption = createAction(types.SET_ARTIST_BANNER_OPTION); -export const setArtistOverviewOption = createAction(types.SET_ARTIST_OVERVIEW_OPTION); +// +// Variables + +export const section = 'artistIndex'; + +// +// State + +export const defaultState = { + sortKey: 'sortName', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortName', + secondarySortDirection: sortDirections.ASCENDING, + filterKey: null, + filterValue: null, + filterType: filterTypes.EQUAL, + view: 'posters', + + posterOptions: { + detailedProgressBar: false, + size: 'large', + showTitle: false, + showQualityProfile: true + }, + + bannerOptions: { + detailedProgressBar: false, + size: 'large', + showTitle: false, + showQualityProfile: true + }, + + overviewOptions: { + detailedProgressBar: false, + size: 'medium', + showNetwork: true, + showQualityProfile: true, + showPreviousAiring: false, + showAdded: false, + showAlbumCount: true, + showPath: false, + showSizeOnDisk: false + }, + + columns: [ + { + name: 'status', + columnLabel: 'Status', + isVisible: true, + isModifiable: false + }, + { + name: 'sortName', + label: 'Artist Name', + isSortable: true, + isVisible: true, + isModifiable: false + }, + { + name: 'artistType', + label: 'Type', + isSortable: true, + isVisible: true, + isModifiable: false + }, + { + name: 'qualityProfileId', + label: 'Quality Profile', + isSortable: true, + isVisible: true + }, + { + name: 'languageProfileId', + label: 'Language Profile', + isSortable: true, + isVisible: false + }, + { + name: 'metadataProfileId', + label: 'Metadata Profile', + isSortable: true, + isVisible: false + }, + { + name: 'nextAiring', + label: 'Next Airing', + isSortable: true, + isVisible: true + }, + { + name: 'previousAiring', + label: 'Previous Airing', + isSortable: true, + isVisible: false + }, + { + name: 'added', + label: 'Added', + isSortable: true, + isVisible: false + }, + { + name: 'albumCount', + label: 'Albums', + isSortable: true, + isVisible: true + }, + { + name: 'trackProgress', + label: 'Tracks', + isSortable: true, + isVisible: true + }, + { + name: 'trackCount', + label: 'Track Count', + isSortable: true, + isVisible: false + }, + { + name: 'latestAlbum', + label: 'Latest Album', + isSortable: true, + isVisible: false + }, + { + name: 'path', + label: 'Path', + isSortable: true, + isVisible: false + }, + { + name: 'sizeOnDisk', + label: 'Size on Disk', + isSortable: true, + isVisible: false + }, + { + name: 'tags', + label: 'Tags', + isSortable: false, + isVisible: false + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ], + + sortPredicates: { + nextAiring: function(item, direction) { + const nextAiring = item.nextAiring; + + if (nextAiring) { + return moment(nextAiring).unix(); + } + + if (direction === sortDirections.DESCENDING) { + return 0; + } + + return Number.MAX_VALUE; + }, + + trackProgress: function(item) { + const { + trackCount = 0, + trackFileCount + } = item; + + const progress = trackCount ? trackFileCount / trackCount * 100 : 100; + + return progress + trackCount / 1000000; + } + }, + + filterPredicates: { + missing: function(item) { + return item.trackCount - item.trackFileCount > 0; + } + } +}; + +export const persistState = [ + 'artistIndex.sortKey', + 'artistIndex.sortDirection', + 'artistIndex.filterKey', + 'artistIndex.filterValue', + 'artistIndex.filterType', + 'artistIndex.view', + 'artistIndex.columns', + 'artistIndex.posterOptions', + 'artistIndex.bannerOptions', + 'artistIndex.overviewOptions' +]; + +// +// Actions Types + +export const SET_ARTIST_SORT = 'artistIndex/setArtistSort'; +export const SET_ARTIST_FILTER = 'artistIndex/setArtistFilter'; +export const SET_ARTIST_VIEW = 'artistIndex/setArtistView'; +export const SET_ARTIST_TABLE_OPTION = 'artistIndex/setArtistTableOption'; +export const SET_ARTIST_POSTER_OPTION = 'artistIndex/setArtistPosterOption'; +export const SET_ARTIST_BANNER_OPTION = 'artistIndex/setArtistBannerOption'; +export const SET_ARTIST_OVERVIEW_OPTION = 'artistIndex/setArtistOverviewOption'; + +// +// Action Creators + +export const setArtistSort = createAction(SET_ARTIST_SORT); +export const setArtistFilter = createAction(SET_ARTIST_FILTER); +export const setArtistView = createAction(SET_ARTIST_VIEW); +export const setArtistTableOption = createAction(SET_ARTIST_TABLE_OPTION); +export const setArtistPosterOption = createAction(SET_ARTIST_POSTER_OPTION); +export const setArtistBannerOption = createAction(SET_ARTIST_BANNER_OPTION); +export const setArtistOverviewOption = createAction(SET_ARTIST_OVERVIEW_OPTION); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_ARTIST_SORT]: createSetClientSideCollectionSortReducer(section), + [SET_ARTIST_FILTER]: createSetClientSideCollectionFilterReducer(section), + + [SET_ARTIST_VIEW]: function(state, { payload }) { + return Object.assign({}, state, { view: payload.view }); + }, + + [SET_ARTIST_TABLE_OPTION]: createSetTableOptionReducer(section), + + [SET_ARTIST_POSTER_OPTION]: function(state, { payload }) { + const posterOptions = state.posterOptions; + + return { + ...state, + posterOptions: { + ...posterOptions, + ...payload + } + }; + }, + + [SET_ARTIST_BANNER_OPTION]: function(state, { payload }) { + const bannerOptions = state.bannerOptions; + + return { + ...state, + bannerOptions: { + ...bannerOptions, + ...payload + } + }; + }, + + [SET_ARTIST_OVERVIEW_OPTION]: function(state, { payload }) { + const overviewOptions = state.overviewOptions; + + return { + ...state, + overviewOptions: { + ...overviewOptions, + ...payload + } + }; + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/baseActions.js b/frontend/src/Store/Actions/baseActions.js index e2d7e9d7e..37be3e0d2 100644 --- a/frontend/src/Store/Actions/baseActions.js +++ b/frontend/src/Store/Actions/baseActions.js @@ -1,13 +1,29 @@ import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -export const set = createAction(types.SET); +// +// Action Types -export const update = createAction(types.UPDATE); -export const updateItem = createAction(types.UPDATE_ITEM); -export const updateServerSideCollection = createAction(types.UPDATE_SERVER_SIDE_COLLECTION); +export const SET = 'base/set'; -export const setSettingValue = createAction(types.SET_SETTING_VALUE); -export const clearPendingChanges = createAction(types.CLEAR_PENDING_CHANGES); +export const UPDATE = 'base/update'; +export const UPDATE_ITEM = 'base/updateItem'; +export const UPDATE_SERVER_SIDE_COLLECTION = 'base/updateServerSideCollection'; -export const removeItem = createAction(types.REMOVE_ITEM); +export const SET_SETTING_VALUE = 'base/setSettingValue'; +export const CLEAR_PENDING_CHANGES = 'base/clearPendingChanges'; + +export const REMOVE_ITEM = 'base/removeItem'; + +// +// Action Creators + +export const set = createAction(SET); + +export const update = createAction(UPDATE); +export const updateItem = createAction(UPDATE_ITEM); +export const updateServerSideCollection = createAction(UPDATE_SERVER_SIDE_COLLECTION); + +export const setSettingValue = createAction(SET_SETTING_VALUE); +export const clearPendingChanges = createAction(CLEAR_PENDING_CHANGES); + +export const removeItem = createAction(REMOVE_ITEM); diff --git a/frontend/src/Store/Actions/blacklistActionHandlers.js b/frontend/src/Store/Actions/blacklistActionHandlers.js deleted file mode 100644 index 0a4e4a1f6..000000000 --- a/frontend/src/Store/Actions/blacklistActionHandlers.js +++ /dev/null @@ -1,17 +0,0 @@ -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; -import * as types from './actionTypes'; -import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; - -const blacklistActionHandlers = { - ...createServerSideCollectionHandlers('blacklist', '/blacklist', (state) => state, { - [serverSideCollectionHandlers.FETCH]: types.FETCH_BLACKLIST, - [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_BLACKLIST_PAGE, - [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_BLACKLIST_PAGE, - [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_BLACKLIST_PAGE, - [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_BLACKLIST_PAGE, - [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_BLACKLIST_PAGE, - [serverSideCollectionHandlers.SORT]: types.SET_BLACKLIST_SORT - }) -}; - -export default blacklistActionHandlers; diff --git a/frontend/src/Store/Actions/blacklistActions.js b/frontend/src/Store/Actions/blacklistActions.js index d931842d1..956fc8675 100644 --- a/frontend/src/Store/Actions/blacklistActions.js +++ b/frontend/src/Store/Actions/blacklistActions.js @@ -1,12 +1,127 @@ import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import blacklistActionHandlers from './blacklistActionHandlers'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import { createThunk, handleThunks } from 'Store/thunks'; +import { sortDirections } from 'Helpers/Props'; +import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; +import createHandleActions from './Creators/createHandleActions'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; -export const fetchBlacklist = blacklistActionHandlers[types.FETCH_BLACKLIST]; -export const gotoBlacklistFirstPage = blacklistActionHandlers[types.GOTO_FIRST_BLACKLIST_PAGE]; -export const gotoBlacklistPreviousPage = blacklistActionHandlers[types.GOTO_PREVIOUS_BLACKLIST_PAGE]; -export const gotoBlacklistNextPage = blacklistActionHandlers[types.GOTO_NEXT_BLACKLIST_PAGE]; -export const gotoBlacklistLastPage = blacklistActionHandlers[types.GOTO_LAST_BLACKLIST_PAGE]; -export const gotoBlacklistPage = blacklistActionHandlers[types.GOTO_BLACKLIST_PAGE]; -export const setBlacklistSort = blacklistActionHandlers[types.SET_BLACKLIST_SORT]; -export const setBlacklistTableOption = createAction(types.SET_BLACKLIST_TABLE_OPTION); +// +// Variables + +export const section = 'blacklist'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'date', + sortDirection: sortDirections.DESCENDING, + error: null, + items: [], + + columns: [ + { + name: 'artist.sortName', + label: 'Artist Name', + isSortable: true, + isVisible: true + }, + { + name: 'sourceTitle', + label: 'Source Title', + isSortable: true, + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: false + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + }, + { + name: 'date', + label: 'Date', + isSortable: true, + isVisible: true + }, + { + name: 'indexer', + label: 'Indexer', + isSortable: true, + isVisible: false + }, + { + name: 'details', + columnLabel: 'Details', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'blacklist.pageSize', + 'blacklist.sortKey', + 'blacklist.sortDirection', + 'blacklist.columns' +]; + +// +// Action Types + +export const FETCH_BLACKLIST = 'blacklist/fetchBlacklist'; +export const GOTO_FIRST_BLACKLIST_PAGE = 'blacklist/gotoBlacklistFirstPage'; +export const GOTO_PREVIOUS_BLACKLIST_PAGE = 'blacklist/gotoBlacklistPreviousPage'; +export const GOTO_NEXT_BLACKLIST_PAGE = 'blacklist/gotoBlacklistNextPage'; +export const GOTO_LAST_BLACKLIST_PAGE = 'blacklist/gotoBlacklistLastPage'; +export const GOTO_BLACKLIST_PAGE = 'blacklist/gotoBlacklistPage'; +export const SET_BLACKLIST_SORT = 'blacklist/setBlacklistSort'; +export const SET_BLACKLIST_TABLE_OPTION = 'blacklist/setBlacklistTableOption'; + +// +// Action Creators + +export const fetchBlacklist = createThunk(FETCH_BLACKLIST); +export const gotoBlacklistFirstPage = createThunk(GOTO_FIRST_BLACKLIST_PAGE); +export const gotoBlacklistPreviousPage = createThunk(GOTO_PREVIOUS_BLACKLIST_PAGE); +export const gotoBlacklistNextPage = createThunk(GOTO_NEXT_BLACKLIST_PAGE); +export const gotoBlacklistLastPage = createThunk(GOTO_LAST_BLACKLIST_PAGE); +export const gotoBlacklistPage = createThunk(GOTO_BLACKLIST_PAGE); +export const setBlacklistSort = createThunk(SET_BLACKLIST_SORT); +export const setBlacklistTableOption = createAction(SET_BLACKLIST_TABLE_OPTION); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + ...createServerSideCollectionHandlers( + section, + '/blacklist', + fetchBlacklist, + { + [serverSideCollectionHandlers.FETCH]: FETCH_BLACKLIST, + [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_BLACKLIST_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_BLACKLIST_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_BLACKLIST_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_BLACKLIST_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_BLACKLIST_PAGE, + [serverSideCollectionHandlers.SORT]: SET_BLACKLIST_SORT + }) +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_BLACKLIST_TABLE_OPTION]: createSetTableOptionReducer(section) + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/calendarActionHandlers.js b/frontend/src/Store/Actions/calendarActionHandlers.js deleted file mode 100644 index bb619858a..000000000 --- a/frontend/src/Store/Actions/calendarActionHandlers.js +++ /dev/null @@ -1,264 +0,0 @@ -import _ from 'lodash'; -import $ from 'jquery'; -import moment from 'moment'; -import { batchActions } from 'redux-batched-actions'; -import * as calendarViews from 'Calendar/calendarViews'; -import * as types from './actionTypes'; -import { set, update } from './baseActions'; -import { fetchCalendar } from './calendarActions'; - -const viewRanges = { - [calendarViews.DAY]: 'day', - [calendarViews.WEEK]: 'week', - [calendarViews.MONTH]: 'month', - [calendarViews.FORECAST]: 'day' -}; - -function getDays(start, end) { - const startTime = moment(start); - const endTime = moment(end); - const difference = endTime.diff(startTime, 'days'); - - // Difference is one less than the number of days we need to account for. - return _.times(difference + 1, (i) => { - return startTime.clone().add(i, 'days').toISOString(); - }); -} - -function getDates(time, view, firstDayOfWeek, dayCount) { - const weekName = firstDayOfWeek === 0 ? 'week' : 'isoWeek'; - - let start = time.clone().startOf('day'); - let end = time.clone().endOf('day'); - - if (view === calendarViews.WEEK) { - start = time.clone().startOf(weekName); - end = time.clone().endOf(weekName); - } - - if (view === calendarViews.FORECAST) { - start = time.clone().subtract(1, 'day').startOf('day'); - end = time.clone().add(dayCount - 2, 'days').endOf('day'); - } - - if (view === calendarViews.MONTH) { - start = time.clone().startOf('month').startOf(weekName); - end = time.clone().endOf('month').endOf(weekName); - } - - if (view === calendarViews.AGENDA) { - start = time.clone().subtract(1, 'day').startOf('day'); - end = time.clone().add(1, 'month').endOf('day'); - } - - return { - start: start.toISOString(), - end: end.toISOString(), - time: time.toISOString(), - dates: getDays(start, end) - }; -} - -function getPopulatableRange(startDate, endDate, view) { - switch (view) { - case calendarViews.DAY: - return { - start: moment(startDate).subtract(1, 'day').toISOString(), - end: moment(endDate).add(1, 'day').toISOString() - }; - case calendarViews.WEEK: - case calendarViews.FORECAST: - return { - start: moment(startDate).subtract(1, 'week').toISOString(), - end: moment(endDate).add(1, 'week').toISOString() - }; - default: - return { - start: startDate, - end: endDate - }; - } -} - -function isRangePopulated(start, end, state) { - const { - start: currentStart, - end: currentEnd, - view: currentView - } = state; - - if (!currentStart || !currentEnd) { - return false; - } - - const { - start: currentPopulatedStart, - end: currentPopulatedEnd - } = getPopulatableRange(currentStart, currentEnd, currentView); - - if ( - moment(start).isAfter(currentPopulatedStart) && - moment(start).isBefore(currentPopulatedEnd) - ) { - return true; - } - - return false; -} - -const section = 'calendar'; - -const calendarActionHandlers = { - [types.FETCH_CALENDAR]: function(payload) { - return function(dispatch, getState) { - const state = getState(); - const unmonitored = state.calendar.unmonitored; - - const { - time, - view - } = payload; - - const dayCount = state.calendar.dayCount; - const dates = getDates(moment(time), view, state.settings.ui.item.firstDayOfWeek, dayCount); - const { start, end } = getPopulatableRange(dates.start, dates.end, view); - const isPrePopulated = isRangePopulated(start, end, state.calendar); - - const basesAttrs = { - section, - isFetching: true - }; - - const attrs = isPrePopulated ? - { - view, - ...basesAttrs, - ...dates - } : - basesAttrs; - - dispatch(set(attrs)); - - const promise = $.ajax({ - url: '/calendar', - data: { - unmonitored, - start, - end - } - }); - - promise.done((data) => { - dispatch(batchActions([ - update({ section, data }), - - set({ - section, - view, - ...dates, - isFetching: false, - isPopulated: true, - error: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isFetching: false, - isPopulated: false, - error: xhr - })); - }); - }; - }, - - [types.SET_CALENDAR_DAYS_COUNT]: function(payload) { - return function(dispatch, getState) { - if (payload.dayCount === getState().calendar.dayCount) { - return; - } - - dispatch(set({ - section, - dayCount: payload.dayCount - })); - - const state = getState(); - const { time, view } = state.calendar; - - dispatch(fetchCalendar({ time, view })); - }; - }, - - [types.SET_CALENDAR_INCLUDE_UNMONITORED]: function(payload) { - return function(dispatch, getState) { - dispatch(set({ - section, - unmonitored: payload.unmonitored - })); - - const state = getState(); - const { time, view } = state.calendar; - - dispatch(fetchCalendar({ time, view })); - }; - }, - - [types.SET_CALENDAR_VIEW]: function(payload) { - return function(dispatch, getState) { - const state = getState(); - const view = payload.view; - const time = view === calendarViews.FORECAST ? - moment() : - state.calendar.time; - - dispatch(fetchCalendar({ time, view })); - }; - }, - - [types.GOTO_CALENDAR_TODAY]: function(payload) { - return function(dispatch, getState) { - const state = getState(); - const view = state.calendar.view; - const time = moment(); - - dispatch(fetchCalendar({ time, view })); - }; - }, - - [types.GOTO_CALENDAR_PREVIOUS_RANGE]: function(payload) { - return function(dispatch, getState) { - const state = getState(); - - const { - view, - dayCount - } = state.calendar; - - const amount = view === calendarViews.FORECAST ? dayCount : 1; - const time = moment(state.calendar.time).subtract(amount, viewRanges[view]); - - dispatch(fetchCalendar({ time, view })); - }; - }, - - [types.GOTO_CALENDAR_NEXT_RANGE]: function(payload) { - return function(dispatch, getState) { - const state = getState(); - - const { - view, - dayCount - } = state.calendar; - - const amount = view === calendarViews.FORECAST ? dayCount : 1; - const time = moment(state.calendar.time).add(amount, viewRanges[view]); - - dispatch(fetchCalendar({ time, view })); - }; - } -}; - -export default calendarActionHandlers; diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js index 452769343..acc3b0f17 100644 --- a/frontend/src/Store/Actions/calendarActions.js +++ b/frontend/src/Store/Actions/calendarActions.js @@ -1,12 +1,323 @@ +import _ from 'lodash'; +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import calendarActionHandlers from './calendarActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import moment from 'moment'; +import { createThunk, handleThunks } from 'Store/thunks'; +import * as calendarViews from 'Calendar/calendarViews'; +import createHandleActions from './Creators/createHandleActions'; +import { set, update } from './baseActions'; -export const fetchCalendar = calendarActionHandlers[types.FETCH_CALENDAR]; -export const setCalendarDaysCount = calendarActionHandlers[types.SET_CALENDAR_DAYS_COUNT]; -export const setCalendarIncludeUnmonitored = calendarActionHandlers[types.SET_CALENDAR_INCLUDE_UNMONITORED]; -export const setCalendarView = calendarActionHandlers[types.SET_CALENDAR_VIEW]; -export const gotoCalendarToday = calendarActionHandlers[types.GOTO_CALENDAR_TODAY]; -export const gotoCalendarPreviousRange = calendarActionHandlers[types.GOTO_CALENDAR_PREVIOUS_RANGE]; -export const gotoCalendarNextRange = calendarActionHandlers[types.GOTO_CALENDAR_NEXT_RANGE]; -export const clearCalendar = createAction(types.CLEAR_CALENDAR); +// +// Variables + +export const section = 'calendar'; + +const viewRanges = { + [calendarViews.DAY]: 'day', + [calendarViews.WEEK]: 'week', + [calendarViews.MONTH]: 'month', + [calendarViews.FORECAST]: 'day' +}; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + start: null, + end: null, + dates: [], + dayCount: 7, + view: window.innerWidth > 768 ? 'week' : 'day', + unmonitored: false, + showUpcoming: true, + error: null, + items: [] +}; + +export const persistState = [ + 'calendar.view', + 'calendar.unmonitored', + 'calendar.showUpcoming' +]; + +// +// Actions Types + +export const FETCH_CALENDAR = 'calendar/fetchCalendar'; +export const SET_CALENDAR_DAYS_COUNT = 'calendar/setCalendarDaysCount'; +export const SET_CALENDAR_INCLUDE_UNMONITORED = 'calendar/setCalendarIncludeUnmonitored'; +export const SET_CALENDAR_VIEW = 'calendar/setCalendarView'; +export const GOTO_CALENDAR_TODAY = 'calendar/gotoCalendarToday'; +export const GOTO_CALENDAR_PREVIOUS_RANGE = 'calendar/gotoCalendarPreviousRange'; +export const GOTO_CALENDAR_NEXT_RANGE = 'calendar/gotoCalendarNextRange'; +export const CLEAR_CALENDAR = 'calendar/clearCalendar'; + +// +// Helpers + +function getDays(start, end) { + const startTime = moment(start); + const endTime = moment(end); + const difference = endTime.diff(startTime, 'days'); + + // Difference is one less than the number of days we need to account for. + return _.times(difference + 1, (i) => { + return startTime.clone().add(i, 'days').toISOString(); + }); +} + +function getDates(time, view, firstDayOfWeek, dayCount) { + const weekName = firstDayOfWeek === 0 ? 'week' : 'isoWeek'; + + let start = time.clone().startOf('day'); + let end = time.clone().endOf('day'); + + if (view === calendarViews.WEEK) { + start = time.clone().startOf(weekName); + end = time.clone().endOf(weekName); + } + + if (view === calendarViews.FORECAST) { + start = time.clone().subtract(1, 'day').startOf('day'); + end = time.clone().add(dayCount - 2, 'days').endOf('day'); + } + + if (view === calendarViews.MONTH) { + start = time.clone().startOf('month').startOf(weekName); + end = time.clone().endOf('month').endOf(weekName); + } + + if (view === calendarViews.AGENDA) { + start = time.clone().subtract(1, 'day').startOf('day'); + end = time.clone().add(1, 'month').endOf('day'); + } + + return { + start: start.toISOString(), + end: end.toISOString(), + time: time.toISOString(), + dates: getDays(start, end) + }; +} + +function getPopulatableRange(startDate, endDate, view) { + switch (view) { + case calendarViews.DAY: + return { + start: moment(startDate).subtract(1, 'day').toISOString(), + end: moment(endDate).add(1, 'day').toISOString() + }; + case calendarViews.WEEK: + case calendarViews.FORECAST: + return { + start: moment(startDate).subtract(1, 'week').toISOString(), + end: moment(endDate).add(1, 'week').toISOString() + }; + default: + return { + start: startDate, + end: endDate + }; + } +} + +function isRangePopulated(start, end, state) { + const { + start: currentStart, + end: currentEnd, + view: currentView + } = state; + + if (!currentStart || !currentEnd) { + return false; + } + + const { + start: currentPopulatedStart, + end: currentPopulatedEnd + } = getPopulatableRange(currentStart, currentEnd, currentView); + + if ( + moment(start).isAfter(currentPopulatedStart) && + moment(start).isBefore(currentPopulatedEnd) + ) { + return true; + } + + return false; +} + +// +// Action Creators + +export const fetchCalendar = createThunk(FETCH_CALENDAR); +export const setCalendarDaysCount = createThunk(SET_CALENDAR_DAYS_COUNT); +export const setCalendarIncludeUnmonitored = createThunk(SET_CALENDAR_INCLUDE_UNMONITORED); +export const setCalendarView = createThunk(SET_CALENDAR_VIEW); +export const gotoCalendarToday = createThunk(GOTO_CALENDAR_TODAY); +export const gotoCalendarPreviousRange = createThunk(GOTO_CALENDAR_PREVIOUS_RANGE); +export const gotoCalendarNextRange = createThunk(GOTO_CALENDAR_NEXT_RANGE); +export const clearCalendar = createAction(CLEAR_CALENDAR); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [FETCH_CALENDAR]: function(getState, payload, dispatch) { + const state = getState(); + const unmonitored = state.calendar.unmonitored; + + const { + time, + view + } = payload; + + const dayCount = state.calendar.dayCount; + const dates = getDates(moment(time), view, state.settings.ui.item.firstDayOfWeek, dayCount); + const { start, end } = getPopulatableRange(dates.start, dates.end, view); + const isPrePopulated = isRangePopulated(start, end, state.calendar); + + const basesAttrs = { + section, + isFetching: true + }; + + const attrs = isPrePopulated ? + { + view, + ...basesAttrs, + ...dates + } : + basesAttrs; + + dispatch(set(attrs)); + + const promise = $.ajax({ + url: '/calendar', + data: { + unmonitored, + start, + end + } + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + view, + ...dates, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }, + + [SET_CALENDAR_DAYS_COUNT]: function(getState, payload, dispatch) { + if (payload.dayCount === getState().calendar.dayCount) { + return; + } + + dispatch(set({ + section, + dayCount: payload.dayCount + })); + + const state = getState(); + const { time, view } = state.calendar; + + dispatch(fetchCalendar({ time, view })); + }, + + [SET_CALENDAR_INCLUDE_UNMONITORED]: function(getState, payload, dispatch) { + dispatch(set({ + section, + unmonitored: payload.unmonitored + })); + + const state = getState(); + const { time, view } = state.calendar; + + dispatch(fetchCalendar({ time, view })); + }, + + [SET_CALENDAR_VIEW]: function(getState, payload, dispatch) { + const state = getState(); + const view = payload.view; + const time = view === calendarViews.FORECAST ? + moment() : + state.calendar.time; + + dispatch(fetchCalendar({ time, view })); + }, + + [GOTO_CALENDAR_TODAY]: function(getState, payload, dispatch) { + const state = getState(); + const view = state.calendar.view; + const time = moment(); + + dispatch(fetchCalendar({ time, view })); + }, + + [GOTO_CALENDAR_PREVIOUS_RANGE]: function(getState, payload, dispatch) { + const state = getState(); + + const { + view, + dayCount + } = state.calendar; + + const amount = view === calendarViews.FORECAST ? dayCount : 1; + const time = moment(state.calendar.time).subtract(amount, viewRanges[view]); + + dispatch(fetchCalendar({ time, view })); + }, + + [GOTO_CALENDAR_NEXT_RANGE]: function(getState, payload, dispatch) { + const state = getState(); + + const { + view, + dayCount + } = state.calendar; + + const amount = view === calendarViews.FORECAST ? dayCount : 1; + const time = moment(state.calendar.time).add(amount, viewRanges[view]); + + dispatch(fetchCalendar({ time, view })); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [CLEAR_CALENDAR]: (state) => { + const { + view, + unmonitored, + showUpcoming, + ...otherDefaultState + } = defaultState; + + return Object.assign({}, state, otherDefaultState); + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/captchaActionHandlers.js b/frontend/src/Store/Actions/captchaActionHandlers.js deleted file mode 100644 index 6e00a840e..000000000 --- a/frontend/src/Store/Actions/captchaActionHandlers.js +++ /dev/null @@ -1,67 +0,0 @@ -import requestAction from 'Utilities/requestAction'; -import * as types from './actionTypes'; -import { setCaptchaValue } from './captchaActions'; - -const captchaActionHandlers = { - [types.REFRESH_CAPTCHA]: function(payload) { - return (dispatch, getState) => { - const actionPayload = { - action: 'checkCaptcha', - ...payload - }; - - dispatch(setCaptchaValue({ - refreshing: true - })); - - const promise = requestAction(actionPayload); - - promise.done((data) => { - if (!data.captchaRequest) { - dispatch(setCaptchaValue({ - refreshing: false - })); - } - - dispatch(setCaptchaValue({ - refreshing: false, - ...data.captchaRequest - })); - }); - - promise.fail(() => { - dispatch(setCaptchaValue({ - refreshing: false - })); - }); - }; - }, - - [types.GET_CAPTCHA_COOKIE]: function(payload) { - return (dispatch, getState) => { - const state = getState().captcha; - - const queryParams = { - responseUrl: state.responseUrl, - ray: state.ray, - captchaResponse: payload.captchaResponse - }; - - const actionPayload = { - action: 'getCaptchaCookie', - queryParams, - ...payload - }; - - const promise = requestAction(actionPayload); - - promise.done((data) => { - dispatch(setCaptchaValue({ - token: data.captchaToken - })); - }); - }; - } -}; - -export default captchaActionHandlers; diff --git a/frontend/src/Store/Actions/captchaActions.js b/frontend/src/Store/Actions/captchaActions.js index e87a2a088..d506566f7 100644 --- a/frontend/src/Store/Actions/captchaActions.js +++ b/frontend/src/Store/Actions/captchaActions.js @@ -1,8 +1,119 @@ import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import captchaActionHandlers from './captchaActionHandlers'; +import requestAction from 'Utilities/requestAction'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createHandleActions from './Creators/createHandleActions'; -export const refreshCaptcha = captchaActionHandlers[types.REFRESH_CAPTCHA]; -export const getCaptchaCookie = captchaActionHandlers[types.GET_CAPTCHA_COOKIE]; -export const setCaptchaValue = createAction(types.SET_CAPTCHA_VALUE); -export const resetCaptcha = createAction(types.RESET_CAPTCHA); +// +// Variables + +export const section = 'captcha'; + +// +// State + +export const defaultState = { + refreshing: false, + token: null, + siteKey: null, + secretToken: null, + ray: null, + stoken: null, + responseUrl: null +}; + +// +// Actions Types + +export const REFRESH_CAPTCHA = 'captcha/refreshCaptcha'; +export const GET_CAPTCHA_COOKIE = 'captcha/getCaptchaCookie'; +export const SET_CAPTCHA_VALUE = 'captcha/setCaptchaValue'; +export const RESET_CAPTCHA = 'captcha/resetCaptcha'; + +// +// Action Creators + +export const refreshCaptcha = createThunk(REFRESH_CAPTCHA); +export const getCaptchaCookie = createThunk(GET_CAPTCHA_COOKIE); +export const setCaptchaValue = createAction(SET_CAPTCHA_VALUE); +export const resetCaptcha = createAction(RESET_CAPTCHA); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [REFRESH_CAPTCHA]: function(getState, payload, dispatch) { + const actionPayload = { + action: 'checkCaptcha', + ...payload + }; + + dispatch(setCaptchaValue({ + refreshing: true + })); + + const promise = requestAction(actionPayload); + + promise.done((data) => { + if (!data.captchaRequest) { + dispatch(setCaptchaValue({ + refreshing: false + })); + } + + dispatch(setCaptchaValue({ + refreshing: false, + ...data.captchaRequest + })); + }); + + promise.fail(() => { + dispatch(setCaptchaValue({ + refreshing: false + })); + }); + }, + + [GET_CAPTCHA_COOKIE]: function(getState, payload, dispatch) { + const state = getState().captcha; + + const queryParams = { + responseUrl: state.responseUrl, + ray: state.ray, + captchaResponse: payload.captchaResponse + }; + + const actionPayload = { + action: 'getCaptchaCookie', + queryParams, + ...payload + }; + + const promise = requestAction(actionPayload); + + promise.done((data) => { + dispatch(setCaptchaValue({ + token: data.captchaToken + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_CAPTCHA_VALUE]: function(state, { payload }) { + const newState = Object.assign(getSectionState(state, section), payload); + + return updateSectionState(state, section, newState); + }, + + [RESET_CAPTCHA]: function(state) { + return updateSectionState(state, section, defaultState); + } + +}, defaultState); diff --git a/frontend/src/Store/Actions/commandActionHandlers.js b/frontend/src/Store/Actions/commandActionHandlers.js deleted file mode 100644 index e0f8baea0..000000000 --- a/frontend/src/Store/Actions/commandActionHandlers.js +++ /dev/null @@ -1,141 +0,0 @@ -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import { isSameCommand } from 'Utilities/Command'; -import { messageTypes } from 'Helpers/Props'; -import * as types from './actionTypes'; -import createFetchHandler from './Creators/createFetchHandler'; -import { showMessage, hideMessage } from './appActions'; -import { updateItem } from './baseActions'; -import { addCommand, removeCommand } from './commandActions'; - -let lastCommand = null; -let lastCommandTimeout = null; -const removeCommandTimeoutIds = {}; - -function showCommandMessage(payload, dispatch) { - const { - id, - name, - manual, - message, - body = {}, - state - } = payload; - - const { - sendUpdatesToClient, - suppressMessages - } = body; - - if (!message || !body || !sendUpdatesToClient || suppressMessages) { - return; - } - - let type = messageTypes.INFO; - let hideAfter = 0; - - if (state === 'completed') { - type = messageTypes.SUCCESS; - hideAfter = 4; - } else if (state === 'failed') { - type = messageTypes.ERROR; - hideAfter = manual ? 10 : 4; - } - - dispatch(showMessage({ - id, - name, - message, - type, - hideAfter - })); -} - -function scheduleRemoveCommand(command, dispatch) { - const { - id, - state - } = command; - - if (state === 'queued') { - return; - } - - const timeoutId = removeCommandTimeoutIds[id]; - - if (timeoutId) { - clearTimeout(timeoutId); - } - - removeCommandTimeoutIds[id] = setTimeout(() => { - dispatch(batchActions([ - removeCommand({ section: 'commands', id }), - hideMessage({ id }) - ])); - - delete removeCommandTimeoutIds[id]; - }, 30000); -} - -const commandActionHandlers = { - [types.FETCH_COMMANDS]: createFetchHandler('commands', '/command'), - - [types.EXECUTE_COMMAND](payload) { - return (dispatch, getState) => { - // TODO: show a message for the user - if (lastCommand && isSameCommand(lastCommand, payload)) { - console.warn('Please wait at least 5 seconds before running this command again'); - } - - lastCommand = payload; - - // clear last command after 5 seconds. - if (lastCommandTimeout) { - clearTimeout(lastCommandTimeout); - } - - lastCommandTimeout = setTimeout(() => { - lastCommand = null; - }, 5000); - - const promise = $.ajax({ - url: '/command', - method: 'POST', - data: JSON.stringify(payload) - }); - - promise.done((data) => { - dispatch(addCommand(data)); - }); - }; - }, - - [types.UPDATE_COMMAND](payload) { - return (dispatch, getState) => { - dispatch(updateItem({ section: 'commands', ...payload })); - - showCommandMessage(payload, dispatch); - scheduleRemoveCommand(payload, dispatch); - }; - }, - - [types.FINISH_COMMAND](payload) { - return (dispatch, getState) => { - const state = getState(); - const handlers = state.commands.handlers; - Object.keys(handlers).forEach((key) => { - const handler = handlers[key]; - - if (handler.name === payload.name) { - dispatch(handler.handler(payload)); - } - }); - - dispatch(removeCommand({ section: 'commands', ...payload })); - showCommandMessage(payload, dispatch); - }; - } - -}; - -export default commandActionHandlers; diff --git a/frontend/src/Store/Actions/commandActions.js b/frontend/src/Store/Actions/commandActions.js index 84b6d4fdb..f5c08270f 100644 --- a/frontend/src/Store/Actions/commandActions.js +++ b/frontend/src/Store/Actions/commandActions.js @@ -1,14 +1,205 @@ +import _ from 'lodash'; +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import commandActionHandlers from './commandActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import { isSameCommand } from 'Utilities/Command'; +import { messageTypes } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import { showMessage, hideMessage } from './appActions'; +import { updateItem } from './baseActions'; -export const fetchCommands = commandActionHandlers[types.FETCH_COMMANDS]; -export const executeCommand = commandActionHandlers[types.EXECUTE_COMMAND]; -export const updateCommand = commandActionHandlers[types.UPDATE_COMMAND]; -export const finishCommand = commandActionHandlers[types.FINISH_COMMAND]; +// +// Variables -export const addCommand = createAction(types.ADD_COMMAND); -export const removeCommand = createAction(types.REMOVE_COMMAND); +export const section = 'commands'; -export const registerFinishCommandHandler = createAction(types.REGISTER_FINISH_COMMAND_HANDLER); -export const unregisterFinishCommandHandler = createAction(types.UNREGISTER_FINISH_COMMAND_HANDLER); +let lastCommand = null; +let lastCommandTimeout = null; +const removeCommandTimeoutIds = {}; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + handlers: {} +}; + +// +// Actions Types + +export const FETCH_COMMANDS = 'commands/fetchCommands'; +export const EXECUTE_COMMAND = 'commands/executeCommand'; +export const ADD_COMMAND = 'commands/updateCommand'; +export const UPDATE_COMMAND = 'commands/finishCommand'; +export const FINISH_COMMAND = 'commands/addCommand'; +export const REMOVE_COMMAND = 'commands/removeCommand'; + +// +// Action Creators + +export const fetchCommands = createThunk(FETCH_COMMANDS); +export const executeCommand = createThunk(EXECUTE_COMMAND); +export const updateCommand = createThunk(UPDATE_COMMAND); +export const finishCommand = createThunk(FINISH_COMMAND); +export const addCommand = createAction(ADD_COMMAND); +export const removeCommand = createAction(REMOVE_COMMAND); + +// +// Helpers + +function showCommandMessage(payload, dispatch) { + const { + id, + name, + manual, + message, + body = {}, + state + } = payload; + + const { + sendUpdatesToClient, + suppressMessages + } = body; + + if (!message || !body || !sendUpdatesToClient || suppressMessages) { + return; + } + + let type = messageTypes.INFO; + let hideAfter = 0; + + if (state === 'completed') { + type = messageTypes.SUCCESS; + hideAfter = 4; + } else if (state === 'failed') { + type = messageTypes.ERROR; + hideAfter = manual ? 10 : 4; + } + + dispatch(showMessage({ + id, + name, + message, + type, + hideAfter + })); +} + +function scheduleRemoveCommand(command, dispatch) { + const { + id, + state + } = command; + + if (state === 'queued') { + return; + } + + const timeoutId = removeCommandTimeoutIds[id]; + + if (timeoutId) { + clearTimeout(timeoutId); + } + + removeCommandTimeoutIds[id] = setTimeout(() => { + dispatch(batchActions([ + removeCommand({ section: 'commands', id }), + hideMessage({ id }) + ])); + + delete removeCommandTimeoutIds[id]; + }, 30000); +} + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [FETCH_COMMANDS]: createFetchHandler('commands', '/command'), + + [EXECUTE_COMMAND]: function(getState, payload, dispatch) { + // TODO: show a message for the user + if (lastCommand && isSameCommand(lastCommand, payload)) { + console.warn('Please wait at least 5 seconds before running this command again'); + } + + lastCommand = payload; + + // clear last command after 5 seconds. + if (lastCommandTimeout) { + clearTimeout(lastCommandTimeout); + } + + lastCommandTimeout = setTimeout(() => { + lastCommand = null; + }, 5000); + + const promise = $.ajax({ + url: '/command', + method: 'POST', + data: JSON.stringify(payload) + }); + + promise.done((data) => { + dispatch(addCommand(data)); + }); + }, + + [UPDATE_COMMAND]: function(getState, payload, dispatch) { + dispatch(updateItem({ section: 'commands', ...payload })); + + showCommandMessage(payload, dispatch); + scheduleRemoveCommand(payload, dispatch); + }, + + [FINISH_COMMAND]: function(getState, payload, dispatch) { + const state = getState(); + const handlers = state.commands.handlers; + + Object.keys(handlers).forEach((key) => { + const handler = handlers[key]; + + if (handler.name === payload.name) { + dispatch(handler.handler(payload)); + } + }); + + dispatch(removeCommand({ section: 'commands', ...payload })); + showCommandMessage(payload, dispatch); + } + +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [ADD_COMMAND]: (state, { payload }) => { + const newState = Object.assign({}, state); + newState.items = [...state.items, payload]; + + return newState; + }, + + [REMOVE_COMMAND]: (state, { payload }) => { + const newState = Object.assign({}, state); + newState.items = [...state.items]; + + const index = _.findIndex(newState.items, { id: payload.id }); + + if (index > -1) { + newState.items.splice(index, 1); + } + + return newState; + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/episodeActionHandlers.js b/frontend/src/Store/Actions/episodeActionHandlers.js deleted file mode 100644 index b74f5c3fa..000000000 --- a/frontend/src/Store/Actions/episodeActionHandlers.js +++ /dev/null @@ -1,111 +0,0 @@ -import _ from 'lodash'; -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import episodeEntities from 'Album/episodeEntities'; -import createFetchHandler from './Creators/createFetchHandler'; -import * as types from './actionTypes'; -import { updateItem } from './baseActions'; - -const section = 'episodes'; - -const episodeActionHandlers = { - [types.FETCH_EPISODES]: createFetchHandler(section, '/album'), - - [types.TOGGLE_EPISODE_MONITORED]: function(payload) { - return function(dispatch, getState) { - const { - albumId, - episodeEntity = episodeEntities.EPISODES, - monitored - } = payload; - - const episodeSection = _.last(episodeEntity.split('.')); - - dispatch(updateItem({ - id: albumId, - section: episodeSection, - isSaving: true - })); - - const promise = $.ajax({ - url: `/album/${albumId}`, - method: 'PUT', - data: JSON.stringify({ monitored }), - dataType: 'json' - }); - - promise.done((data) => { - dispatch(updateItem({ - id: albumId, - section: episodeSection, - isSaving: false, - monitored - })); - }); - - promise.fail((xhr) => { - dispatch(updateItem({ - id: albumId, - section: episodeSection, - isSaving: false - })); - }); - }; - }, - - [types.TOGGLE_EPISODES_MONITORED]: function(payload) { - return function(dispatch, getState) { - const { - albumIds, - episodeEntity = episodeEntities.EPISODES, - monitored - } = payload; - - const episodeSection = _.last(episodeEntity.split('.')); - - dispatch(batchActions( - albumIds.map((albumId) => { - return updateItem({ - id: albumId, - section: episodeSection, - isSaving: true - }); - }) - )); - - const promise = $.ajax({ - url: '/album/monitor', - method: 'PUT', - data: JSON.stringify({ albumIds, monitored }), - dataType: 'json' - }); - - promise.done((data) => { - dispatch(batchActions( - albumIds.map((albumId) => { - return updateItem({ - id: albumId, - section: episodeSection, - isSaving: false, - monitored - }); - }) - )); - }); - - promise.fail((xhr) => { - dispatch(batchActions( - albumIds.map((albumId) => { - return updateItem({ - id: albumId, - section: episodeSection, - isSaving: false - }); - }) - )); - }); - }; - } -}; - -export default episodeActionHandlers; diff --git a/frontend/src/Store/Actions/episodeActions.js b/frontend/src/Store/Actions/episodeActions.js index b0abe85eb..0e1859444 100644 --- a/frontend/src/Store/Actions/episodeActions.js +++ b/frontend/src/Store/Actions/episodeActions.js @@ -1,10 +1,222 @@ +import _ from 'lodash'; +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import episodeActionHandlers from './episodeActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; +import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; +import episodeEntities from 'Album/episodeEntities'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import { updateItem } from './baseActions'; -export const fetchEpisodes = episodeActionHandlers[types.FETCH_EPISODES]; -export const setEpisodesSort = createAction(types.SET_EPISODES_SORT); -export const setEpisodesTableOption = createAction(types.SET_EPISODES_TABLE_OPTION); -export const clearEpisodes = createAction(types.CLEAR_EPISODES); -export const toggleEpisodeMonitored = episodeActionHandlers[types.TOGGLE_EPISODE_MONITORED]; -export const toggleEpisodesMonitored = episodeActionHandlers[types.TOGGLE_EPISODES_MONITORED]; +// +// Variables + +export const section = 'episodes'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + sortKey: 'releaseDate', + sortDirection: sortDirections.DESCENDING, + items: [], + + columns: [ + { + name: 'monitored', + columnLabel: 'Monitored', + isVisible: true, + isModifiable: false + }, + { + name: 'title', + label: 'Title', + isVisible: true + }, + { + name: 'path', + label: 'Path', + isVisible: false + }, + { + name: 'releaseDate', + label: 'Release Date', + isVisible: true + }, + { + name: 'mediumCount', + label: 'Media Count', + isVisible: false + }, + { + name: 'trackCount', + label: 'Track Count', + isVisible: false + }, + { + name: 'duration', + label: 'Duration', + isVisible: false + }, + { + name: 'status', + label: 'Status', + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'episodes.columns' +]; + +// +// Actions Types + +export const FETCH_EPISODES = 'episodes/fetchEpisodes'; +export const SET_EPISODES_SORT = 'episodes/setEpisodesSort'; +export const SET_EPISODES_TABLE_OPTION = 'episodes/setEpisodesTableOption'; +export const CLEAR_EPISODES = 'episodes/clearEpisodes'; +export const TOGGLE_EPISODE_MONITORED = 'episodes/toggleEpisodeMonitored'; +export const TOGGLE_EPISODES_MONITORED = 'episodes/toggleEpisodesMonitored'; + +// +// Action Creators + +export const fetchEpisodes = createThunk(FETCH_EPISODES); +export const setEpisodesSort = createAction(SET_EPISODES_SORT); +export const setEpisodesTableOption = createAction(SET_EPISODES_TABLE_OPTION); +export const clearEpisodes = createAction(CLEAR_EPISODES); +export const toggleEpisodeMonitored = createThunk(TOGGLE_EPISODE_MONITORED); +export const toggleEpisodesMonitored = createThunk(TOGGLE_EPISODES_MONITORED); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [FETCH_EPISODES]: createFetchHandler(section, '/album'), + + [TOGGLE_EPISODE_MONITORED]: function(getState, payload, dispatch) { + const { + albumId, + episodeEntity = episodeEntities.EPISODES, + monitored + } = payload; + + const episodeSection = _.last(episodeEntity.split('.')); + + dispatch(updateItem({ + id: albumId, + section: episodeSection, + isSaving: true + })); + + const promise = $.ajax({ + url: `/album/${albumId}`, + method: 'PUT', + data: JSON.stringify({ monitored }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(updateItem({ + id: albumId, + section: episodeSection, + isSaving: false, + monitored + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + id: albumId, + section: episodeSection, + isSaving: false + })); + }); + }, + + [TOGGLE_EPISODES_MONITORED]: function(getState, payload, dispatch) { + const { + albumIds, + episodeEntity = episodeEntities.EPISODES, + monitored + } = payload; + + const episodeSection = _.last(episodeEntity.split('.')); + + dispatch(batchActions( + albumIds.map((albumId) => { + return updateItem({ + id: albumId, + section: episodeSection, + isSaving: true + }); + }) + )); + + const promise = $.ajax({ + url: '/album/monitor', + method: 'PUT', + data: JSON.stringify({ albumIds, monitored }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(batchActions( + albumIds.map((albumId) => { + return updateItem({ + id: albumId, + section: episodeSection, + isSaving: false, + monitored + }); + }) + )); + }); + + promise.fail((xhr) => { + dispatch(batchActions( + albumIds.map((albumId) => { + return updateItem({ + id: albumId, + section: episodeSection, + isSaving: false + }); + }) + )); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_EPISODES_TABLE_OPTION]: createSetTableOptionReducer(section), + + [CLEAR_EPISODES]: (state) => { + return Object.assign({}, state, { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }); + }, + + [SET_EPISODES_SORT]: createSetClientSideCollectionSortReducer(section) + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/historyActionHandlers.js b/frontend/src/Store/Actions/historyActionHandlers.js deleted file mode 100644 index 44cff0fc3..000000000 --- a/frontend/src/Store/Actions/historyActionHandlers.js +++ /dev/null @@ -1,60 +0,0 @@ -import $ from 'jquery'; -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; -import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; -import * as types from './actionTypes'; -import { updateItem } from './baseActions'; - -const section = 'history'; - -const historyActionHandlers = { - ...createServerSideCollectionHandlers('history', '/history', (state) => state.history, { - [serverSideCollectionHandlers.FETCH]: types.FETCH_HISTORY, - [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_HISTORY_PAGE, - [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_HISTORY_PAGE, - [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_HISTORY_PAGE, - [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_HISTORY_PAGE, - [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_HISTORY_PAGE, - [serverSideCollectionHandlers.SORT]: types.SET_HISTORY_SORT, - [serverSideCollectionHandlers.FILTER]: types.SET_HISTORY_FILTER - }), - - [types.MARK_AS_FAILED]: function(payload) { - return function(dispatch, getState) { - const id = payload.id; - - dispatch(updateItem({ - section, - id, - isMarkingAsFailed: true - })); - - const promise = $.ajax({ - url: '/history/failed', - method: 'POST', - data: { - id - } - }); - - promise.done(() => { - dispatch(updateItem({ - section, - id, - isMarkingAsFailed: false, - markAsFailedError: null - })); - }); - - promise.fail((xhr) => { - dispatch(updateItem({ - section, - id, - isMarkingAsFailed: false, - markAsFailedError: xhr - })); - }); - }; - } -}; - -export default historyActionHandlers; diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index d4fe71be9..1a346459e 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -1,16 +1,202 @@ +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import historyActionHandlers from './historyActionHandlers'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createClearReducer from './Creators/Reducers/createClearReducer'; +import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; +import createHandleActions from './Creators/createHandleActions'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; +import { updateItem } from './baseActions'; -export const fetchHistory = historyActionHandlers[types.FETCH_HISTORY]; -export const gotoHistoryFirstPage = historyActionHandlers[types.GOTO_FIRST_HISTORY_PAGE]; -export const gotoHistoryPreviousPage = historyActionHandlers[types.GOTO_PREVIOUS_HISTORY_PAGE]; -export const gotoHistoryNextPage = historyActionHandlers[types.GOTO_NEXT_HISTORY_PAGE]; -export const gotoHistoryLastPage = historyActionHandlers[types.GOTO_LAST_HISTORY_PAGE]; -export const gotoHistoryPage = historyActionHandlers[types.GOTO_HISTORY_PAGE]; -export const setHistorySort = historyActionHandlers[types.SET_HISTORY_SORT]; -export const setHistoryFilter = historyActionHandlers[types.SET_HISTORY_FILTER]; -export const setHistoryTableOption = createAction(types.SET_HISTORY_TABLE_OPTION); -export const clearHistory = createAction(types.CLEAR_HISTORY); +// +// Variables -export const markAsFailed = historyActionHandlers[types.MARK_AS_FAILED]; +export const section = 'history'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + pageSize: 20, + sortKey: 'date', + sortDirection: sortDirections.DESCENDING, + filterKey: null, + filterValue: null, + items: [], + + columns: [ + { + name: 'eventType', + columnLabel: 'Event Type', + isVisible: true, + isModifiable: false + }, + { + name: 'artist.sortName', + label: 'Artist', + isSortable: true, + isVisible: true + }, + { + name: 'episodeTitle', + label: 'Album Title', + isVisible: true + }, + { + name: 'trackTitle', + label: 'Track Title', + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: false + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + }, + { + name: 'date', + label: 'Date', + isSortable: true, + isVisible: true + }, + { + name: 'downloadClient', + label: 'Download Client', + isVisible: false + }, + { + name: 'indexer', + label: 'Indexer', + isVisible: false + }, + { + name: 'releaseGroup', + label: 'Release Group', + isVisible: false + }, + { + name: 'details', + columnLabel: 'Details', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'history.pageSize', + 'history.sortKey', + 'history.sortDirection', + 'history.filterKey', + 'history.filterValue' +]; + +// +// Actions Types + +export const FETCH_HISTORY = 'history/fetchHistory'; +export const GOTO_FIRST_HISTORY_PAGE = 'history/gotoHistoryFirstPage'; +export const GOTO_PREVIOUS_HISTORY_PAGE = 'history/gotoHistoryPreviousPage'; +export const GOTO_NEXT_HISTORY_PAGE = 'history/gotoHistoryNextPage'; +export const GOTO_LAST_HISTORY_PAGE = 'history/gotoHistoryLastPage'; +export const GOTO_HISTORY_PAGE = 'history/gotoHistoryPage'; +export const SET_HISTORY_SORT = 'history/setHistorySort'; +export const SET_HISTORY_FILTER = 'history/setHistoryFilter'; +export const SET_HISTORY_TABLE_OPTION = 'history/setHistoryTableOption'; +export const CLEAR_HISTORY = 'history/clearHistory'; +export const MARK_AS_FAILED = 'history/markAsFailed'; + +// +// Action Creators + +export const fetchHistory = createThunk(FETCH_HISTORY); +export const gotoHistoryFirstPage = createThunk(GOTO_FIRST_HISTORY_PAGE); +export const gotoHistoryPreviousPage = createThunk(GOTO_PREVIOUS_HISTORY_PAGE); +export const gotoHistoryNextPage = createThunk(GOTO_NEXT_HISTORY_PAGE); +export const gotoHistoryLastPage = createThunk(GOTO_LAST_HISTORY_PAGE); +export const gotoHistoryPage = createThunk(GOTO_HISTORY_PAGE); +export const setHistorySort = createThunk(SET_HISTORY_SORT); +export const setHistoryFilter = createThunk(SET_HISTORY_FILTER); +export const setHistoryTableOption = createAction(SET_HISTORY_TABLE_OPTION); +export const clearHistory = createAction(CLEAR_HISTORY); +export const markAsFailed = createThunk(MARK_AS_FAILED); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + ...createServerSideCollectionHandlers( + section, + '/history', + fetchHistory, + { + [serverSideCollectionHandlers.FETCH]: FETCH_HISTORY, + [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_HISTORY_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_HISTORY_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_HISTORY_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_HISTORY_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_HISTORY_PAGE, + [serverSideCollectionHandlers.SORT]: SET_HISTORY_SORT, + [serverSideCollectionHandlers.FILTER]: SET_HISTORY_FILTER + }), + + [MARK_AS_FAILED]: function(getState, payload, dispatch) { + const id = payload.id; + + dispatch(updateItem({ + section, + id, + isMarkingAsFailed: true + })); + + const promise = $.ajax({ + url: '/history/failed', + method: 'POST', + data: { + id + } + }); + + promise.done(() => { + dispatch(updateItem({ + section, + id, + isMarkingAsFailed: false, + markAsFailedError: null + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + section, + id, + isMarkingAsFailed: false, + markAsFailedError: xhr + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_HISTORY_TABLE_OPTION]: createSetTableOptionReducer(section), + + [CLEAR_HISTORY]: createClearReducer('history', { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }) + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/importArtistActionHandlers.js b/frontend/src/Store/Actions/importArtistActionHandlers.js deleted file mode 100644 index 2f27182bc..000000000 --- a/frontend/src/Store/Actions/importArtistActionHandlers.js +++ /dev/null @@ -1,172 +0,0 @@ -import _ from 'lodash'; -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import getNewArtist from 'Utilities/Artist/getNewArtist'; -import * as types from './actionTypes'; -import { set, updateItem, removeItem } from './baseActions'; -import { startLookupArtist } from './importArtistActions'; -import { fetchRootFolders } from './rootFolderActions'; - -const section = 'importArtist'; -let concurrentLookups = 0; - -const importArtistActionHandlers = { - [types.QUEUE_LOOKUP_ARTIST]: function(payload) { - return function(dispatch, getState) { - const { - name, - path, - term - } = payload; - - const state = getState().importArtist; - const item = _.find(state.items, { id: name }) || { - id: name, - term, - path, - isFetching: false, - isPopulated: false, - error: null - }; - - dispatch(updateItem({ - section, - ...item, - term, - queued: true, - items: [] - })); - - if (term && term.length > 2) { - dispatch(startLookupArtist()); - } - }; - }, - - [types.START_LOOKUP_ARTIST]: function(payload) { - return function(dispatch, getState) { - if (concurrentLookups >= 1) { - return; - } - - const state = getState().importArtist; - const queued = _.find(state.items, { queued: true }); - - if (!queued) { - return; - } - - concurrentLookups++; - - dispatch(updateItem({ - section, - id: queued.id, - isFetching: true - })); - - const promise = $.ajax({ - url: '/artist/lookup', - data: { - term: queued.term - } - }); - - promise.done((data) => { - dispatch(updateItem({ - section, - id: queued.id, - isFetching: false, - isPopulated: true, - error: null, - items: data, - queued: false, - selectedArtist: queued.selectedArtist || data[0] - })); - }); - - promise.fail((xhr) => { - dispatch(updateItem({ - section, - id: queued.id, - isFetching: false, - isPopulated: false, - error: xhr, - queued: false - })); - }); - - promise.always(() => { - concurrentLookups--; - dispatch(startLookupArtist()); - }); - }; - }, - - [types.IMPORT_ARTIST]: function(payload) { - return function(dispatch, getState) { - dispatch(set({ section, isImporting: true })); - - const ids = payload.ids; - const items = getState().importArtist.items; - const addedIds = []; - - const allNewSeries = ids.reduce((acc, id) => { - const item = _.find(items, { id }); - const selectedArtist = item.selectedArtist; - - // Make sure we have a selected artist and - // the same artist hasn't been added yet. - if (selectedArtist && !_.some(acc, { foreignArtistId: selectedArtist.foreignArtistId })) { - const newSeries = getNewArtist(_.cloneDeep(selectedArtist), item); - newSeries.path = item.path; - - addedIds.push(id); - acc.push(newSeries); - } - - return acc; - }, []); - - const promise = $.ajax({ - url: '/artist/import', - method: 'POST', - contentType: 'application/json', - data: JSON.stringify(allNewSeries) - }); - - promise.done((data) => { - dispatch(batchActions([ - set({ - section, - isImporting: false, - isImported: true - }), - - ...data.map((artist) => updateItem({ section: 'artist', ...artist })), - - ...addedIds.map((id) => removeItem({ section, id })) - ])); - - dispatch(fetchRootFolders()); - }); - - promise.fail((xhr) => { - dispatch(batchActions( - set({ - section, - isImporting: false, - isImported: true - }), - - addedIds.map((id) => updateItem({ - section, - id, - importError: xhr - })) - )); - }); - }; - } -}; - -export default importArtistActionHandlers; diff --git a/frontend/src/Store/Actions/importArtistActions.js b/frontend/src/Store/Actions/importArtistActions.js index 7bbf8a78b..ad4fbf0f3 100644 --- a/frontend/src/Store/Actions/importArtistActions.js +++ b/frontend/src/Store/Actions/importArtistActions.js @@ -1,16 +1,241 @@ +import _ from 'lodash'; +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import importArtistActionHandlers from './importArtistActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import getNewArtist from 'Utilities/Artist/getNewArtist'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createHandleActions from './Creators/createHandleActions'; +import { set, removeItem, updateItem } from './baseActions'; +import { fetchRootFolders } from './rootFolderActions'; -export const queueLookupArtist = importArtistActionHandlers[types.QUEUE_LOOKUP_ARTIST]; -export const startLookupArtist = importArtistActionHandlers[types.START_LOOKUP_ARTIST]; -export const importArtist = importArtistActionHandlers[types.IMPORT_ARTIST]; -export const clearImportArtist = createAction(types.CLEAR_IMPORT_ARTIST); +// +// Variables -export const setImportArtistValue = createAction(types.SET_IMPORT_ARTIST_VALUE, (payload) => { +export const section = 'importArtist'; +let concurrentLookups = 0; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isImporting: false, + isImported: false, + importError: null, + items: [] +}; + +// +// Actions Types + +export const QUEUE_LOOKUP_ARTIST = 'importArtist/queueLookupArtist'; +export const START_LOOKUP_ARTIST = 'importArtist/startLookupArtist'; +export const CLEAR_IMPORT_ARTIST = 'importArtist/importArtist'; +export const SET_IMPORT_ARTIST_VALUE = 'importArtist/clearImportArtist'; +export const IMPORT_ARTIST = 'importArtist/setImportArtistValue'; + +// +// Action Creators + +export const queueLookupArtist = createThunk(QUEUE_LOOKUP_ARTIST); +export const startLookupArtist = createThunk(START_LOOKUP_ARTIST); +export const importArtist = createThunk(IMPORT_ARTIST); +export const clearImportArtist = createAction(CLEAR_IMPORT_ARTIST); + +export const setImportArtistValue = createAction(SET_IMPORT_ARTIST_VALUE, (payload) => { return { - section: 'importArtist', + section, ...payload }; }); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [QUEUE_LOOKUP_ARTIST]: function(getState, payload, dispatch) { + const { + name, + path, + term + } = payload; + + const state = getState().importArtist; + const item = _.find(state.items, { id: name }) || { + id: name, + term, + path, + isFetching: false, + isPopulated: false, + error: null + }; + + dispatch(updateItem({ + section, + ...item, + term, + queued: true, + items: [] + })); + + if (term && term.length > 2) { + dispatch(startLookupArtist()); + } + }, + + [START_LOOKUP_ARTIST]: function(getState, payload, dispatch) { + if (concurrentLookups >= 1) { + return; + } + + const state = getState().importArtist; + const queued = _.find(state.items, { queued: true }); + + if (!queued) { + return; + } + + concurrentLookups++; + + dispatch(updateItem({ + section, + id: queued.id, + isFetching: true + })); + + const promise = $.ajax({ + url: '/artist/lookup', + data: { + term: queued.term + } + }); + + promise.done((data) => { + dispatch(updateItem({ + section, + id: queued.id, + isFetching: false, + isPopulated: true, + error: null, + items: data, + queued: false, + selectedArtist: queued.selectedArtist || data[0] + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + section, + id: queued.id, + isFetching: false, + isPopulated: false, + error: xhr, + queued: false + })); + }); + + promise.always(() => { + concurrentLookups--; + dispatch(startLookupArtist()); + }); + }, + + [IMPORT_ARTIST]: function(getState, payload, dispatch) { + dispatch(set({ section, isImporting: true })); + + const ids = payload.ids; + const items = getState().importArtist.items; + const addedIds = []; + + const allNewArtist = ids.reduce((acc, id) => { + const item = _.find(items, { id }); + const selectedArtist = item.selectedArtist; + + // Make sure we have a selected artist and + // the same artist hasn't been added yet. + if (selectedArtist && !_.some(acc, { tvdbId: selectedArtist.tvdbId })) { + const newArtist = getNewArtist(_.cloneDeep(selectedArtist), item); + newArtist.path = item.path; + + addedIds.push(id); + acc.push(newArtist); + } + + return acc; + }, []); + + const promise = $.ajax({ + url: '/artist/import', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(allNewArtist) + }); + + promise.done((data) => { + dispatch(batchActions([ + set({ + section, + isImporting: false, + isImported: true + }), + + ...data.map((artist) => updateItem({ section: 'artist', ...artist })), + + ...addedIds.map((id) => removeItem({ section, id })) + ])); + + dispatch(fetchRootFolders()); + }); + + promise.fail((xhr) => { + dispatch(batchActions( + set({ + section, + isImporting: false, + isImported: true + }), + + addedIds.map((id) => updateItem({ + section, + id, + importError: xhr + })) + )); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [CLEAR_IMPORT_ARTIST]: function(state) { + return Object.assign({}, state, defaultState); + }, + + [SET_IMPORT_ARTIST_VALUE]: function(state, { payload }) { + const newState = getSectionState(state, section); + const items = newState.items; + const index = _.findIndex(items, { id: payload.id }); + + newState.items = [...items]; + + if (index >= 0) { + const item = items[index]; + + newState.items.splice(index, 1, { ...item, ...payload }); + } else { + newState.items.push({ ...payload }); + } + + return updateSectionState(state, section, newState); + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js new file mode 100644 index 000000000..46a7eff65 --- /dev/null +++ b/frontend/src/Store/Actions/index.js @@ -0,0 +1,57 @@ +import * as addArtist from './addArtistActions'; +import * as app from './appActions'; +import * as blacklist from './blacklistActions'; +import * as captcha from './captchaActions'; +import * as calendar from './calendarActions'; +import * as commands from './commandActions'; +import * as episodes from './episodeActions'; +import * as trackFiles from './trackFileActions'; +import * as albumHistory from './albumHistoryActions'; +import * as history from './historyActions'; +import * as importArtist from './importArtistActions'; +import * as interactiveImportActions from './interactiveImportActions'; +import * as oAuth from './oAuthActions'; +import * as organizePreview from './organizePreviewActions'; +import * as paths from './pathActions'; +import * as queue from './queueActions'; +import * as releases from './releaseActions'; +import * as rootFolders from './rootFolderActions'; +import * as albumStudio from './albumStudioActions'; +import * as artist from './artistActions'; +import * as artistEditor from './artistEditorActions'; +import * as artistIndex from './artistIndexActions'; +import * as settings from './settingsActions'; +import * as system from './systemActions'; +import * as tags from './tagActions'; +import * as tracks from './trackActions'; +import * as wanted from './wantedActions'; + +export default [ + addArtist, + app, + blacklist, + captcha, + calendar, + commands, + episodes, + trackFiles, + albumHistory, + history, + importArtist, + interactiveImportActions, + oAuth, + organizePreview, + paths, + queue, + releases, + rootFolders, + albumStudio, + artist, + artistEditor, + artistIndex, + settings, + system, + tags, + tracks, + wanted +]; diff --git a/frontend/src/Store/Actions/interactiveImportActionHandlers.js b/frontend/src/Store/Actions/interactiveImportActionHandlers.js deleted file mode 100644 index 811916bc0..000000000 --- a/frontend/src/Store/Actions/interactiveImportActionHandlers.js +++ /dev/null @@ -1,51 +0,0 @@ -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import createFetchHandler from './Creators/createFetchHandler'; -import * as types from './actionTypes'; -import { set, update } from './baseActions'; - -const section = 'interactiveImport'; - -const interactiveImportActionHandlers = { - [types.FETCH_INTERACTIVE_IMPORT_ITEMS]: function(payload) { - return function(dispatch, getState) { - if (!payload.downloadId && !payload.folder) { - dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } })); - return; - } - - dispatch(set({ section, isFetching: true })); - - const promise = $.ajax({ - url: '/manualimport', - data: payload - }); - - promise.done((data) => { - dispatch(batchActions([ - update({ section, data }), - - set({ - section, - isFetching: false, - isPopulated: true, - error: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isFetching: false, - isPopulated: false, - error: xhr - })); - }); - }; - }, - - [types.FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler('interactiveImportAlbums', '/album') -}; - -export default interactiveImportActionHandlers; diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index 2a8da6efb..eb1d8064a 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -1,15 +1,196 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import moment from 'moment'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import interactiveImportActionHandlers from './interactiveImportActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import { createThunk, handleThunks } from 'Store/thunks'; +import { sortDirections } from 'Helpers/Props'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import { set, update } from './baseActions'; -export const fetchInteractiveImportItems = interactiveImportActionHandlers[types.FETCH_INTERACTIVE_IMPORT_ITEMS]; -export const setInteractiveImportSort = createAction(types.SET_INTERACTIVE_IMPORT_SORT); -export const updateInteractiveImportItem = createAction(types.UPDATE_INTERACTIVE_IMPORT_ITEM); -export const clearInteractiveImport = createAction(types.CLEAR_INTERACTIVE_IMPORT); -export const addRecentFolder = createAction(types.ADD_RECENT_FOLDER); -export const removeRecentFolder = createAction(types.REMOVE_RECENT_FOLDER); -export const setInteractiveImportMode = createAction(types.SET_INTERACTIVE_IMPORT_MODE); +// +// Variables -export const fetchInteractiveImportAlbums = interactiveImportActionHandlers[types.FETCH_INTERACTIVE_IMPORT_ALBUMS]; -export const setInteractiveImportAlbumsSort = createAction(types.SET_INTERACTIVE_IMPORT_ALBUMS_SORT); -export const clearInteractiveImportAlbums = createAction(types.CLEAR_INTERACTIVE_IMPORT_ALBUMS); +export const section = 'interactiveImport'; + +const albumsSection = `${section}.albums`; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + sortKey: 'quality', + sortDirection: sortDirections.DESCENDING, + recentFolders: [], + importMode: 'move', + sortPredicates: { + artist: function(item, direction) { + const artist = item.artist; + + return artist ? artist.sortName : ''; + }, + + quality: function(item, direction) { + return item.quality.qualityWeight; + } + }, + + albums: { + isFetching: false, + isPopulated: false, + error: null, + sortKey: 'albumTitle', + sortDirection: sortDirections.DESCENDING, + items: [] + } +}; + +export const persistState = [ + 'interactiveImport.recentFolders', + 'interactiveImport.importMode' +]; + +// +// Actions Types + +export const FETCH_INTERACTIVE_IMPORT_ITEMS = 'FETCH_INTERACTIVE_IMPORT_ITEMS'; +export const UPDATE_INTERACTIVE_IMPORT_ITEM = 'UPDATE_INTERACTIVE_IMPORT_ITEM'; +export const SET_INTERACTIVE_IMPORT_SORT = 'SET_INTERACTIVE_IMPORT_SORT'; +export const CLEAR_INTERACTIVE_IMPORT = 'CLEAR_INTERACTIVE_IMPORT'; +export const ADD_RECENT_FOLDER = 'ADD_RECENT_FOLDER'; +export const REMOVE_RECENT_FOLDER = 'REMOVE_RECENT_FOLDER'; +export const SET_INTERACTIVE_IMPORT_MODE = 'SET_INTERACTIVE_IMPORT_MODE'; + +export const FETCH_INTERACTIVE_IMPORT_ALBUMS = 'FETCH_INTERACTIVE_IMPORT_ALBUMS'; +export const SET_INTERACTIVE_IMPORT_ALBUMS_SORT = 'SET_INTERACTIVE_IMPORT_ALBUMS_SORT'; +export const CLEAR_INTERACTIVE_IMPORT_ALBUMS = 'CLEAR_INTERACTIVE_IMPORT_ALBUMS'; + +// +// Action Creators + +export const fetchInteractiveImportItems = createThunk(FETCH_INTERACTIVE_IMPORT_ITEMS); +export const setInteractiveImportSort = createAction(SET_INTERACTIVE_IMPORT_SORT); +export const updateInteractiveImportItem = createAction(UPDATE_INTERACTIVE_IMPORT_ITEM); +export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT); +export const addRecentFolder = createAction(ADD_RECENT_FOLDER); +export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER); +export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE); + +export const fetchInteractiveImportAlbums = createThunk(FETCH_INTERACTIVE_IMPORT_ALBUMS); +export const setInteractiveImportAlbumsSort = createAction(SET_INTERACTIVE_IMPORT_ALBUMS_SORT); +export const clearInteractiveImportAlbums = createAction(CLEAR_INTERACTIVE_IMPORT_ALBUMS); + +// +// Action Handlers +export const actionHandlers = handleThunks({ + [FETCH_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) { + if (!payload.downloadId && !payload.folder) { + dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } })); + return; + } + + dispatch(set({ section, isFetching: true })); + + const promise = $.ajax({ + url: '/manualimport', + data: payload + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }, + + [FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler('interactiveImport.albums', '/album') +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [UPDATE_INTERACTIVE_IMPORT_ITEM]: (state, { payload }) => { + const id = payload.id; + const newState = Object.assign({}, state); + const items = newState.items; + const index = _.findIndex(items, { id }); + const item = Object.assign({}, items[index], payload); + + newState.items = [...items]; + newState.items.splice(index, 1, item); + + return newState; + }, + + [ADD_RECENT_FOLDER]: function(state, { payload }) { + const folder = payload.folder; + const recentFolder = { folder, lastUsed: moment().toISOString() }; + const recentFolders = [...state.recentFolders]; + const index = _.findIndex(recentFolders, { folder }); + + if (index > -1) { + recentFolders.splice(index, 1, recentFolder); + } else { + recentFolders.push(recentFolder); + } + + return Object.assign({}, state, { recentFolders }); + }, + + [REMOVE_RECENT_FOLDER]: function(state, { payload }) { + const folder = payload.folder; + const recentFolders = _.remove([...state.recentFolders], { folder }); + + return Object.assign({}, state, { recentFolders }); + }, + + [CLEAR_INTERACTIVE_IMPORT]: function(state) { + const newState = { + ...defaultState, + recentFolders: state.recentFolders, + importMode: state.importMode + }; + + return newState; + }, + + [SET_INTERACTIVE_IMPORT_SORT]: createSetClientSideCollectionSortReducer(section), + + [SET_INTERACTIVE_IMPORT_MODE]: function(state, { payload }) { + return Object.assign({}, state, { importMode: payload.importMode }); + }, + + [SET_INTERACTIVE_IMPORT_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(albumsSection), + + [CLEAR_INTERACTIVE_IMPORT_ALBUMS]: (state) => { + return updateSectionState(state, albumsSection, { + ...defaultState.albums + }); + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/oAuthActionHandlers.js b/frontend/src/Store/Actions/oAuthActionHandlers.js deleted file mode 100644 index aa2d5e038..000000000 --- a/frontend/src/Store/Actions/oAuthActionHandlers.js +++ /dev/null @@ -1,81 +0,0 @@ -/* eslint callback-return: 0 */ -import _ from 'lodash'; -import $ from 'jquery'; -import requestAction from 'Utilities/requestAction'; -import * as types from './actionTypes'; -import { setOAuthValue } from './oAuthActions'; - -function showOAuthWindow(url) { - const deferred = $.Deferred(); - const selfWindow = window; - - window.open(url); - - selfWindow.onCompleteOauth = function(query, callback) { - delete selfWindow.onCompleteOauth; - - const queryParams = {}; - const splitQuery = query.substring(1).split('&'); - - _.each(splitQuery, (param) => { - const paramSplit = param.split('='); - - queryParams[paramSplit[0]] = paramSplit[1]; - }); - - callback(); - deferred.resolve(queryParams); - }; - - return deferred.promise(); -} - -const oAuthActionHandlers = { - - [types.START_OAUTH]: function(payload) { - return (dispatch, getState) => { - const actionPayload = { - action: 'startOAuth', - queryParams: { callbackUrl: `${window.location.origin}/oauth.html` }, - ...payload - }; - - dispatch(setOAuthValue({ - authorizing: true - })); - - const promise = requestAction(actionPayload) - .then((response) => { - return showOAuthWindow(response.oauthUrl); - }) - .then((queryParams) => { - return requestAction({ - action: 'getOAuthToken', - queryParams, - ...payload - }); - }) - .then((response) => { - const { - accessToken, - accessTokenSecret - } = response; - - dispatch(setOAuthValue({ - authorizing: false, - accessToken, - accessTokenSecret - })); - }); - - promise.fail(() => { - dispatch(setOAuthValue({ - authorizing: false - })); - }); - }; - } - -}; - -export default oAuthActionHandlers; diff --git a/frontend/src/Store/Actions/oAuthActions.js b/frontend/src/Store/Actions/oAuthActions.js index 93de6f11f..39a177cd8 100644 --- a/frontend/src/Store/Actions/oAuthActions.js +++ b/frontend/src/Store/Actions/oAuthActions.js @@ -1,7 +1,129 @@ +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import oAuthActionHandlers from './oAuthActionHandlers'; +import requestAction from 'Utilities/requestAction'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createHandleActions from './Creators/createHandleActions'; -export const startOAuth = oAuthActionHandlers[types.START_OAUTH]; -export const setOAuthValue = createAction(types.SET_OAUTH_VALUE); -export const resetOAuth = createAction(types.RESET_OAUTH); +// +// Variables + +export const section = 'oAuth'; + +// +// State + +export const defaultState = { + authorizing: false, + accessToken: null, + accessTokenSecret: null +}; + +// +// Actions Types + +export const START_OAUTH = 'oAuth/startOAuth'; +export const SET_OAUTH_VALUE = 'oAuth/setOAuthValue'; +export const RESET_OAUTH = 'oAuth/resetOAuth'; + +// +// Action Creators + +export const startOAuth = createThunk(START_OAUTH); +export const setOAuthValue = createAction(SET_OAUTH_VALUE); +export const resetOAuth = createAction(RESET_OAUTH); + +// +// Helpers + +function showOAuthWindow(url) { + const deferred = $.Deferred(); + const selfWindow = window; + + window.open(url); + + selfWindow.onCompleteOauth = function(query, onComplete) { + delete selfWindow.onCompleteOauth; + + const queryParams = {}; + const splitQuery = query.substring(1).split('&'); + + splitQuery.forEach((param) => { + const paramSplit = param.split('='); + + queryParams[paramSplit[0]] = paramSplit[1]; + }); + + onComplete(); + deferred.resolve(queryParams); + }; + + return deferred.promise(); +} + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [START_OAUTH]: function(getState, payload, dispatch) { + const actionPayload = { + action: 'startOAuth', + queryParams: { callbackUrl: `${window.location.origin}/oauth.html` }, + ...payload + }; + + dispatch(setOAuthValue({ + authorizing: true + })); + + const promise = requestAction(actionPayload) + .then((response) => { + return showOAuthWindow(response.oauthUrl); + }) + .then((queryParams) => { + return requestAction({ + action: 'getOAuthToken', + queryParams, + ...payload + }); + }) + .then((response) => { + const { + accessToken, + accessTokenSecret + } = response; + + dispatch(setOAuthValue({ + authorizing: false, + accessToken, + accessTokenSecret + })); + }); + + promise.fail(() => { + dispatch(setOAuthValue({ + authorizing: false + })); + }); + } + +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_OAUTH_VALUE]: function(state, { payload }) { + const newState = Object.assign(getSectionState(state, section), payload); + + return updateSectionState(state, section, newState); + }, + + [RESET_OAUTH]: function(state) { + return updateSectionState(state, section, defaultState); + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/organizePreviewActionHandlers.js b/frontend/src/Store/Actions/organizePreviewActionHandlers.js deleted file mode 100644 index d0901b5ea..000000000 --- a/frontend/src/Store/Actions/organizePreviewActionHandlers.js +++ /dev/null @@ -1,8 +0,0 @@ -import createFetchHandler from './Creators/createFetchHandler'; -import * as types from './actionTypes'; - -const organizePreviewActionHandlers = { - [types.FETCH_ORGANIZE_PREVIEW]: createFetchHandler('organizePreview', '/rename') -}; - -export default organizePreviewActionHandlers; diff --git a/frontend/src/Store/Actions/organizePreviewActions.js b/frontend/src/Store/Actions/organizePreviewActions.js index 602028ff4..78f943f32 100644 --- a/frontend/src/Store/Actions/organizePreviewActions.js +++ b/frontend/src/Store/Actions/organizePreviewActions.js @@ -1,6 +1,51 @@ import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import organizePreviewActionHandlers from './organizePreviewActionHandlers'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; -export const fetchOrganizePreview = organizePreviewActionHandlers[types.FETCH_ORGANIZE_PREVIEW]; -export const clearOrganizePreview = createAction(types.CLEAR_ORGANIZE_PREVIEW); +// +// Variables + +export const section = 'organizePreview'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [] +}; + +// +// Actions Types + +export const FETCH_ORGANIZE_PREVIEW = 'organizePreview/fetchOrganizePreview'; +export const CLEAR_ORGANIZE_PREVIEW = 'organizePreview/clearOrganizePreview'; + +// +// Action Creators + +export const fetchOrganizePreview = createThunk(FETCH_ORGANIZE_PREVIEW); +export const clearOrganizePreview = createAction(CLEAR_ORGANIZE_PREVIEW); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [FETCH_ORGANIZE_PREVIEW]: createFetchHandler('organizePreview', '/rename') + +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [CLEAR_ORGANIZE_PREVIEW]: (state) => { + return Object.assign({}, state, defaultState); + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/pathActionHandlers.js b/frontend/src/Store/Actions/pathActionHandlers.js deleted file mode 100644 index 11ebfaf21..000000000 --- a/frontend/src/Store/Actions/pathActionHandlers.js +++ /dev/null @@ -1,43 +0,0 @@ -import $ from 'jquery'; -import * as types from './actionTypes'; -import { set } from './baseActions'; -import { updatePaths } from './pathActions'; - -const section = 'paths'; - -const pathActionHandlers = { - [types.FETCH_PATHS](payload) { - return (dispatch, getState) => { - dispatch(set({ section, isFetching: true })); - - const promise = $.ajax({ - url: '/filesystem', - data: { - path: payload.path - } - }); - - promise.done((data) => { - dispatch(updatePaths({ path: payload.path, ...data })); - - dispatch(set({ - section, - isFetching: false, - isPopulated: true, - error: null - })); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isFetching: false, - isPopulated: false, - error: xhr - })); - }); - }; - } -}; - -export default pathActionHandlers; diff --git a/frontend/src/Store/Actions/pathActions.js b/frontend/src/Store/Actions/pathActions.js index fd4e76f02..ad1817cb0 100644 --- a/frontend/src/Store/Actions/pathActions.js +++ b/frontend/src/Store/Actions/pathActions.js @@ -1,7 +1,104 @@ +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import pathActionHandlers from './pathActionHandlers'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createHandleActions from './Creators/createHandleActions'; +import { set } from './baseActions'; -export const fetchPaths = pathActionHandlers[types.FETCH_PATHS]; -export const updatePaths = createAction(types.UPDATE_PATHS); -export const clearPaths = createAction(types.CLEAR_PATHS); +// +// Variables + +export const section = 'paths'; + +// +// State + +export const defaultState = { + currentPath: '', + isPopulated: false, + isFetching: false, + error: null, + directories: [], + files: [], + parent: null +}; + +// +// Actions Types + +export const FETCH_PATHS = 'paths/fetchPaths'; +export const UPDATE_PATHS = 'paths/updatePaths'; +export const CLEAR_PATHS = 'paths/clearPaths'; + +// +// Action Creators + +export const fetchPaths = createThunk(FETCH_PATHS); +export const updatePaths = createAction(UPDATE_PATHS); +export const clearPaths = createAction(CLEAR_PATHS); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [FETCH_PATHS]: function(getState, payload, dispatch) { + dispatch(set({ section, isFetching: true })); + + const promise = $.ajax({ + url: '/filesystem', + data: { + path: payload.path + } + }); + + promise.done((data) => { + dispatch(updatePaths({ path: payload.path, ...data })); + + dispatch(set({ + section, + isFetching: false, + isPopulated: true, + error: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + } + +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [UPDATE_PATHS]: (state, { payload }) => { + const newState = Object.assign({}, state); + + newState.currentPath = payload.path; + newState.directories = payload.directories; + newState.files = payload.files; + newState.parent = payload.parent; + + return newState; + }, + + [CLEAR_PATHS]: (state, { payload }) => { + const newState = Object.assign({}, state); + + newState.path = ''; + newState.directories = []; + newState.files = []; + newState.parent = ''; + + return newState; + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/queueActionHandlers.js b/frontend/src/Store/Actions/queueActionHandlers.js deleted file mode 100644 index 494942b61..000000000 --- a/frontend/src/Store/Actions/queueActionHandlers.js +++ /dev/null @@ -1,233 +0,0 @@ -import _ from 'lodash'; -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; -import createFetchHandler from './Creators/createFetchHandler'; -import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; -import * as types from './actionTypes'; -import { set, updateItem } from './baseActions'; -import { fetchQueue } from './queueActions'; - -const fetchQueueDetailsHandler = createFetchHandler('details', '/queue/details'); - -const queueActionHandlers = { - [types.FETCH_QUEUE_STATUS]: createFetchHandler('queueStatus', '/queue/status'), - - [types.FETCH_QUEUE_DETAILS]: function(payload) { - return function(dispatch, getState) { - let params = payload; - - // If the payload params are empty try to get params from state. - - if (params && !_.isEmpty(params)) { - dispatch(set({ section: 'details', params })); - } else { - params = getState().queue.details.params; - } - - // Ensure there are params before trying to fetch the queue - // so we don't make a bad request to the server. - - if (params && !_.isEmpty(params)) { - const fetchFunction = fetchQueueDetailsHandler(params); - fetchFunction(dispatch, getState); - } - }; - }, - - ...createServerSideCollectionHandlers('paged', '/queue', (state) => state.queue, { - [serverSideCollectionHandlers.FETCH]: types.FETCH_QUEUE, - [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_QUEUE_PAGE, - [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_QUEUE_PAGE, - [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_QUEUE_PAGE, - [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_QUEUE_PAGE, - [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_QUEUE_PAGE, - [serverSideCollectionHandlers.SORT]: types.SET_QUEUE_SORT - }), - - [types.GRAB_QUEUE_ITEM]: function(payload) { - const section = 'paged'; - - const { - id - } = payload; - - return function(dispatch, getState) { - dispatch(updateItem({ section, id, isGrabbing: true })); - - const promise = $.ajax({ - url: `/queue/grab/${id}`, - method: 'POST' - }); - - promise.done((data) => { - dispatch(batchActions([ - dispatch(fetchQueue()), - - set({ - section, - isGrabbing: false, - grabError: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(updateItem({ - section, - id, - isGrabbing: false, - grabError: xhr - })); - }); - }; - }, - - [types.GRAB_QUEUE_ITEMS]: function(payload) { - const section = 'paged'; - - const { - ids - } = payload; - - return function(dispatch, getState) { - dispatch(batchActions([ - ...ids.map((id) => { - return updateItem({ - section, - id, - isGrabbing: true - }); - }), - - set({ - section, - isGrabbing: true - }) - ])); - - const promise = $.ajax({ - url: '/queue/grab/bulk', - method: 'POST', - dataType: 'json', - data: JSON.stringify(payload) - }); - - promise.done((data) => { - dispatch(batchActions([ - dispatch(fetchQueue()), - - ...ids.map((id) => { - return updateItem({ - section, - id, - isGrabbing: false, - grabError: null - }); - }), - - set({ - section, - isGrabbing: false, - grabError: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(batchActions([ - ...ids.map((id) => { - return updateItem({ - section, - id, - isGrabbing: false, - grabError: null - }); - }), - - set({ section, isGrabbing: false }) - ])); - }); - }; - }, - - [types.REMOVE_QUEUE_ITEM]: function(payload) { - const section = 'paged'; - - const { - id, - blacklist - } = payload; - - return function(dispatch, getState) { - dispatch(updateItem({ section, id, isRemoving: true })); - - const promise = $.ajax({ - url: `/queue/${id}?blacklist=${blacklist}`, - method: 'DELETE' - }); - - promise.done((data) => { - dispatch(fetchQueue()); - }); - - promise.fail((xhr) => { - dispatch(updateItem({ section, id, isRemoving: false })); - }); - }; - }, - - [types.REMOVE_QUEUE_ITEMS]: function(payload) { - const section = 'paged'; - - const { - ids, - blacklist - } = payload; - - return function(dispatch, getState) { - dispatch(batchActions([ - ...ids.map((id) => { - return updateItem({ - section, - id, - isRemoving: true - }); - }), - - set({ section, isRemoving: true }) - ])); - - const promise = $.ajax({ - url: `/queue/bulk?blacklist=${blacklist}`, - method: 'DELETE', - dataType: 'json', - data: JSON.stringify({ ids }) - }); - - promise.done((data) => { - dispatch(batchActions([ - set({ section, isRemoving: false }), - fetchQueue() - ])); - }); - - promise.fail((xhr) => { - dispatch(batchActions([ - ...ids.map((id) => { - return updateItem({ - section, - id, - isRemoving: false - }); - }), - - set({ section, isRemoving: false }) - ])); - }); - }; - } - -}; - -export default queueActionHandlers; diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index 51dda41df..3c7de6ac9 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -1,23 +1,395 @@ +import _ from 'lodash'; +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import queueActionHandlers from './queueActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createClearReducer from './Creators/Reducers/createClearReducer'; +import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; +import { set, updateItem } from './baseActions'; -export const fetchQueueStatus = queueActionHandlers[types.FETCH_QUEUE_STATUS]; +// +// Variables -export const fetchQueueDetails = queueActionHandlers[types.FETCH_QUEUE_DETAILS]; -export const clearQueueDetails = createAction(types.CLEAR_QUEUE_DETAILS); +export const section = 'queue'; +const status = `${section}.status`; +const details = `${section}.details`; +const paged = `${section}.paged`; -export const fetchQueue = queueActionHandlers[types.FETCH_QUEUE]; -export const gotoQueueFirstPage = queueActionHandlers[types.GOTO_FIRST_QUEUE_PAGE]; -export const gotoQueuePreviousPage = queueActionHandlers[types.GOTO_PREVIOUS_QUEUE_PAGE]; -export const gotoQueueNextPage = queueActionHandlers[types.GOTO_NEXT_QUEUE_PAGE]; -export const gotoQueueLastPage = queueActionHandlers[types.GOTO_LAST_QUEUE_PAGE]; -export const gotoQueuePage = queueActionHandlers[types.GOTO_QUEUE_PAGE]; -export const setQueueSort = queueActionHandlers[types.SET_QUEUE_SORT]; -export const setQueueTableOption = createAction(types.SET_QUEUE_TABLE_OPTION); -export const clearQueue = createAction(types.CLEAR_QUEUE); +// +// State + +export const defaultState = { + status: { + isFetching: false, + isPopulated: false, + error: null, + item: {} + }, + + details: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + params: {} + }, + + paged: { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'timeleft', + sortDirection: sortDirections.ASCENDING, + error: null, + items: [], + isGrabbing: false, + isRemoving: false, + + columns: [ + { + name: 'status', + columnLabel: 'Status', + isVisible: true, + isModifiable: false + }, + { + name: 'artist.sortName', + label: 'Artist', + isSortable: true, + isVisible: true + }, + { + name: 'episodeTitle', + label: 'Album Title', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isSortable: true, + isVisible: true + }, + { + name: 'protocol', + label: 'Protocol', + isVisible: false + }, + { + name: 'indexer', + label: 'Indexer', + isVisible: false + }, + { + name: 'downloadClient', + label: 'Download Client', + isVisible: false + }, + { + name: 'estimatedCompletionTime', + label: 'Timeleft', + isSortable: true, + isVisible: true + }, + { + name: 'progress', + label: 'Progress', + isSortable: true, + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] + } +}; + +export const persistState = [ + 'queue.paged.pageSize', + 'queue.paged.sortKey', + 'queue.paged.sortDirection', + 'queue.paged.columns' +]; + +// +// Actions Types + +export const FETCH_QUEUE_STATUS = 'queue/fetchQueueStatus'; + +export const FETCH_QUEUE_DETAILS = 'queue/fetchQueueDetails'; +export const CLEAR_QUEUE_DETAILS = 'queue/clearQueueDetails'; + +export const FETCH_QUEUE = 'queue/fetchQueue'; +export const GOTO_FIRST_QUEUE_PAGE = 'queue/gotoQueueFirstPage'; +export const GOTO_PREVIOUS_QUEUE_PAGE = 'queue/gotoQueuePreviousPage'; +export const GOTO_NEXT_QUEUE_PAGE = 'queue/gotoQueueNextPage'; +export const GOTO_LAST_QUEUE_PAGE = 'queue/gotoQueueLastPage'; +export const GOTO_QUEUE_PAGE = 'queue/gotoQueuePage'; +export const SET_QUEUE_SORT = 'queue/setQueueSort'; +export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption'; +export const CLEAR_QUEUE = 'queue/clearQueue'; + +export const GRAB_QUEUE_ITEM = 'queue/grabQueueItem'; +export const GRAB_QUEUE_ITEMS = 'queue/grabQueueItems'; +export const REMOVE_QUEUE_ITEM = 'queue/removeQueueItem'; +export const REMOVE_QUEUE_ITEMS = 'queue/removeQueueItems'; + +// +// Action Creators + +export const fetchQueueStatus = createThunk(FETCH_QUEUE_STATUS); + +export const fetchQueueDetails = createThunk(FETCH_QUEUE_DETAILS); +export const clearQueueDetails = createAction(CLEAR_QUEUE_DETAILS); + +export const fetchQueue = createThunk(FETCH_QUEUE); +export const gotoQueueFirstPage = createThunk(GOTO_FIRST_QUEUE_PAGE); +export const gotoQueuePreviousPage = createThunk(GOTO_PREVIOUS_QUEUE_PAGE); +export const gotoQueueNextPage = createThunk(GOTO_NEXT_QUEUE_PAGE); +export const gotoQueueLastPage = createThunk(GOTO_LAST_QUEUE_PAGE); +export const gotoQueuePage = createThunk(GOTO_QUEUE_PAGE); +export const setQueueSort = createThunk(SET_QUEUE_SORT); +export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION); +export const clearQueue = createAction(CLEAR_QUEUE); + +export const grabQueueItem = createThunk(GRAB_QUEUE_ITEM); +export const grabQueueItems = createThunk(GRAB_QUEUE_ITEMS); +export const removeQueueItem = createThunk(REMOVE_QUEUE_ITEM); +export const removeQueueItems = createThunk(REMOVE_QUEUE_ITEMS); + +// +// Helpers + +const fetchQueueDetailsHelper = createFetchHandler(details, '/queue/details'); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [FETCH_QUEUE_STATUS]: createFetchHandler(status, '/queue/status'), + + [FETCH_QUEUE_DETAILS]: function(payload) { + return function(dispatch, getState) { + let params = payload; + + // If the payload params are empty try to get params from state. + + if (params && !_.isEmpty(params)) { + dispatch(set({ section: details, params })); + } else { + params = getState().queue.details.params; + } + + // Ensure there are params before trying to fetch the queue + // so we don't make a bad request to the server. + + if (params && !_.isEmpty(params)) { + fetchQueueDetailsHelper(getState, params, dispatch); + } + }; + }, + + ...createServerSideCollectionHandlers( + paged, + '/queue', + fetchQueue, + { + [serverSideCollectionHandlers.FETCH]: FETCH_QUEUE, + [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_QUEUE_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_QUEUE_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE, + [serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT + }), + + [GRAB_QUEUE_ITEM]: function(getState, payload, dispatch) { + const id = payload.id; + + dispatch(updateItem({ section: paged, id, isGrabbing: true })); + + const promise = $.ajax({ + url: `/queue/grab/${id}`, + method: 'POST' + }); + + promise.done((data) => { + dispatch(batchActions([ + dispatch(fetchQueue()), + + set({ + section: paged, + isGrabbing: false, + grabError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + section: paged, + id, + isGrabbing: false, + grabError: xhr + })); + }); + }, + + [GRAB_QUEUE_ITEMS]: function(getState, payload, dispatch) { + const ids = payload.ids; + + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section: paged, + id, + isGrabbing: true + }); + }), + + set({ + section: paged, + isGrabbing: true + }) + ])); + + const promise = $.ajax({ + url: '/queue/grab/bulk', + method: 'POST', + dataType: 'json', + data: JSON.stringify(payload) + }); + + promise.done((data) => { + dispatch(batchActions([ + dispatch(fetchQueue()), + + ...ids.map((id) => { + return updateItem({ + section: paged, + id, + isGrabbing: false, + grabError: null + }); + }), + + set({ + section: paged, + isGrabbing: false, + grabError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section: paged, + id, + isGrabbing: false, + grabError: null + }); + }), + + set({ section: paged, isGrabbing: false }) + ])); + }); + }, + + [REMOVE_QUEUE_ITEM]: function(getState, payload, dispatch) { + const { + id, + blacklist + } = payload; + + dispatch(updateItem({ section: paged, id, isRemoving: true })); + + const promise = $.ajax({ + url: `/queue/${id}?blacklist=${blacklist}`, + method: 'DELETE' + }); + + promise.done((data) => { + dispatch(fetchQueue()); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ section: paged, id, isRemoving: false })); + }); + }, + + [REMOVE_QUEUE_ITEMS]: function(getState, payload, dispatch) { + const { + ids, + blacklist + } = payload; + + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section: paged, + id, + isRemoving: true + }); + }), + + set({ section: paged, isRemoving: true }) + ])); + + const promise = $.ajax({ + url: `/queue/bulk?blacklist=${blacklist}`, + method: 'DELETE', + dataType: 'json', + data: JSON.stringify({ ids }) + }); + + promise.done((data) => { + dispatch(batchActions([ + set({ section: paged, isRemoving: false }), + fetchQueue() + ])); + }); + + promise.fail((xhr) => { + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section: paged, + id, + isRemoving: false + }); + }), + + set({ section: paged, isRemoving: false }) + ])); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [CLEAR_QUEUE_DETAILS]: createClearReducer(details, defaultState.details), + + [SET_QUEUE_TABLE_OPTION]: createSetTableOptionReducer(paged), + + [CLEAR_QUEUE]: createClearReducer(paged, { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }) + +}, defaultState, section); -export const grabQueueItem = queueActionHandlers[types.GRAB_QUEUE_ITEM]; -export const grabQueueItems = queueActionHandlers[types.GRAB_QUEUE_ITEMS]; -export const removeQueueItem = queueActionHandlers[types.REMOVE_QUEUE_ITEM]; -export const removeQueueItems = queueActionHandlers[types.REMOVE_QUEUE_ITEMS]; diff --git a/frontend/src/Store/Actions/reducers.js b/frontend/src/Store/Actions/reducers.js new file mode 100644 index 000000000..0254cd226 --- /dev/null +++ b/frontend/src/Store/Actions/reducers.js @@ -0,0 +1,20 @@ +import { combineReducers } from 'redux'; +import { enableBatching } from 'redux-batched-actions'; +import { routerReducer } from 'react-router-redux'; +import actions from 'Store/Actions'; + +const defaultState = {}; + +const reducers = { + routing: routerReducer +}; + +actions.forEach((action) => { + const section = action.section; + + defaultState[section] = action.defaultState; + reducers[section] = action.reducers; +}); + +export { defaultState }; +export default enableBatching(combineReducers(reducers)); diff --git a/frontend/src/Store/Actions/releaseActionHandlers.js b/frontend/src/Store/Actions/releaseActionHandlers.js deleted file mode 100644 index 6fc0520f5..000000000 --- a/frontend/src/Store/Actions/releaseActionHandlers.js +++ /dev/null @@ -1,64 +0,0 @@ -import $ from 'jquery'; -import createFetchHandler from './Creators/createFetchHandler'; -import * as types from './actionTypes'; -import { updateRelease } from './releaseActions'; - -let abortCurrentRequest = null; -const section = 'releases'; - -const fetchReleases = createFetchHandler(section, '/release'); - -const releaseActionHandlers = { - [types.FETCH_RELEASES]: function(payload) { - return function(dispatch, getState) { - const abortRequest = fetchReleases(payload)(dispatch, getState); - - abortCurrentRequest = abortRequest; - }; - }, - - [types.CANCEL_FETCH_RELEASES]: function(payload) { - return function(dispatch, getState) { - if (abortCurrentRequest) { - abortCurrentRequest = abortCurrentRequest(); - } - }; - }, - - [types.GRAB_RELEASE]: function(payload) { - return function(dispatch, getState) { - const guid = payload.guid; - - dispatch(updateRelease({ guid, isGrabbing: true })); - - const promise = $.ajax({ - url: '/release', - method: 'POST', - contentType: 'application/json', - data: JSON.stringify(payload) - }); - - promise.done((data) => { - dispatch(updateRelease({ - guid, - isGrabbing: false, - isGrabbed: true, - grabError: null - })); - }); - - promise.fail((xhr) => { - const grabError = xhr.responseJSON && xhr.responseJSON.message || 'Failed to add to download queue'; - - dispatch(updateRelease({ - guid, - isGrabbing: false, - isGrabbed: false, - grabError - })); - }); - }; - } -}; - -export default releaseActionHandlers; diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index 065ff1253..ca5f50b0c 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -1,10 +1,152 @@ +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import releaseActionHandlers from './releaseActionHandlers'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; -export const fetchReleases = releaseActionHandlers[types.FETCH_RELEASES]; -export const cancelFetchReleases = releaseActionHandlers[types.CANCEL_FETCH_RELEASES]; -export const setReleasesSort = createAction(types.SET_RELEASES_SORT); -export const clearReleases = createAction(types.CLEAR_RELEASES); -export const grabRelease = releaseActionHandlers[types.GRAB_RELEASE]; -export const updateRelease = createAction(types.UPDATE_RELEASE); +// +// Variables + +export const section = 'releases'; +let abortCurrentRequest = null; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + sortKey: 'releaseWeight', + sortDirection: sortDirections.ASCENDING, + sortPredicates: { + peers: function(item, direction) { + const seeders = item.seeders || 0; + const leechers = item.leechers || 0; + + return seeders * 1000000 + leechers; + }, + + rejections: function(item, direction) { + const rejections = item.rejections; + const releaseWeight = item.releaseWeight; + + if (rejections.length !== 0) { + return releaseWeight + 1000000; + } + + return releaseWeight; + } + } +}; + +// +// Actions Types + +export const FETCH_RELEASES = 'releases/fetchReleases'; +export const CANCEL_FETCH_RELEASES = 'releases/cancelFetchReleases'; +export const SET_RELEASES_SORT = 'releases/setReleasesSort'; +export const CLEAR_RELEASES = 'releases/clearReleases'; +export const GRAB_RELEASE = 'releases/grabRelease'; +export const UPDATE_RELEASE = 'releases/updateRelease'; + +// +// Action Creators + +export const fetchReleases = createThunk(FETCH_RELEASES); +export const cancelFetchReleases = createThunk(CANCEL_FETCH_RELEASES); +export const setReleasesSort = createAction(SET_RELEASES_SORT); +export const clearReleases = createAction(CLEAR_RELEASES); +export const grabRelease = createThunk(GRAB_RELEASE); +export const updateRelease = createAction(UPDATE_RELEASE); + +// +// Helpers + +const fetchReleasesHelper = createFetchHandler(section, '/release'); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [FETCH_RELEASES]: function(getState, payload, dispatch) { + const abortRequest = fetchReleasesHelper(getState, payload, dispatch); + + abortCurrentRequest = abortRequest; + }, + + [CANCEL_FETCH_RELEASES]: function(getState, payload, dispatch) { + if (abortCurrentRequest) { + abortCurrentRequest = abortCurrentRequest(); + } + }, + + [GRAB_RELEASE]: function(getState, payload, dispatch) { + const guid = payload.guid; + + dispatch(updateRelease({ guid, isGrabbing: true })); + + const promise = $.ajax({ + url: '/release', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(payload) + }); + + promise.done((data) => { + dispatch(updateRelease({ + guid, + isGrabbing: false, + isGrabbed: true, + grabError: null + })); + }); + + promise.fail((xhr) => { + const grabError = xhr.responseJSON && xhr.responseJSON.message || 'Failed to add to download queue'; + + dispatch(updateRelease({ + guid, + isGrabbing: false, + isGrabbed: false, + grabError + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [CLEAR_RELEASES]: (state) => { + return Object.assign({}, state, defaultState); + }, + + [UPDATE_RELEASE]: (state, { payload }) => { + const guid = payload.guid; + const newState = Object.assign({}, state); + const items = newState.items; + + // Return early if there aren't any items (the user closed the modal) + if (!items.length) { + return; + } + + const index = items.findIndex((item) => item.guid === guid); + const item = Object.assign({}, items[index], payload); + + newState.items = [...items]; + newState.items.splice(index, 1, item); + + return newState; + }, + + [SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section) + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/rootFolderActionHandlers.js b/frontend/src/Store/Actions/rootFolderActionHandlers.js deleted file mode 100644 index bf0800f31..000000000 --- a/frontend/src/Store/Actions/rootFolderActionHandlers.js +++ /dev/null @@ -1,60 +0,0 @@ -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import * as types from './actionTypes'; -import createFetchHandler from './Creators/createFetchHandler'; -import createRemoveItemHandler from './Creators/createRemoveItemHandler'; -import { set, updateItem } from './baseActions'; - -const section = 'rootFolders'; - -const rootFolderActionHandlers = { - [types.FETCH_ROOT_FOLDERS]: createFetchHandler('rootFolders', '/rootFolder'), - - [types.DELETE_ROOT_FOLDER]: createRemoveItemHandler( - 'rootFolders', - '/rootFolder', - (state) => state.rootFolders), - - [types.ADD_ROOT_FOLDER]: function(payload) { - return function(dispatch, getState) { - const path = payload.path; - - dispatch(set({ - section, - isSaving: true - })); - - const promise = $.ajax({ - url: '/rootFolder', - method: 'POST', - data: JSON.stringify({ path }), - dataType: 'json' - }); - - promise.done((data) => { - dispatch(batchActions([ - updateItem({ - section, - ...data - }), - - set({ - section, - isSaving: false, - saveError: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isSaving: false, - saveError: xhr - })); - }); - }; - } -}; - -export default rootFolderActionHandlers; diff --git a/frontend/src/Store/Actions/rootFolderActions.js b/frontend/src/Store/Actions/rootFolderActions.js index 0d0b8112a..8180cdc7d 100644 --- a/frontend/src/Store/Actions/rootFolderActions.js +++ b/frontend/src/Store/Actions/rootFolderActions.js @@ -1,6 +1,97 @@ -import * as types from './actionTypes'; -import rootFolderActionHandlers from './rootFolderActionHandlers'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import { set, updateItem } from './baseActions'; -export const fetchRootFolders = rootFolderActionHandlers[types.FETCH_ROOT_FOLDERS]; -export const addRootFolder = rootFolderActionHandlers[types.ADD_ROOT_FOLDER]; -export const deleteRootFolder = rootFolderActionHandlers[types.DELETE_ROOT_FOLDER]; +// +// Variables + +export const section = 'rootFolders'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [] +}; + +// +// Actions Types + +export const FETCH_ROOT_FOLDERS = 'rootFolders/fetchRootFolders'; +export const ADD_ROOT_FOLDER = 'rootFolders/addRootFolder'; +export const DELETE_ROOT_FOLDER = 'rootFolders/deleteRootFolder'; + +// +// Action Creators + +export const fetchRootFolders = createThunk(FETCH_ROOT_FOLDERS); +export const addRootFolder = createThunk(ADD_ROOT_FOLDER); +export const deleteRootFolder = createThunk(DELETE_ROOT_FOLDER); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [FETCH_ROOT_FOLDERS]: createFetchHandler('rootFolders', '/rootFolder'), + + [DELETE_ROOT_FOLDER]: createRemoveItemHandler( + 'rootFolders', + '/rootFolder', + (state) => state.rootFolders + ), + + [ADD_ROOT_FOLDER]: function(getState, payload, dispatch) { + const path = payload.path; + + dispatch(set({ + section, + isSaving: true + })); + + const promise = $.ajax({ + url: '/rootFolder', + method: 'POST', + data: JSON.stringify({ path }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(batchActions([ + updateItem({ + section, + ...data + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + } + +}); + +// +// Reducers + +export const reducers = createHandleActions({}, defaultState, section); diff --git a/frontend/src/Store/Actions/settingsActionHandlers.js b/frontend/src/Store/Actions/settingsActionHandlers.js deleted file mode 100644 index ba93b3ea3..000000000 --- a/frontend/src/Store/Actions/settingsActionHandlers.js +++ /dev/null @@ -1,286 +0,0 @@ -import _ from 'lodash'; -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import * as types from './actionTypes'; -import createFetchHandler from './Creators/createFetchHandler'; -import createFetchSchemaHandler from './Creators/createFetchSchemaHandler'; -import createSaveHandler from './Creators/createSaveHandler'; -import createSaveProviderHandler, { createCancelSaveProviderHandler } from './Creators/createSaveProviderHandler'; -import createRemoveItemHandler from './Creators/createRemoveItemHandler'; -import createTestProviderHandler, { createCancelTestProviderHandler } from './Creators/createTestProviderHandler'; -import { set, update, clearPendingChanges } from './baseActions'; - -const settingsActionHandlers = { - [types.FETCH_UI_SETTINGS]: createFetchHandler('ui', '/config/ui'), - [types.SAVE_UI_SETTINGS]: createSaveHandler('ui', '/config/ui', (state) => state.settings.ui), - - [types.FETCH_MEDIA_MANAGEMENT_SETTINGS]: createFetchHandler('mediaManagement', '/config/mediamanagement'), - [types.SAVE_MEDIA_MANAGEMENT_SETTINGS]: createSaveHandler('mediaManagement', '/config/mediamanagement', (state) => state.settings.mediaManagement), - - [types.FETCH_NAMING_SETTINGS]: createFetchHandler('naming', '/config/naming'), - [types.SAVE_NAMING_SETTINGS]: createSaveHandler('naming', '/config/naming', (state) => state.settings.naming), - - [types.FETCH_NAMING_EXAMPLES]: function(payload) { - const section = 'namingExamples'; - - return function(dispatch, getState) { - dispatch(set({ section, isFetching: true })); - - const naming = getState().settings.naming; - - const promise = $.ajax({ - url: '/config/naming/examples', - data: Object.assign({}, naming.item, naming.pendingChanges) - }); - - promise.done((data) => { - dispatch(batchActions([ - update({ section, data }), - - set({ - section, - isFetching: false, - isPopulated: true, - error: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isFetching: false, - isPopulated: false, - error: xhr - })); - }); - }; - }, - - [types.REORDER_DELAY_PROFILE]: function(payload) { - const section = 'delayProfiles'; - - return function(dispatch, getState) { - const { id, moveIndex } = payload; - const moveOrder = moveIndex + 1; - const delayProfiles = getState().settings.delayProfiles.items; - const moving = _.find(delayProfiles, { id }); - - // Don't move if the order hasn't changed - if (moving.order === moveOrder) { - return; - } - - const after = moveIndex > 0 ? _.find(delayProfiles, { order: moveIndex }) : null; - const afterQueryParam = after ? `after=${after.id}` : ''; - - const promise = $.ajax({ - method: 'PUT', - url: `/delayprofile/reorder/${id}?${afterQueryParam}` - }); - - promise.done((data) => { - dispatch(update({ section, data })); - }); - }; - }, - - [types.FETCH_QUALITY_PROFILES]: createFetchHandler('qualityProfiles', '/qualityprofile'), - [types.FETCH_QUALITY_PROFILE_SCHEMA]: createFetchSchemaHandler('qualityProfiles', '/qualityprofile/schema'), - - [types.SAVE_QUALITY_PROFILE]: createSaveProviderHandler( - 'qualityProfiles', - '/qualityprofile', - (state) => state.settings.qualityProfiles), - - [types.DELETE_QUALITY_PROFILE]: createRemoveItemHandler( - 'qualityProfiles', - '/qualityprofile', - (state) => state.settings.qualityProfiles), - - [types.FETCH_LANGUAGE_PROFILES]: createFetchHandler('languageProfiles', '/languageprofile'), - [types.FETCH_LANGUAGE_PROFILE_SCHEMA]: createFetchSchemaHandler('languageProfiles', '/languageprofile/schema'), - - [types.SAVE_LANGUAGE_PROFILE]: createSaveProviderHandler( - 'languageProfiles', - '/languageprofile', - (state) => state.settings.languageProfiles), - - [types.DELETE_LANGUAGE_PROFILE]: createRemoveItemHandler( - 'languageProfiles', - '/languageprofile', - (state) => state.settings.languageProfiles), - - [types.FETCH_METADATA_PROFILES]: createFetchHandler('metadataProfiles', '/metadataprofile'), - [types.FETCH_METADATA_PROFILE_SCHEMA]: createFetchSchemaHandler('metadataProfiles', '/metadataprofile/schema'), - - [types.SAVE_METADATA_PROFILE]: createSaveProviderHandler( - 'metadataProfiles', - '/metadataprofile', - (state) => state.settings.metadataProfiles), - - [types.DELETE_METADATA_PROFILE]: createRemoveItemHandler( - 'metadataProfiles', - '/metadataprofile', - (state) => state.settings.metadataProfiles), - - [types.FETCH_DELAY_PROFILES]: createFetchHandler('delayProfiles', '/delayprofile'), - - [types.SAVE_DELAY_PROFILE]: createSaveProviderHandler( - 'delayProfiles', - '/delayprofile', - (state) => state.settings.delayProfiles), - - [types.DELETE_DELAY_PROFILE]: createRemoveItemHandler( - 'delayProfiles', - '/delayprofile', - (state) => state.settings.delayProfiles), - - [types.FETCH_QUALITY_DEFINITIONS]: createFetchHandler('qualityDefinitions', '/qualitydefinition'), - [types.SAVE_QUALITY_DEFINITIONS]: createSaveHandler('qualityDefinitions', '/qualitydefinition', (state) => state.settings.qualitydefinitions), - - [types.SAVE_QUALITY_DEFINITIONS]: function() { - const section = 'qualityDefinitions'; - - return function(dispatch, getState) { - const qualityDefinitions = getState().settings.qualityDefinitions; - - const upatedDefinitions = Object.keys(qualityDefinitions.pendingChanges).map((key) => { - const id = parseInt(key); - const pendingChanges = qualityDefinitions.pendingChanges[id] || {}; - const item = _.find(qualityDefinitions.items, { id }); - - return Object.assign({}, item, pendingChanges); - }); - - // If there is nothing to save don't bother isSaving - if (!upatedDefinitions || !upatedDefinitions.length) { - return; - } - - const promise = $.ajax({ - method: 'PUT', - url: '/qualityDefinition/update', - data: JSON.stringify(upatedDefinitions) - }); - - promise.done((data) => { - dispatch(batchActions([ - update({ section, data }), - clearPendingChanges({ section: 'qualityDefinitions' }) - ])); - }); - }; - }, - - [types.FETCH_INDEXERS]: createFetchHandler('indexers', '/indexer'), - [types.FETCH_INDEXER_SCHEMA]: createFetchSchemaHandler('indexers', '/indexer/schema'), - - [types.SAVE_INDEXER]: createSaveProviderHandler( - 'indexers', - '/indexer', - (state) => state.settings.indexers), - - [types.CANCEL_SAVE_INDEXER]: createCancelSaveProviderHandler('indexers'), - - [types.DELETE_INDEXER]: createRemoveItemHandler( - 'indexers', - '/indexer', - (state) => state.settings.indexers), - - [types.TEST_INDEXER]: createTestProviderHandler( - 'indexers', - '/indexer', - (state) => state.settings.indexers), - - [types.CANCEL_TEST_INDEXER]: createCancelTestProviderHandler('indexers'), - - [types.FETCH_INDEXER_OPTIONS]: createFetchHandler('indexerOptions', '/config/indexer'), - [types.SAVE_INDEXER_OPTIONS]: createSaveHandler('indexerOptions', '/config/indexer', (state) => state.settings.indexerOptions), - - [types.FETCH_RESTRICTIONS]: createFetchHandler('restrictions', '/restriction'), - - [types.SAVE_RESTRICTION]: createSaveProviderHandler( - 'restrictions', - '/restriction', - (state) => state.settings.restrictions), - - [types.DELETE_RESTRICTION]: createRemoveItemHandler( - 'restrictions', - '/restriction', - (state) => state.settings.restrictions), - - [types.FETCH_DOWNLOAD_CLIENTS]: createFetchHandler('downloadClients', '/downloadclient'), - [types.FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler('downloadClients', '/downloadclient/schema'), - - [types.SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler( - 'downloadClients', - '/downloadclient', - (state) => state.settings.downloadClients), - - [types.CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler('downloadClients'), - - [types.DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler( - 'downloadClients', - '/downloadclient', - (state) => state.settings.downloadClients), - - [types.TEST_DOWNLOAD_CLIENT]: createTestProviderHandler( - 'downloadClients', - '/downloadclient', - (state) => state.settings.downloadClients), - - [types.CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler('downloadClients'), - - [types.FETCH_DOWNLOAD_CLIENT_OPTIONS]: createFetchHandler('downloadClientOptions', '/config/downloadclient'), - [types.SAVE_DOWNLOAD_CLIENT_OPTIONS]: createSaveHandler('downloadClientOptions', '/config/downloadclient', (state) => state.settings.downloadClientOptions), - - [types.FETCH_REMOTE_PATH_MAPPINGS]: createFetchHandler('remotePathMappings', '/remotepathmapping'), - - [types.SAVE_REMOTE_PATH_MAPPING]: createSaveProviderHandler( - 'remotePathMappings', - '/remotepathmapping', - (state) => state.settings.remotePathMappings), - - [types.DELETE_REMOTE_PATH_MAPPING]: createRemoveItemHandler( - 'remotePathMappings', - '/remotepathmapping', - (state) => state.settings.remotePathMappings), - - [types.FETCH_NOTIFICATIONS]: createFetchHandler('notifications', '/notification'), - [types.FETCH_NOTIFICATION_SCHEMA]: createFetchSchemaHandler('notifications', '/notification/schema'), - - [types.SAVE_NOTIFICATION]: createSaveProviderHandler( - 'notifications', - '/notification', - (state) => state.settings.notifications), - - [types.CANCEL_SAVE_NOTIFICATION]: createCancelSaveProviderHandler('notifications'), - - [types.DELETE_NOTIFICATION]: createRemoveItemHandler( - 'notifications', - '/notification', - (state) => state.settings.notifications), - - [types.TEST_NOTIFICATION]: createTestProviderHandler( - 'notifications', - '/notification', - (state) => state.settings.notifications), - - [types.CANCEL_TEST_NOTIFICATION]: createCancelTestProviderHandler('notifications'), - - [types.FETCH_METADATA]: createFetchHandler('metadata', '/metadata'), - - [types.SAVE_METADATA]: createSaveProviderHandler( - 'metadata', - '/metadata', - (state) => state.settings.metadata), - - [types.FETCH_METADATA_PROVIDER]: createFetchHandler('metadataProvider', '/config/metadataProvider'), - [types.SAVE_METADATA_PROVIDER]: createSaveHandler('metadataProvider', '/config/metadataProvider', (state) => state.settings.metadataProvider), - - [types.FETCH_GENERAL_SETTINGS]: createFetchHandler('general', '/config/host'), - [types.SAVE_GENERAL_SETTINGS]: createSaveHandler('general', '/config/host', (state) => state.settings.general) -}; - -export default settingsActionHandlers; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index 44ec54d54..cf83df51f 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -1,234 +1,140 @@ import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import settingsActionHandlers from './settingsActionHandlers'; +import { handleThunks } from 'Store/thunks'; +import createHandleActions from './Creators/createHandleActions'; +import delayProfiles from './Settings/delayProfiles'; +import downloadClients from './Settings/downloadClients'; +import downloadClientOptions from './Settings/downloadClientOptions'; +import general from './Settings/general'; +import indexerOptions from './Settings/indexerOptions'; +import indexers from './Settings/indexers'; +import languageProfiles from './Settings/languageProfiles'; +import metadataProfiles from './Settings/metadataProfiles'; +import mediaManagement from './Settings/mediaManagement'; +import metadata from './Settings/metadata'; +import metadataProvider from './Settings/metadataProvider'; +import naming from './Settings/naming'; +import namingExamples from './Settings/namingExamples'; +import notifications from './Settings/notifications'; +import qualityDefinitions from './Settings/qualityDefinitions'; +import qualityProfiles from './Settings/qualityProfiles'; +import remotePathMappings from './Settings/remotePathMappings'; +import restrictions from './Settings/restrictions'; +import ui from './Settings/ui'; -export const toggleAdvancedSettings = createAction(types.TOGGLE_ADVANCED_SETTINGS); +export * from './Settings/delayProfiles'; +export * from './Settings/downloadClients'; +export * from './Settings/downloadClientOptions'; +export * from './Settings/general'; +export * from './Settings/indexerOptions'; +export * from './Settings/indexers'; +export * from './Settings/languageProfiles'; +export * from './Settings/metadataProfiles'; +export * from './Settings/mediaManagement'; +export * from './Settings/metadata'; +export * from './Settings/metadataProvider'; +export * from './Settings/naming'; +export * from './Settings/namingExamples'; +export * from './Settings/notifications'; +export * from './Settings/qualityDefinitions'; +export * from './Settings/qualityProfiles'; +export * from './Settings/remotePathMappings'; +export * from './Settings/restrictions'; +export * from './Settings/ui'; -export const fetchUISettings = settingsActionHandlers[types.FETCH_UI_SETTINGS]; -export const saveUISettings = settingsActionHandlers[types.SAVE_UI_SETTINGS]; -export const setUISettingsValue = createAction(types.SET_UI_SETTINGS_VALUE, (payload) => { - return { - section: 'ui', - ...payload - }; +// +// Variables + +export const section = 'settings'; + +// +// State + +export const defaultState = { + advancedSettings: false, + + delayProfiles: delayProfiles.defaultState, + downloadClients: downloadClients.defaultState, + downloadClientOptions: downloadClientOptions.defaultState, + general: general.defaultState, + indexerOptions: indexerOptions.defaultState, + indexers: indexers.defaultState, + languageProfiles: languageProfiles.defaultState, + metadataProfiles: metadataProfiles.defaultState, + mediaManagement: mediaManagement.defaultState, + metadata: metadata.defaultState, + metadataProvider: metadataProvider.defaultState, + naming: naming.defaultState, + namingExamples: namingExamples.defaultState, + notifications: notifications.defaultState, + qualityDefinitions: qualityDefinitions.defaultState, + qualityProfiles: qualityProfiles.defaultState, + remotePathMappings: remotePathMappings.defaultState, + restrictions: restrictions.defaultState, + ui: ui.defaultState +}; + +// +// Actions Types + +export const TOGGLE_ADVANCED_SETTINGS = 'settings/toggleAdvancedSettings'; + +// +// Action Creators + +export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + ...delayProfiles.actionHandlers, + ...downloadClients.actionHandlers, + ...downloadClientOptions.actionHandlers, + ...general.actionHandlers, + ...indexerOptions.actionHandlers, + ...indexers.actionHandlers, + ...languageProfiles.actionHandlers, + ...metadataProfiles.actionHandlers, + ...mediaManagement.actionHandlers, + ...metadata.actionHandlers, + ...metadataProvider.actionHandlers, + ...naming.actionHandlers, + ...namingExamples.actionHandlers, + ...notifications.actionHandlers, + ...qualityDefinitions.actionHandlers, + ...qualityProfiles.actionHandlers, + ...remotePathMappings.actionHandlers, + ...restrictions.actionHandlers, + ...ui.actionHandlers }); -export const fetchMediaManagementSettings = settingsActionHandlers[types.FETCH_MEDIA_MANAGEMENT_SETTINGS]; -export const saveMediaManagementSettings = settingsActionHandlers[types.SAVE_MEDIA_MANAGEMENT_SETTINGS]; -export const setMediaManagementSettingsValue = createAction(types.SET_MEDIA_MANAGEMENT_SETTINGS_VALUE, (payload) => { - return { - section: 'mediaManagement', - ...payload - }; -}); +// +// Reducers -export const fetchNamingSettings = settingsActionHandlers[types.FETCH_NAMING_SETTINGS]; -export const saveNamingSettings = settingsActionHandlers[types.SAVE_NAMING_SETTINGS]; -export const setNamingSettingsValue = createAction(types.SET_NAMING_SETTINGS_VALUE, (payload) => { - return { - section: 'naming', - ...payload - }; -}); +export const reducers = createHandleActions({ -export const fetchNamingExamples = settingsActionHandlers[types.FETCH_NAMING_EXAMPLES]; + [TOGGLE_ADVANCED_SETTINGS]: (state, { payload }) => { + return Object.assign({}, state, { advancedSettings: !state.advancedSettings }); + }, -export const fetchQualityProfiles = settingsActionHandlers[types.FETCH_QUALITY_PROFILES]; -export const fetchQualityProfileSchema = settingsActionHandlers[types.FETCH_QUALITY_PROFILE_SCHEMA]; -export const saveQualityProfile = settingsActionHandlers[types.SAVE_QUALITY_PROFILE]; -export const deleteQualityProfile = settingsActionHandlers[types.DELETE_QUALITY_PROFILE]; + ...delayProfiles.reducers, + ...downloadClients.reducers, + ...downloadClientOptions.reducers, + ...general.reducers, + ...indexerOptions.reducers, + ...indexers.reducers, + ...languageProfiles.reducers, + ...metadataProfiles.reducers, + ...mediaManagement.reducers, + ...metadata.reducers, + ...metadataProvider.reducers, + ...naming.reducers, + ...namingExamples.reducers, + ...notifications.reducers, + ...qualityDefinitions.reducers, + ...qualityProfiles.reducers, + ...remotePathMappings.reducers, + ...restrictions.reducers, + ...ui.reducers -export const setQualityProfileValue = createAction(types.SET_QUALITY_PROFILE_VALUE, (payload) => { - return { - section: 'qualityProfiles', - ...payload - }; -}); - -export const fetchLanguageProfiles = settingsActionHandlers[types.FETCH_LANGUAGE_PROFILES]; -export const fetchLanguageProfileSchema = settingsActionHandlers[types.FETCH_LANGUAGE_PROFILE_SCHEMA]; -export const saveLanguageProfile = settingsActionHandlers[types.SAVE_LANGUAGE_PROFILE]; -export const deleteLanguageProfile = settingsActionHandlers[types.DELETE_LANGUAGE_PROFILE]; - -export const setLanguageProfileValue = createAction(types.SET_LANGUAGE_PROFILE_VALUE, (payload) => { - return { - section: 'languageProfiles', - ...payload - }; -}); - -export const fetchMetadataProfiles = settingsActionHandlers[types.FETCH_METADATA_PROFILES]; -export const fetchMetadataProfileSchema = settingsActionHandlers[types.FETCH_METADATA_PROFILE_SCHEMA]; -export const saveMetadataProfile = settingsActionHandlers[types.SAVE_METADATA_PROFILE]; -export const deleteMetadataProfile = settingsActionHandlers[types.DELETE_METADATA_PROFILE]; - -export const setMetadataProfileValue = createAction(types.SET_METADATA_PROFILE_VALUE, (payload) => { - return { - section: 'metadataProfiles', - ...payload - }; -}); - -export const fetchDelayProfiles = settingsActionHandlers[types.FETCH_DELAY_PROFILES]; -export const saveDelayProfile = settingsActionHandlers[types.SAVE_DELAY_PROFILE]; -export const deleteDelayProfile = settingsActionHandlers[types.DELETE_DELAY_PROFILE]; -export const reorderDelayProfile = settingsActionHandlers[types.REORDER_DELAY_PROFILE]; - -export const setDelayProfileValue = createAction(types.SET_DELAY_PROFILE_VALUE, (payload) => { - return { - section: 'delayProfiles', - ...payload - }; -}); - -export const fetchQualityDefinitions = settingsActionHandlers[types.FETCH_QUALITY_DEFINITIONS]; -export const saveQualityDefinitions = settingsActionHandlers[types.SAVE_QUALITY_DEFINITIONS]; - -export const setQualityDefinitionValue = createAction(types.SET_QUALITY_DEFINITION_VALUE); - -export const fetchIndexers = settingsActionHandlers[types.FETCH_INDEXERS]; -export const fetchIndexerSchema = settingsActionHandlers[types.FETCH_INDEXER_SCHEMA]; -export const selectIndexerSchema = createAction(types.SELECT_INDEXER_SCHEMA); - -export const saveIndexer = settingsActionHandlers[types.SAVE_INDEXER]; -export const cancelSaveIndexer = settingsActionHandlers[types.CANCEL_SAVE_INDEXER]; -export const deleteIndexer = settingsActionHandlers[types.DELETE_INDEXER]; -export const testIndexer = settingsActionHandlers[types.TEST_INDEXER]; -export const cancelTestIndexer = settingsActionHandlers[types.CANCEL_TEST_INDEXER]; - -export const setIndexerValue = createAction(types.SET_INDEXER_VALUE, (payload) => { - return { - section: 'indexers', - ...payload - }; -}); - -export const setIndexerFieldValue = createAction(types.SET_INDEXER_FIELD_VALUE, (payload) => { - return { - section: 'indexers', - ...payload - }; -}); - -export const fetchIndexerOptions = settingsActionHandlers[types.FETCH_INDEXER_OPTIONS]; -export const saveIndexerOptions = settingsActionHandlers[types.SAVE_INDEXER_OPTIONS]; -export const setIndexerOptionsValue = createAction(types.SET_INDEXER_OPTIONS_VALUE, (payload) => { - return { - section: 'indexerOptions', - ...payload - }; -}); - -export const fetchRestrictions = settingsActionHandlers[types.FETCH_RESTRICTIONS]; -export const saveRestriction = settingsActionHandlers[types.SAVE_RESTRICTION]; -export const deleteRestriction = settingsActionHandlers[types.DELETE_RESTRICTION]; - -export const setRestrictionValue = createAction(types.SET_RESTRICTION_VALUE, (payload) => { - return { - section: 'restrictions', - ...payload - }; -}); - -export const fetchDownloadClients = settingsActionHandlers[types.FETCH_DOWNLOAD_CLIENTS]; -export const fetchDownloadClientSchema = settingsActionHandlers[types.FETCH_DOWNLOAD_CLIENT_SCHEMA]; -export const selectDownloadClientSchema = createAction(types.SELECT_DOWNLOAD_CLIENT_SCHEMA); - -export const saveDownloadClient = settingsActionHandlers[types.SAVE_DOWNLOAD_CLIENT]; -export const cancelSaveDownloadClient = settingsActionHandlers[types.CANCEL_SAVE_DOWNLOAD_CLIENT]; -export const deleteDownloadClient = settingsActionHandlers[types.DELETE_DOWNLOAD_CLIENT]; -export const testDownloadClient = settingsActionHandlers[types.TEST_DOWNLOAD_CLIENT]; -export const cancelTestDownloadClient = settingsActionHandlers[types.CANCEL_TEST_DOWNLOAD_CLIENT]; - -export const setDownloadClientValue = createAction(types.SET_DOWNLOAD_CLIENT_VALUE, (payload) => { - return { - section: 'downloadClients', - ...payload - }; -}); - -export const setDownloadClientFieldValue = createAction(types.SET_DOWNLOAD_CLIENT_FIELD_VALUE, (payload) => { - return { - section: 'downloadClients', - ...payload - }; -}); - -export const fetchDownloadClientOptions = settingsActionHandlers[types.FETCH_DOWNLOAD_CLIENT_OPTIONS]; -export const saveDownloadClientOptions = settingsActionHandlers[types.SAVE_DOWNLOAD_CLIENT_OPTIONS]; -export const setDownloadClientOptionsValue = createAction(types.SET_DOWNLOAD_CLIENT_OPTIONS_VALUE, (payload) => { - return { - section: 'downloadClientOptions', - ...payload - }; -}); - -export const fetchRemotePathMappings = settingsActionHandlers[types.FETCH_REMOTE_PATH_MAPPINGS]; -export const saveRemotePathMapping = settingsActionHandlers[types.SAVE_REMOTE_PATH_MAPPING]; -export const deleteRemotePathMapping = settingsActionHandlers[types.DELETE_REMOTE_PATH_MAPPING]; - -export const setRemotePathMappingValue = createAction(types.SET_REMOTE_PATH_MAPPING_VALUE, (payload) => { - return { - section: 'remotePathMappings', - ...payload - }; -}); - -export const fetchNotifications = settingsActionHandlers[types.FETCH_NOTIFICATIONS]; -export const fetchNotificationSchema = settingsActionHandlers[types.FETCH_NOTIFICATION_SCHEMA]; -export const selectNotificationSchema = createAction(types.SELECT_NOTIFICATION_SCHEMA); - -export const saveNotification = settingsActionHandlers[types.SAVE_NOTIFICATION]; -export const cancelSaveNotification = settingsActionHandlers[types.CANCEL_SAVE_NOTIFICATION]; -export const deleteNotification = settingsActionHandlers[types.DELETE_NOTIFICATION]; -export const testNotification = settingsActionHandlers[types.TEST_NOTIFICATION]; -export const cancelTestNotification = settingsActionHandlers[types.CANCEL_TEST_NOTIFICATION]; - -export const setNotificationValue = createAction(types.SET_NOTIFICATION_VALUE, (payload) => { - return { - section: 'notifications', - ...payload - }; -}); - -export const setNotificationFieldValue = createAction(types.SET_NOTIFICATION_FIELD_VALUE, (payload) => { - return { - section: 'notifications', - ...payload - }; -}); - -export const fetchMetadata = settingsActionHandlers[types.FETCH_METADATA]; -export const saveMetadata = settingsActionHandlers[types.SAVE_METADATA]; - -export const setMetadataValue = createAction(types.SET_METADATA_VALUE, (payload) => { - return { - section: 'metadata', - ...payload - }; -}); - -export const setMetadataFieldValue = createAction(types.SET_METADATA_FIELD_VALUE, (payload) => { - return { - section: 'metadata', - ...payload - }; -}); - -export const fetchMetadataProvider = settingsActionHandlers[types.FETCH_METADATA_PROVIDER]; -export const saveMetadataProvider = settingsActionHandlers[types.SAVE_METADATA_PROVIDER]; -export const setMetadataProviderValue = createAction(types.SET_METADATA_PROVIDER_VALUE, (payload) => { - return { - section: 'metadataProvider', - ...payload - }; -}); - -export const fetchGeneralSettings = settingsActionHandlers[types.FETCH_GENERAL_SETTINGS]; -export const saveGeneralSettings = settingsActionHandlers[types.SAVE_GENERAL_SETTINGS]; -export const setGeneralSettingsValue = createAction(types.SET_GENERAL_SETTINGS_VALUE, (payload) => { - return { - section: 'general', - ...payload - }; -}); +}, defaultState, section); diff --git a/frontend/src/Store/Actions/systemActionHandlers.js b/frontend/src/Store/Actions/systemActionHandlers.js deleted file mode 100644 index d40674da3..000000000 --- a/frontend/src/Store/Actions/systemActionHandlers.js +++ /dev/null @@ -1,48 +0,0 @@ -import $ from 'jquery'; -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; -import * as types from './actionTypes'; -import createFetchHandler from './Creators/createFetchHandler'; -import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; - -const systemActionHandlers = { - [types.FETCH_STATUS]: createFetchHandler('status', '/system/status'), - [types.FETCH_HEALTH]: createFetchHandler('health', '/health'), - [types.FETCH_DISK_SPACE]: createFetchHandler('diskSpace', '/diskspace'), - [types.FETCH_TASK]: createFetchHandler('tasks', '/system/task'), - [types.FETCH_TASKS]: createFetchHandler('tasks', '/system/task'), - [types.FETCH_BACKUPS]: createFetchHandler('backups', '/system/backup'), - [types.FETCH_UPDATES]: createFetchHandler('updates', '/update'), - [types.FETCH_LOG_FILES]: createFetchHandler('logFiles', '/log/file'), - [types.FETCH_UPDATE_LOG_FILES]: createFetchHandler('updateLogFiles', '/log/file/update'), - - ...createServerSideCollectionHandlers('logs', '/log', (state) => state.system, { - [serverSideCollectionHandlers.FETCH]: types.FETCH_LOGS, - [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_LOGS_PAGE, - [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_LOGS_PAGE, - [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_LOGS_PAGE, - [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_LOGS_PAGE, - [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_LOGS_PAGE, - [serverSideCollectionHandlers.SORT]: types.SET_LOGS_SORT, - [serverSideCollectionHandlers.FILTER]: types.SET_LOGS_FILTER - }), - - [types.RESTART]: function() { - return function() { - $.ajax({ - url: '/system/restart', - method: 'POST' - }); - }; - }, - - [types.SHUTDOWN]: function() { - return function() { - $.ajax({ - url: '/system/shutdown', - method: 'POST' - }); - }; - } -}; - -export default systemActionHandlers; diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js index b5614d2d3..77493320d 100644 --- a/frontend/src/Store/Actions/systemActions.js +++ b/frontend/src/Store/Actions/systemActions.js @@ -1,28 +1,237 @@ +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import systemActionHandlers from './systemActionHandlers'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; -export const fetchStatus = systemActionHandlers[types.FETCH_STATUS]; -export const fetchHealth = systemActionHandlers[types.FETCH_HEALTH]; -export const fetchDiskSpace = systemActionHandlers[types.FETCH_DISK_SPACE]; +// +// Variables -export const fetchTask = systemActionHandlers[types.FETCH_TASK]; -export const fetchTasks = systemActionHandlers[types.FETCH_TASKS]; -export const fetchBackups = systemActionHandlers[types.FETCH_BACKUPS]; -export const fetchUpdates = systemActionHandlers[types.FETCH_UPDATES]; +export const section = 'system'; -export const fetchLogs = systemActionHandlers[types.FETCH_LOGS]; -export const gotoLogsFirstPage = systemActionHandlers[types.GOTO_FIRST_LOGS_PAGE]; -export const gotoLogsPreviousPage = systemActionHandlers[types.GOTO_PREVIOUS_LOGS_PAGE]; -export const gotoLogsNextPage = systemActionHandlers[types.GOTO_NEXT_LOGS_PAGE]; -export const gotoLogsLastPage = systemActionHandlers[types.GOTO_LAST_LOGS_PAGE]; -export const gotoLogsPage = systemActionHandlers[types.GOTO_LOGS_PAGE]; -export const setLogsSort = systemActionHandlers[types.SET_LOGS_SORT]; -export const setLogsFilter = systemActionHandlers[types.SET_LOGS_FILTER]; -export const setLogsTableOption = createAction(types.SET_LOGS_TABLE_OPTION); +// +// State -export const fetchLogFiles = systemActionHandlers[types.FETCH_LOG_FILES]; -export const fetchUpdateLogFiles = systemActionHandlers[types.FETCH_UPDATE_LOG_FILES]; +export const defaultState = { + status: { + isFetching: false, + isPopulated: false, + error: null, + item: {} + }, -export const restart = systemActionHandlers[types.RESTART]; -export const shutdown = systemActionHandlers[types.SHUTDOWN]; + health: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + diskSpace: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + tasks: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + backups: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + updates: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + logs: { + isFetching: false, + isPopulated: false, + pageSize: 50, + sortKey: 'time', + sortDirection: sortDirections.DESCENDING, + filterKey: null, + filterValue: null, + error: null, + items: [], + + columns: [ + { + name: 'level', + isSortable: true, + isVisible: true + }, + { + name: 'logger', + label: 'Component', + isSortable: true, + isVisible: true + }, + { + name: 'message', + label: 'Message', + isVisible: true + }, + { + name: 'time', + label: 'Time', + isSortable: true, + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isSortable: true, + isVisible: true, + isModifiable: false + } + ] + }, + + logFiles: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + updateLogFiles: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + } +}; + +export const persistState = [ + 'system.logs.pageSize', + 'system.logs.sortKey', + 'system.logs.sortDirection', + 'system.logs.filterKey', + 'system.logs.filterValue' +]; + +// +// Actions Types + +export const FETCH_STATUS = 'system/status/fetchStatus'; +export const FETCH_HEALTH = 'system/health/fetchHealth'; +export const FETCH_DISK_SPACE = 'system/diskSpace/fetchDiskSPace'; + +export const FETCH_TASK = 'system/tasks/fetchTask'; +export const FETCH_TASKS = 'system/tasks/fetchTasks'; +export const FETCH_BACKUPS = 'system/backups/fetchBackups'; +export const FETCH_UPDATES = 'system/updates/fetchUpdates'; + +export const FETCH_LOGS = 'system/logs/fetchLogs'; +export const GOTO_FIRST_LOGS_PAGE = 'system/logs/gotoLogsFirstPage'; +export const GOTO_PREVIOUS_LOGS_PAGE = 'system/logs/gotoLogsPreviousPage'; +export const GOTO_NEXT_LOGS_PAGE = 'system/logs/gotoLogsNextPage'; +export const GOTO_LAST_LOGS_PAGE = 'system/logs/gotoLogsLastPage'; +export const GOTO_LOGS_PAGE = 'system/logs/gotoLogsPage'; +export const SET_LOGS_SORT = 'system/logs/setLogsSort'; +export const SET_LOGS_FILTER = 'system/logs/setLogsFilter'; +export const SET_LOGS_TABLE_OPTION = 'system/logs/ssetLogsTableOption'; + +export const FETCH_LOG_FILES = 'system/logFiles/fetchLogFiles'; +export const FETCH_UPDATE_LOG_FILES = 'system/updateLogFiles/fetchUpdateLogFiles'; + +export const RESTART = 'system/restart'; +export const SHUTDOWN = 'system/shutdown'; + +// +// Action Creators + +export const fetchStatus = createThunk(FETCH_STATUS); +export const fetchHealth = createThunk(FETCH_HEALTH); +export const fetchDiskSpace = createThunk(FETCH_DISK_SPACE); + +export const fetchTask = createThunk(FETCH_TASK); +export const fetchTasks = createThunk(FETCH_TASKS); +export const fetchBackups = createThunk(FETCH_BACKUPS); +export const fetchUpdates = createThunk(FETCH_UPDATES); + +export const fetchLogs = createThunk(FETCH_LOGS); +export const gotoLogsFirstPage = createThunk(GOTO_FIRST_LOGS_PAGE); +export const gotoLogsPreviousPage = createThunk(GOTO_PREVIOUS_LOGS_PAGE); +export const gotoLogsNextPage = createThunk(GOTO_NEXT_LOGS_PAGE); +export const gotoLogsLastPage = createThunk(GOTO_LAST_LOGS_PAGE); +export const gotoLogsPage = createThunk(GOTO_LOGS_PAGE); +export const setLogsSort = createThunk(SET_LOGS_SORT); +export const setLogsFilter = createThunk(SET_LOGS_FILTER); +export const setLogsTableOption = createAction(SET_LOGS_TABLE_OPTION); + +export const fetchLogFiles = createThunk(FETCH_LOG_FILES); +export const fetchUpdateLogFiles = createThunk(FETCH_UPDATE_LOG_FILES); + +export const restart = createThunk(RESTART); +export const shutdown = createThunk(SHUTDOWN); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [FETCH_STATUS]: createFetchHandler('system.status', '/system/status'), + [FETCH_HEALTH]: createFetchHandler('system.health', '/health'), + [FETCH_DISK_SPACE]: createFetchHandler('system.diskSpace', '/diskspace'), + [FETCH_TASK]: createFetchHandler('system.tasks', '/system/task'), + [FETCH_TASKS]: createFetchHandler('system.tasks', '/system/task'), + [FETCH_BACKUPS]: createFetchHandler('system.backups', '/system/backup'), + [FETCH_UPDATES]: createFetchHandler('system.updates', '/update'), + [FETCH_LOG_FILES]: createFetchHandler('system.logFiles', '/log/file'), + [FETCH_UPDATE_LOG_FILES]: createFetchHandler('system.updateLogFiles', '/log/file/update'), + + ...createServerSideCollectionHandlers( + 'system.logs', + '/log', + fetchLogs, + { + [serverSideCollectionHandlers.FETCH]: FETCH_LOGS, + [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_LOGS_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_LOGS_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_LOGS_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_LOGS_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_LOGS_PAGE, + [serverSideCollectionHandlers.SORT]: SET_LOGS_SORT, + [serverSideCollectionHandlers.FILTER]: SET_LOGS_FILTER + } + ), + + [RESTART]: function() { + $.ajax({ + url: '/system/restart', + method: 'POST' + }); + }, + + [SHUTDOWN]: function() { + $.ajax({ + url: '/system/shutdown', + method: 'POST' + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_LOGS_TABLE_OPTION]: createSetTableOptionReducer('logs') + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/tagActionHandlers.js b/frontend/src/Store/Actions/tagActionHandlers.js deleted file mode 100644 index c4e007f6c..000000000 --- a/frontend/src/Store/Actions/tagActionHandlers.js +++ /dev/null @@ -1,28 +0,0 @@ -import $ from 'jquery'; -import * as types from './actionTypes'; -import { update } from './baseActions'; -import createFetchHandler from './Creators/createFetchHandler'; - -const tagActionHandlers = { - [types.FETCH_TAGS]: createFetchHandler('tags', '/tag'), - - [types.ADD_TAG]: function(payload) { - return (dispatch, getState) => { - const promise = $.ajax({ - url: '/tag', - method: 'POST', - data: JSON.stringify(payload.tag) - }); - - promise.done((data) => { - const tags = getState().tags.items.slice(); - tags.push(data); - - dispatch(update({ section: 'tags', data: tags })); - payload.onTagCreated(data); - }); - }; - } -}; - -export default tagActionHandlers; diff --git a/frontend/src/Store/Actions/tagActions.js b/frontend/src/Store/Actions/tagActions.js index 45f0141ce..3f5b708fe 100644 --- a/frontend/src/Store/Actions/tagActions.js +++ b/frontend/src/Store/Actions/tagActions.js @@ -1,5 +1,61 @@ -import * as types from './actionTypes'; -import tagActionHandlers from './tagActionHandlers'; +import $ from 'jquery'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import { update } from './baseActions'; -export const fetchTags = tagActionHandlers[types.FETCH_TAGS]; -export const addTag = tagActionHandlers[types.ADD_TAG]; +// +// Variables + +export const section = 'tags'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [] +}; + +// +// Actions Types + +export const FETCH_TAGS = 'tags/fetchTags'; +export const ADD_TAG = 'tags/addTag'; + +// +// Action Creators + +export const fetchTags = createThunk(FETCH_TAGS); +export const addTag = createThunk(ADD_TAG); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [FETCH_TAGS]: createFetchHandler('tags', '/tag'), + + [ADD_TAG]: function(payload) { + return (dispatch, getState) => { + const promise = $.ajax({ + url: '/tag', + method: 'POST', + data: JSON.stringify(payload.tag) + }); + + promise.done((data) => { + const tags = getState().tags.items.slice(); + tags.push(data); + + dispatch(update({ section: 'tags', data: tags })); + payload.onTagCreated(data); + }); + }; + } +}); + +// +// Reducers +export const reducers = createHandleActions({}, defaultState, section); diff --git a/frontend/src/Store/Actions/trackActionHandlers.js b/frontend/src/Store/Actions/trackActionHandlers.js deleted file mode 100644 index efda3ae40..000000000 --- a/frontend/src/Store/Actions/trackActionHandlers.js +++ /dev/null @@ -1,11 +0,0 @@ -import createFetchHandler from './Creators/createFetchHandler'; -import * as types from './actionTypes'; - -const section = 'tracks'; - -const trackActionHandlers = { - [types.FETCH_TRACKS]: createFetchHandler(section, '/track') - -}; - -export default trackActionHandlers; diff --git a/frontend/src/Store/Actions/trackActions.js b/frontend/src/Store/Actions/trackActions.js index 57d503c12..a2b4fd570 100644 --- a/frontend/src/Store/Actions/trackActions.js +++ b/frontend/src/Store/Actions/trackActions.js @@ -1,8 +1,111 @@ import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import trackActionHandlers from './trackActionHandlers'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; +import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; -export const fetchTracks = trackActionHandlers[types.FETCH_TRACKS]; -export const setTracksSort = createAction(types.SET_TRACKS_SORT); -export const setTracksTableOption = createAction(types.SET_TRACKS_TABLE_OPTION); -export const clearTracks = createAction(types.CLEAR_TRACKS); +// +// Variables + +export const section = 'tracks'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + sortKey: 'mediumNumber', + sortDirection: sortDirections.DESCENDING, + items: [], + + columns: [ + { + name: 'medium', + label: 'Medium', + isVisible: true + }, + { + name: 'absoluteTrackNumber', + label: 'Track', + isVisible: true + }, + { + name: 'title', + label: 'Title', + isVisible: true + }, + { + name: 'duration', + label: 'Duration', + isVisible: true + }, + { + name: 'audioInfo', + label: 'Audio Info', + isVisible: true + }, + { + name: 'status', + label: 'Status', + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'tracks.columns' +]; + +// +// Actions Types + +export const FETCH_TRACKS = 'tracks/fetchTracks'; +export const SET_TRACKS_SORT = 'tracks/setTracksSort'; +export const SET_TRACKS_TABLE_OPTION = 'tracks/setTracksTableOption'; +export const CLEAR_TRACKS = 'tracks/clearTracks'; + +// +// Action Creators + +export const fetchTracks = createThunk(FETCH_TRACKS); +export const setTracksSort = createAction(SET_TRACKS_SORT); +export const setTracksTableOption = createAction(SET_TRACKS_TABLE_OPTION); +export const clearTracks = createAction(CLEAR_TRACKS); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [FETCH_TRACKS]: createFetchHandler(section, '/track') + +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_TRACKS_TABLE_OPTION]: createSetTableOptionReducer(section), + + [FETCH_TRACKS]: (state) => { + return Object.assign({}, state, { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }); + }, + + [SET_TRACKS_SORT]: createSetClientSideCollectionSortReducer(section) + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/trackFileActionHandlers.js b/frontend/src/Store/Actions/trackFileActionHandlers.js deleted file mode 100644 index bbb09a39e..000000000 --- a/frontend/src/Store/Actions/trackFileActionHandlers.js +++ /dev/null @@ -1,164 +0,0 @@ -import _ from 'lodash'; -import $ from 'jquery'; -import { batchActions } from 'redux-batched-actions'; -import episodeEntities from 'Album/episodeEntities'; -import createFetchHandler from './Creators/createFetchHandler'; -import createRemoveItemHandler from './Creators/createRemoveItemHandler'; -import * as types from './actionTypes'; -import { set, removeItem, updateItem } from './baseActions'; - -const section = 'trackFiles'; -const deleteTrackFile = createRemoveItemHandler(section, '/trackFile'); - -const trackFileActionHandlers = { - [types.FETCH_TRACK_FILES]: createFetchHandler(section, '/trackFile'), - - [types.DELETE_TRACK_FILE]: function(payload) { - return function(dispatch, getState) { - const { - id: trackFileId, - episodeEntity = episodeEntities.EPISODES - } = payload; - - const episodeSection = _.last(episodeEntity.split('.')); - - const deletePromise = deleteTrackFile(payload)(dispatch, getState); - - deletePromise.done(() => { - const episodes = getState().episodes.items; - const episodesWithRemovedFiles = _.filter(episodes, { trackFileId }); - - dispatch(batchActions([ - ...episodesWithRemovedFiles.map((episode) => { - return updateItem({ - section: episodeSection, - ...episode, - trackFileId: 0, - hasFile: false - }); - }) - ])); - }); - }; - }, - - [types.DELETE_TRACK_FILES]: function(payload) { - return function(dispatch, getState) { - const { - trackFileIds - } = payload; - - dispatch(set({ section, isDeleting: true })); - - const promise = $.ajax({ - url: '/trackFile/bulk', - method: 'DELETE', - dataType: 'json', - data: JSON.stringify({ trackFileIds }) - }); - - promise.done(() => { - const episodes = getState().episodes.items; - const episodesWithRemovedFiles = trackFileIds.reduce((acc, trackFileId) => { - acc.push(..._.filter(episodes, { trackFileId })); - - return acc; - }, []); - - dispatch(batchActions([ - ...trackFileIds.map((id) => { - return removeItem({ section, id }); - }), - - ...episodesWithRemovedFiles.map((episode) => { - return updateItem({ - section: 'episodes', - ...episode, - trackFileId: 0, - hasFile: false - }); - }), - - set({ - section, - isDeleting: false, - deleteError: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isDeleting: false, - deleteError: xhr - })); - }); - }; - }, - - [types.UPDATE_TRACK_FILES]: function(payload) { - return function(dispatch, getState) { - const { - trackFileIds, - language, - quality - } = payload; - - dispatch(set({ section, isSaving: true })); - - const data = { - trackFileIds - }; - - if (language) { - data.language = language; - } - - if (quality) { - data.quality = quality; - } - - const promise = $.ajax({ - url: '/trackFile/editor', - method: 'PUT', - dataType: 'json', - data: JSON.stringify(data) - }); - - promise.done(() => { - dispatch(batchActions([ - ...trackFileIds.map((id) => { - const props = {}; - - if (language) { - props.language = language; - } - - if (quality) { - props.quality = quality; - } - - return updateItem({ section, id, ...props }); - }), - - set({ - section, - isSaving: false, - saveError: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isSaving: false, - saveError: xhr - })); - }); - }; - } -}; - -export default trackFileActionHandlers; diff --git a/frontend/src/Store/Actions/trackFileActions.js b/frontend/src/Store/Actions/trackFileActions.js index fa4b6544a..8be088b0f 100644 --- a/frontend/src/Store/Actions/trackFileActions.js +++ b/frontend/src/Store/Actions/trackFileActions.js @@ -1,9 +1,210 @@ +import _ from 'lodash'; +import $ from 'jquery'; import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import trackFileActionHandlers from './trackFileActionHandlers'; +import { batchActions } from 'redux-batched-actions'; +import { createThunk, handleThunks } from 'Store/thunks'; +import episodeEntities from 'Album/episodeEntities'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import { set, removeItem, updateItem } from './baseActions'; -export const fetchTrackFiles = trackFileActionHandlers[types.FETCH_TRACK_FILES]; -export const deleteTrackFile = trackFileActionHandlers[types.DELETE_TRACK_FILE]; -export const deleteTrackFiles = trackFileActionHandlers[types.DELETE_TRACK_FILES]; -export const updateTrackFiles = trackFileActionHandlers[types.UPDATE_TRACK_FILES]; -export const clearTrackFiles = createAction(types.CLEAR_TRACK_FILES); +// +// Variables + +export const section = 'trackFiles'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isSaving: false, + saveError: null, + items: [] +}; + +// +// Actions Types + +export const FETCH_TRACK_FILES = 'trackFiles/fetchTrackFiles'; +export const DELETE_TRACK_FILE = 'trackFiles/deleteTrackFile'; +export const DELETE_TRACK_FILES = 'trackFiles/deleteTrackFiles'; +export const UPDATE_TRACK_FILES = 'trackFiles/updateTrackFiles'; +export const CLEAR_TRACK_FILES = 'trackFiles/clearTrackFiles'; + +// +// Action Creators + +export const fetchTrackFiles = createThunk(FETCH_TRACK_FILES); +export const deleteTrackFile = createThunk(DELETE_TRACK_FILE); +export const deleteTrackFiles = createThunk(DELETE_TRACK_FILES); +export const updateTrackFiles = createThunk(UPDATE_TRACK_FILES); +export const clearTrackFiles = createAction(CLEAR_TRACK_FILES); + +// +// Helpers + +const deleteTrackFileHelper = createRemoveItemHandler(section, '/trackFile'); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [FETCH_TRACK_FILES]: createFetchHandler(section, '/trackFile'), + + [DELETE_TRACK_FILE]: function(getState, payload, dispatch) { + const { + id: trackFileId, + episodeEntity = episodeEntities.EPISODES + } = payload; + + const episodeSection = _.last(episodeEntity.split('.')); + const deletePromise = deleteTrackFileHelper(getState, payload, dispatch); + + deletePromise.done(() => { + const episodes = getState().episodes.items; + const tracksWithRemovedFiles = _.filter(episodes, { trackFileId }); + + dispatch(batchActions([ + ...tracksWithRemovedFiles.map((track) => { + return updateItem({ + section: episodeSection, + ...track, + trackFileId: 0, + hasFile: false + }); + }) + ])); + }); + }, + + [DELETE_TRACK_FILES]: function(getState, payload, dispatch) { + const { + trackFileIds + } = payload; + + dispatch(set({ section, isDeleting: true })); + + const promise = $.ajax({ + url: '/trackFile/bulk', + method: 'DELETE', + dataType: 'json', + data: JSON.stringify({ trackFileIds }) + }); + + promise.done(() => { + const tracks = getState().tracks.items; + const tracksWithRemovedFiles = trackFileIds.reduce((acc, trackFileId) => { + acc.push(..._.filter(tracks, { trackFileId })); + + return acc; + }, []); + + dispatch(batchActions([ + ...trackFileIds.map((id) => { + return removeItem({ section, id }); + }), + + ...tracksWithRemovedFiles.map((track) => { + return updateItem({ + section: 'tracks', + ...track, + trackFileId: 0, + hasFile: false + }); + }), + + set({ + section, + isDeleting: false, + deleteError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + }, + + [UPDATE_TRACK_FILES]: function(getState, payload, dispatch) { + const { + trackFileIds, + language, + quality + } = payload; + + dispatch(set({ section, isSaving: true })); + + const data = { + trackFileIds + }; + + if (language) { + data.language = language; + } + + if (quality) { + data.quality = quality; + } + + const promise = $.ajax({ + url: '/trackFile/editor', + method: 'PUT', + dataType: 'json', + data: JSON.stringify(data) + }); + + promise.done(() => { + dispatch(batchActions([ + ...trackFileIds.map((id) => { + const props = {}; + + if (language) { + props.language = language; + } + + if (quality) { + props.quality = quality; + } + + return updateItem({ section, id, ...props }); + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [CLEAR_TRACK_FILES]: (state) => { + return Object.assign({}, state, defaultState); + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/wantedActionHandlers.js b/frontend/src/Store/Actions/wantedActionHandlers.js deleted file mode 100644 index 60cb121b4..000000000 --- a/frontend/src/Store/Actions/wantedActionHandlers.js +++ /dev/null @@ -1,34 +0,0 @@ -import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; -import createBatchToggleAlbumMonitoredHandler from './Creators/createBatchToggleAlbumMonitoredHandler'; -import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; -import * as types from './actionTypes'; - -const wantedActionHandlers = { - ...createServerSideCollectionHandlers('missing', '/wanted/missing', (state) => state.wanted, { - [serverSideCollectionHandlers.FETCH]: types.FETCH_MISSING, - [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_MISSING_PAGE, - [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_MISSING_PAGE, - [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_MISSING_PAGE, - [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_MISSING_PAGE, - [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_MISSING_PAGE, - [serverSideCollectionHandlers.SORT]: types.SET_MISSING_SORT, - [serverSideCollectionHandlers.FILTER]: types.SET_MISSING_FILTER - }), - - [types.BATCH_TOGGLE_MISSING_ALBUMS]: createBatchToggleAlbumMonitoredHandler('missing', (state) => state.wanted.missing), - - ...createServerSideCollectionHandlers('cutoffUnmet', '/wanted/cutoff', (state) => state.wanted, { - [serverSideCollectionHandlers.FETCH]: types.FETCH_CUTOFF_UNMET, - [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_CUTOFF_UNMET_PAGE, - [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_CUTOFF_UNMET_PAGE, - [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_CUTOFF_UNMET_PAGE, - [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_CUTOFF_UNMET_PAGE, - [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_CUTOFF_UNMET_PAGE, - [serverSideCollectionHandlers.SORT]: types.SET_CUTOFF_UNMET_SORT, - [serverSideCollectionHandlers.FILTER]: types.SET_CUTOFF_UNMET_FILTER - }), - - [types.BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS]: createBatchToggleAlbumMonitoredHandler('cutoffUnmet', (state) => state.wanted.cutoffUnmet) -}; - -export default wantedActionHandlers; diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js index 75aac6e60..2de0f972e 100644 --- a/frontend/src/Store/Actions/wantedActions.js +++ b/frontend/src/Store/Actions/wantedActions.js @@ -1,35 +1,278 @@ import { createAction } from 'redux-actions'; -import * as types from './actionTypes'; -import wantedActionHandlers from './wantedActionHandlers'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createClearReducer from './Creators/Reducers/createClearReducer'; +import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; +import createBatchToggleAlbumMonitoredHandler from './Creators/createBatchToggleAlbumMonitoredHandler'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; +import createHandleActions from './Creators/createHandleActions'; // -// Missing +// Variables -export const fetchMissing = wantedActionHandlers[types.FETCH_MISSING]; -export const gotoMissingFirstPage = wantedActionHandlers[types.GOTO_FIRST_MISSING_PAGE]; -export const gotoMissingPreviousPage = wantedActionHandlers[types.GOTO_PREVIOUS_MISSING_PAGE]; -export const gotoMissingNextPage = wantedActionHandlers[types.GOTO_NEXT_MISSING_PAGE]; -export const gotoMissingLastPage = wantedActionHandlers[types.GOTO_LAST_MISSING_PAGE]; -export const gotoMissingPage = wantedActionHandlers[types.GOTO_MISSING_PAGE]; -export const setMissingSort = wantedActionHandlers[types.SET_MISSING_SORT]; -export const setMissingFilter = wantedActionHandlers[types.SET_MISSING_FILTER]; -export const setMissingTableOption = createAction(types.SET_MISSING_TABLE_OPTION); -export const clearMissing = createAction(types.CLEAR_MISSING); - -export const batchToggleMissingAlbums = wantedActionHandlers[types.BATCH_TOGGLE_MISSING_ALBUMS]; +export const section = 'wanted'; // -// Cutoff Unmet +// State -export const fetchCutoffUnmet = wantedActionHandlers[types.FETCH_CUTOFF_UNMET]; -export const gotoCutoffUnmetFirstPage = wantedActionHandlers[types.GOTO_FIRST_CUTOFF_UNMET_PAGE]; -export const gotoCutoffUnmetPreviousPage = wantedActionHandlers[types.GOTO_PREVIOUS_CUTOFF_UNMET_PAGE]; -export const gotoCutoffUnmetNextPage = wantedActionHandlers[types.GOTO_NEXT_CUTOFF_UNMET_PAGE]; -export const gotoCutoffUnmetLastPage = wantedActionHandlers[types.GOTO_LAST_CUTOFF_UNMET_PAGE]; -export const gotoCutoffUnmetPage = wantedActionHandlers[types.GOTO_CUTOFF_UNMET_PAGE]; -export const setCutoffUnmetSort = wantedActionHandlers[types.SET_CUTOFF_UNMET_SORT]; -export const setCutoffUnmetFilter = wantedActionHandlers[types.SET_CUTOFF_UNMET_FILTER]; -export const setCutoffUnmetTableOption= createAction(types.SET_CUTOFF_UNMET_TABLE_OPTION); -export const clearCutoffUnmet= createAction(types.CLEAR_CUTOFF_UNMET); +export const defaultState = { + missing: { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'releaseDate', + sortDirection: sortDirections.DESCENDING, + filterKey: 'monitored', + filterValue: 'true', + error: null, + items: [], -export const batchToggleCutoffUnmetAlbums = wantedActionHandlers[types.BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS]; + columns: [ + { + name: 'artist.sortName', + label: 'Artist Name', + isSortable: true, + isVisible: true + }, + // { + // name: 'episode', + // label: 'Episode', + // isVisible: true + // }, + { + name: 'albumTitle', + label: 'Album Title', + isVisible: true + }, + { + name: 'albumType', + label: 'Album Type', + isSortable: true, + isVisible: true + }, + { + name: 'releaseDate', + label: 'Release Date', + isSortable: true, + isVisible: true + }, + // { + // name: 'status', + // label: 'Status', + // isVisible: true + // }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] + }, + + cutoffUnmet: { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'releaseDate', + sortDirection: sortDirections.DESCENDING, + filterKey: 'monitored', + filterValue: true, + error: null, + items: [], + + columns: [ + { + name: 'artist.sortName', + label: 'Artist Name', + isSortable: true, + isVisible: true + }, + // { + // name: 'episode', + // label: 'Episode', + // isVisible: true + // }, + { + name: 'albumTitle', + label: 'Album Title', + isVisible: true + }, + { + name: 'albumType', + label: 'Album Type', + isSortable: true, + isVisible: true + }, + { + name: 'releaseDate', + label: 'Release Date', + isSortable: true, + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: false + }, + // { + // name: 'status', + // label: 'Status', + // isVisible: true + // }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] + } +}; + +export const persistState = [ + 'wanted.missing.pageSize', + 'wanted.missing.sortKey', + 'wanted.missing.sortDirection', + 'wanted.missing.filterKey', + 'wanted.missing.filterValue', + 'wanted.missing.columns', + 'wanted.cutoffUnmet.pageSize', + 'wanted.cutoffUnmet.sortKey', + 'wanted.cutoffUnmet.sortDirection', + 'wanted.cutoffUnmet.filterKey', + 'wanted.cutoffUnmet.filterValue', + 'wanted.cutoffUnmet.columns' +]; + +// +// Actions Types + +export const FETCH_MISSING = 'wanted/missing/fetchMissing'; +export const GOTO_FIRST_MISSING_PAGE = 'wanted/missing/gotoMissingFirstPage'; +export const GOTO_PREVIOUS_MISSING_PAGE = 'wanted/missing/gotoMissingPreviousPage'; +export const GOTO_NEXT_MISSING_PAGE = 'wanted/missing/gotoMissingNextPage'; +export const GOTO_LAST_MISSING_PAGE = 'wanted/missing/gotoMissingLastPage'; +export const GOTO_MISSING_PAGE = 'wanted/missing/gotoMissingPage'; +export const SET_MISSING_SORT = 'wanted/missing/setMissingSort'; +export const SET_MISSING_FILTER = 'wanted/missing/setMissingFilter'; +export const SET_MISSING_TABLE_OPTION = 'wanted/missing/setMissingTableOption'; +export const CLEAR_MISSING = 'wanted/missing/clearMissing'; + +export const BATCH_TOGGLE_MISSING_ALBUMS = 'wanted/missing/batchToggleMissingAlbums'; + +export const FETCH_CUTOFF_UNMET = 'wanted/cutoffUnmet/fetchCutoffUnmet'; +export const GOTO_FIRST_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetFirstPage'; +export const GOTO_PREVIOUS_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetPreviousPage'; +export const GOTO_NEXT_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetNextPage'; +export const GOTO_LAST_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetFastPage'; +export const GOTO_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetPage'; +export const SET_CUTOFF_UNMET_SORT = 'wanted/cutoffUnmet/setCutoffUnmetSort'; +export const SET_CUTOFF_UNMET_FILTER = 'wanted/cutoffUnmet/setCutoffUnmetFilter'; +export const SET_CUTOFF_UNMET_TABLE_OPTION = 'wanted/cutoffUnmet/setCutoffUnmetTableOption'; +export const CLEAR_CUTOFF_UNMET = 'wanted/cutoffUnmet/clearCutoffUnmet'; + +export const BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS = 'wanted/cutoffUnmet/batchToggleCutoffUnmetAlbums'; + +// +// Action Creators + +export const fetchMissing = createThunk(FETCH_MISSING); +export const gotoMissingFirstPage = createThunk(GOTO_FIRST_MISSING_PAGE); +export const gotoMissingPreviousPage = createThunk(GOTO_PREVIOUS_MISSING_PAGE); +export const gotoMissingNextPage = createThunk(GOTO_NEXT_MISSING_PAGE); +export const gotoMissingLastPage = createThunk(GOTO_LAST_MISSING_PAGE); +export const gotoMissingPage = createThunk(GOTO_MISSING_PAGE); +export const setMissingSort = createThunk(SET_MISSING_SORT); +export const setMissingFilter = createThunk(SET_MISSING_FILTER); +export const setMissingTableOption = createAction(SET_MISSING_TABLE_OPTION); +export const clearMissing = createAction(CLEAR_MISSING); + +export const batchToggleMissingAlbums = createThunk(BATCH_TOGGLE_MISSING_ALBUMS); + +export const fetchCutoffUnmet = createThunk(FETCH_CUTOFF_UNMET); +export const gotoCutoffUnmetFirstPage = createThunk(GOTO_FIRST_CUTOFF_UNMET_PAGE); +export const gotoCutoffUnmetPreviousPage = createThunk(GOTO_PREVIOUS_CUTOFF_UNMET_PAGE); +export const gotoCutoffUnmetNextPage = createThunk(GOTO_NEXT_CUTOFF_UNMET_PAGE); +export const gotoCutoffUnmetLastPage = createThunk(GOTO_LAST_CUTOFF_UNMET_PAGE); +export const gotoCutoffUnmetPage = createThunk(GOTO_CUTOFF_UNMET_PAGE); +export const setCutoffUnmetSort = createThunk(SET_CUTOFF_UNMET_SORT); +export const setCutoffUnmetFilter = createThunk(SET_CUTOFF_UNMET_FILTER); +export const setCutoffUnmetTableOption = createAction(SET_CUTOFF_UNMET_TABLE_OPTION); +export const clearCutoffUnmet = createAction(CLEAR_CUTOFF_UNMET); + +export const batchToggleCutoffUnmetAlbums = createThunk(BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + ...createServerSideCollectionHandlers( + 'wanted.missing', + '/wanted/missing', + fetchMissing, + { + [serverSideCollectionHandlers.FETCH]: FETCH_MISSING, + [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_MISSING_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_MISSING_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_MISSING_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_MISSING_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_MISSING_PAGE, + [serverSideCollectionHandlers.SORT]: SET_MISSING_SORT, + [serverSideCollectionHandlers.FILTER]: SET_MISSING_FILTER + } + ), + + [BATCH_TOGGLE_MISSING_ALBUMS]: createBatchToggleAlbumMonitoredHandler('wanted.missing'), + + ...createServerSideCollectionHandlers( + 'wanted.cutoffUnmet', + '/wanted/cutoff', + fetchCutoffUnmet, + { + [serverSideCollectionHandlers.FETCH]: FETCH_CUTOFF_UNMET, + [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.SORT]: SET_CUTOFF_UNMET_SORT, + [serverSideCollectionHandlers.FILTER]: SET_CUTOFF_UNMET_FILTER + } + ), + + [BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS]: createBatchToggleAlbumMonitoredHandler('wanted.cutoffUnmet') + +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_MISSING_TABLE_OPTION]: createSetTableOptionReducer('wanted.missing'), + [SET_CUTOFF_UNMET_TABLE_OPTION]: createSetTableOptionReducer('wanted.cutoffUnmet'), + + [CLEAR_MISSING]: createClearReducer( + 'wanted.missing', + { + isFetching: false, + isPopulated: false, + error: null, + items: [] + } + ), + + [CLEAR_CUTOFF_UNMET]: createClearReducer( + 'wanted.cutoffUnmet', + { + isFetching: false, + isPopulated: false, + error: null, + items: [] + } + ) + +}, defaultState, section); diff --git a/frontend/src/Store/Middleware/persistState.js b/frontend/src/Store/Middleware/persistState.js index 456efc470..220d4064a 100644 --- a/frontend/src/Store/Middleware/persistState.js +++ b/frontend/src/Store/Middleware/persistState.js @@ -1,47 +1,19 @@ import _ from 'lodash'; import persistState from 'redux-localstorage'; -import * as addArtistReducers from 'Store/Reducers/addArtistReducers'; -import * as episodeReducers from 'Store/Reducers/episodeReducers'; -import * as trackReducers from 'Store/Reducers/trackReducers'; -import * as artistIndexReducers from 'Store/Reducers/artistIndexReducers'; -import * as artistEditorReducers from 'Store/Reducers/artistEditorReducers'; -import * as albumStudioReducers from 'Store/Reducers/albumStudioReducers'; -import * as calendarReducers from 'Store/Reducers/calendarReducers'; -import * as historyReducers from 'Store/Reducers/historyReducers'; -import * as blacklistReducers from 'Store/Reducers/blacklistReducers'; -import * as wantedReducers from 'Store/Reducers/wantedReducers'; -import * as settingsReducers from 'Store/Reducers/settingsReducers'; -import * as systemReducers from 'Store/Reducers/systemReducers'; -import * as interactiveImportReducers from 'Store/Reducers/interactiveImportReducers'; -import * as queueReducers from 'Store/Reducers/queueReducers'; - -const reducers = [ - addArtistReducers, - episodeReducers, - trackReducers, - artistIndexReducers, - artistEditorReducers, - albumStudioReducers, - calendarReducers, - historyReducers, - blacklistReducers, - wantedReducers, - settingsReducers, - systemReducers, - interactiveImportReducers, - queueReducers -]; +import actions from 'Store/Actions'; const columnPaths = []; -const paths = _.reduce(reducers, (acc, reducer) => { - reducer.persistState.forEach((path) => { - if (path.match(/\.columns$/)) { - columnPaths.push(path); - } +const paths = _.reduce([...actions], (acc, action) => { + if (action.persistState) { + action.persistState.forEach((path) => { + if (path.match(/\.columns$/)) { + columnPaths.push(path); + } - acc.push(path); - }); + acc.push(path); + }); + } return acc; }, []); diff --git a/frontend/src/Store/Reducers/Creators/createAddItemReducer.js b/frontend/src/Store/Reducers/Creators/createAddItemReducer.js deleted file mode 100644 index d0e75c758..000000000 --- a/frontend/src/Store/Reducers/Creators/createAddItemReducer.js +++ /dev/null @@ -1,23 +0,0 @@ -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; - -function createAddItemReducer(section) { - return (state, { payload }) => { - const { - section: payloadSection, - ...otherProps - } = payload; - - if (section === payloadSection) { - const newState = getSectionState(state, section); - - newState.items = [...newState.items, { ...otherProps }]; - - return updateSectionState(state, section, newState); - } - - return state; - }; -} - -export default createAddItemReducer; diff --git a/frontend/src/Store/Reducers/Creators/createClearPendingChangesReducer.js b/frontend/src/Store/Reducers/Creators/createClearPendingChangesReducer.js deleted file mode 100644 index 6ff6e7b25..000000000 --- a/frontend/src/Store/Reducers/Creators/createClearPendingChangesReducer.js +++ /dev/null @@ -1,21 +0,0 @@ -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; - -function createClearPendingChangesReducer(section) { - return (state, { payload }) => { - if (section === payload.section) { - const newState = getSectionState(state, section); - newState.pendingChanges = {}; - - if (newState.hasOwnProperty('saveError')) { - newState.saveError = null; - } - - return updateSectionState(state, section, newState); - } - - return state; - }; -} - -export default createClearPendingChangesReducer; diff --git a/frontend/src/Store/Reducers/Creators/createReducers.js b/frontend/src/Store/Reducers/Creators/createReducers.js deleted file mode 100644 index 13ed584e8..000000000 --- a/frontend/src/Store/Reducers/Creators/createReducers.js +++ /dev/null @@ -1,20 +0,0 @@ -function createReducers(sections, createReducer) { - const reducers = {}; - - sections.forEach((section) => { - reducers[section] = createReducer(section); - }); - - return (state, action) => { - const section = action.payload.section; - const reducer = reducers[section]; - - if (reducer) { - return reducer(state, action); - } - - return state; - }; -} - -export default createReducers; diff --git a/frontend/src/Store/Reducers/Creators/createRemoveItemReducer.js b/frontend/src/Store/Reducers/Creators/createRemoveItemReducer.js deleted file mode 100644 index c09655b0c..000000000 --- a/frontend/src/Store/Reducers/Creators/createRemoveItemReducer.js +++ /dev/null @@ -1,20 +0,0 @@ -import _ from 'lodash'; -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; - -function createRemoveItemReducer(section) { - return (state, { payload }) => { - if (section === payload.section) { - const newState = getSectionState(state, section); - - newState.items = [...newState.items]; - _.remove(newState.items, { id: payload.id }); - - return updateSectionState(state, section, newState); - } - - return state; - }; -} - -export default createRemoveItemReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetReducer.js b/frontend/src/Store/Reducers/Creators/createSetReducer.js deleted file mode 100644 index 2c2126c03..000000000 --- a/frontend/src/Store/Reducers/Creators/createSetReducer.js +++ /dev/null @@ -1,23 +0,0 @@ -import _ from 'lodash'; -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; - -const blacklistedProperties = [ - 'section', - 'id' -]; - -function createSetReducer(section) { - return (state, { payload }) => { - if (section === payload.section) { - const newState = Object.assign(getSectionState(state, section), - _.omit(payload, blacklistedProperties)); - - return updateSectionState(state, section, newState); - } - - return state; - }; -} - -export default createSetReducer; diff --git a/frontend/src/Store/Reducers/Creators/createUpdateItemReducer.js b/frontend/src/Store/Reducers/Creators/createUpdateItemReducer.js deleted file mode 100644 index aba730afd..000000000 --- a/frontend/src/Store/Reducers/Creators/createUpdateItemReducer.js +++ /dev/null @@ -1,36 +0,0 @@ -import _ from 'lodash'; -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; - -function createUpdateItemReducer(section, idProp = 'id') { - return (state, { payload }) => { - const { - section: payloadSection, - updateOnly = false, - ...otherProps - } = payload; - - if (section === payloadSection) { - const newState = getSectionState(state, section); - const items = newState.items; - const index = _.findIndex(items, { [idProp]: payload[idProp] }); - - newState.items = [...items]; - - // TODO: Move adding to it's own reducer - if (index >= 0) { - const item = items[index]; - - newState.items.splice(index, 1, { ...item, ...otherProps }); - } else if (!updateOnly) { - newState.items.push({ ...otherProps }); - } - - return updateSectionState(state, section, newState); - } - - return state; - }; -} - -export default createUpdateItemReducer; diff --git a/frontend/src/Store/Reducers/Creators/createUpdateReducer.js b/frontend/src/Store/Reducers/Creators/createUpdateReducer.js deleted file mode 100644 index ea566ad9b..000000000 --- a/frontend/src/Store/Reducers/Creators/createUpdateReducer.js +++ /dev/null @@ -1,23 +0,0 @@ -import _ from 'lodash'; -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; - -function createUpdateReducer(section) { - return (state, { payload }) => { - if (section === payload.section) { - const newState = getSectionState(state, section); - - if (_.isArray(payload.data)) { - newState.items = payload.data; - } else { - newState.item = payload.data; - } - - return updateSectionState(state, section, newState); - } - - return state; - }; -} - -export default createUpdateReducer; diff --git a/frontend/src/Store/Reducers/Creators/createUpdateServerSideCollectionReducer.js b/frontend/src/Store/Reducers/Creators/createUpdateServerSideCollectionReducer.js deleted file mode 100644 index 235a1016a..000000000 --- a/frontend/src/Store/Reducers/Creators/createUpdateServerSideCollectionReducer.js +++ /dev/null @@ -1,24 +0,0 @@ -import _ from 'lodash'; -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; - -function createUpdateServerSideCollectionReducer(section) { - return (state, { payload }) => { - if (section === payload.section) { - const data = payload.data; - const newState = getSectionState(state, section); - - const serverState = _.omit(data, ['records']); - const calculatedState = { - totalPages: Math.max(Math.ceil(data.totalRecords / data.pageSize), 1), - items: data.records - }; - - return updateSectionState(state, section, Object.assign(newState, serverState, calculatedState)); - } - - return state; - }; -} - -export default createUpdateServerSideCollectionReducer; diff --git a/frontend/src/Store/Reducers/addArtistReducers.js b/frontend/src/Store/Reducers/addArtistReducers.js deleted file mode 100644 index 374c1a8d1..000000000 --- a/frontend/src/Store/Reducers/addArtistReducers.js +++ /dev/null @@ -1,68 +0,0 @@ -import { handleActions } from 'redux-actions'; -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createSetSettingValueReducer from './Creators/createSetSettingValueReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createRemoveItemReducer from './Creators/createRemoveItemReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - isAdding: false, - isAdded: false, - addError: null, - items: [], - - defaults: { - rootFolderPath: '', - monitor: 'allEpisodes', - qualityProfileId: 0, - languageProfileId: 0, - metadataProfileId: 0, - albumFolder: true, - tags: [] - } -}; - -export const persistState = [ - 'addArtist.defaults' -]; - -const reducerSection = 'addArtist'; - -const addArtistReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), - [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), - - [types.SET_ADD_ARTIST_VALUE]: createSetSettingValueReducer(reducerSection), - - [types.SET_ADD_ARTIST_DEFAULT]: function(state, { payload }) { - const newState = getSectionState(state, reducerSection); - - newState.defaults = { - ...newState.defaults, - ...payload - }; - - return updateSectionState(state, reducerSection, newState); - }, - - [types.CLEAR_ADD_ARTIST]: function(state) { - const { - defaults, - ...otherDefaultState - } = defaultState; - - return Object.assign({}, state, otherDefaultState); - } - -}, defaultState); - -export default addArtistReducers; diff --git a/frontend/src/Store/Reducers/albumHistoryReducers.js b/frontend/src/Store/Reducers/albumHistoryReducers.js deleted file mode 100644 index 44b89b1ee..000000000 --- a/frontend/src/Store/Reducers/albumHistoryReducers.js +++ /dev/null @@ -1,26 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - items: [] -}; - -const reducerSection = 'albumHistory'; - -const albumHistoryReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - - [types.CLEAR_ALBUM_HISTORY]: (state) => { - return Object.assign({}, state, defaultState); - } - -}, defaultState); - -export default albumHistoryReducers; diff --git a/frontend/src/Store/Reducers/albumStudioReducers.js b/frontend/src/Store/Reducers/albumStudioReducers.js deleted file mode 100644 index 658d02a4e..000000000 --- a/frontend/src/Store/Reducers/albumStudioReducers.js +++ /dev/null @@ -1,39 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { filterTypes, sortDirections } from 'Helpers/Props'; -import createSetReducer from './Creators/createSetReducer'; -import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; -import createSetClientSideCollectionFilterReducer from './Creators/createSetClientSideCollectionFilterReducer'; - -export const defaultState = { - isSaving: false, - saveError: null, - sortKey: 'sortName', - sortDirection: sortDirections.ASCENDING, - secondarySortKey: 'sortName', - secondarySortDirection: sortDirections.ASCENDING, - filterKey: null, - filterValue: null, - filterType: filterTypes.EQUAL -}; - -export const persistState = [ - 'albumStudio.sortKey', - 'albumStudio.sortDirection', - 'albumStudio.filterKey', - 'albumStudio.filterValue', - 'albumStudio.filterType' -]; - -const reducerSection = 'albumStudio'; - -const albumStudioReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - - [types.SET_ALBUM_STUDIO_SORT]: createSetClientSideCollectionSortReducer(reducerSection), - [types.SET_ALBUM_STUDIO_FILTER]: createSetClientSideCollectionFilterReducer(reducerSection) - -}, defaultState); - -export default albumStudioReducers; diff --git a/frontend/src/Store/Reducers/appReducers.js b/frontend/src/Store/Reducers/appReducers.js deleted file mode 100644 index f574495e3..000000000 --- a/frontend/src/Store/Reducers/appReducers.js +++ /dev/null @@ -1,74 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createRemoveItemReducer from './Creators/createRemoveItemReducer'; - -function getDimensions(width, height) { - const dimensions = { - width, - height, - isExtraSmallScreen: width <= 480, - isSmallScreen: width <= 768, - isMediumScreen: width <= 992, - isLargeScreen: width <= 1200 - }; - - return dimensions; -} - -export const defaultState = { - dimensions: getDimensions(window.innerWidth, window.innerHeight), - messages: { - items: [] - }, - version: window.Sonarr.version, - isUpdated: false, - isConnected: true, - isReconnecting: false, - isDisconnected: false, - isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen -}; - -const appReducers = handleActions({ - - [types.SAVE_DIMENSIONS]: function(state, { payload }) { - const { - width, - height - } = payload; - - const dimensions = getDimensions(width, height); - - return Object.assign({}, state, { dimensions }); - }, - - [types.SHOW_MESSAGE]: createUpdateItemReducer('messages'), - [types.HIDE_MESSAGE]: createRemoveItemReducer('messages'), - - [types.SET_APP_VALUE]: createSetReducer('app'), - [types.SET_VERSION]: function(state, { payload }) { - const version = payload.version; - - const newState = { - version - }; - - if (state.version !== version) { - newState.isUpdated = true; - } - - return Object.assign({}, state, newState); - }, - - [types.SET_IS_SIDEBAR_VISIBLE]: function(state, { payload }) { - const newState = { - isSidebarVisible: payload.isSidebarVisible - }; - - return Object.assign({}, state, newState); - } - -}, defaultState); - -export default appReducers; diff --git a/frontend/src/Store/Reducers/artistEditorReducers.js b/frontend/src/Store/Reducers/artistEditorReducers.js deleted file mode 100644 index 6eaa07fa4..000000000 --- a/frontend/src/Store/Reducers/artistEditorReducers.js +++ /dev/null @@ -1,41 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { filterTypes, sortDirections } from 'Helpers/Props'; -import createSetReducer from './Creators/createSetReducer'; -import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; -import createSetClientSideCollectionFilterReducer from './Creators/createSetClientSideCollectionFilterReducer'; - -export const defaultState = { - isSaving: false, - saveError: null, - isDeleting: false, - deleteError: null, - sortKey: 'sortName', - sortDirection: sortDirections.ASCENDING, - secondarySortKey: 'sortName', - secondarySortDirection: sortDirections.ASCENDING, - filterKey: null, - filterValue: null, - filterType: filterTypes.EQUAL -}; - -export const persistState = [ - 'artistEditor.sortKey', - 'artistEditor.sortDirection', - 'artistEditor.filterKey', - 'artistEditor.filterValue', - 'artistEditor.filterType' -]; - -const reducerSection = 'artistEditor'; - -const artistEditorReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - - [types.SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(reducerSection), - [types.SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(reducerSection) - -}, defaultState); - -export default artistEditorReducers; diff --git a/frontend/src/Store/Reducers/artistIndexReducers.js b/frontend/src/Store/Reducers/artistIndexReducers.js deleted file mode 100644 index eb057622b..000000000 --- a/frontend/src/Store/Reducers/artistIndexReducers.js +++ /dev/null @@ -1,253 +0,0 @@ -import moment from 'moment'; -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { filterTypes, sortDirections } from 'Helpers/Props'; -import createSetReducer from './Creators/createSetReducer'; -import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; -import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; -import createSetClientSideCollectionFilterReducer from './Creators/createSetClientSideCollectionFilterReducer'; - -export const defaultState = { - sortKey: 'sortName', - sortDirection: sortDirections.ASCENDING, - secondarySortKey: 'sortName', - secondarySortDirection: sortDirections.ASCENDING, - filterKey: null, - filterValue: null, - filterType: filterTypes.EQUAL, - view: 'posters', - - posterOptions: { - detailedProgressBar: false, - size: 'large', - showTitle: false, - showQualityProfile: true - }, - - bannerOptions: { - detailedProgressBar: false, - size: 'large', - showTitle: false, - showQualityProfile: true - }, - - overviewOptions: { - detailedProgressBar: false, - size: 'medium', - showNetwork: true, - showQualityProfile: true, - showPreviousAiring: false, - showAdded: false, - showAlbumCount: true, - showPath: false, - showSizeOnDisk: false - }, - - columns: [ - { - name: 'status', - columnLabel: 'Status', - isVisible: true, - isModifiable: false - }, - { - name: 'sortName', - label: 'Artist Name', - isSortable: true, - isVisible: true, - isModifiable: false - }, - { - name: 'artistType', - label: 'Type', - isSortable: true, - isVisible: true, - isModifiable: false - }, - { - name: 'qualityProfileId', - label: 'Quality Profile', - isSortable: true, - isVisible: true - }, - { - name: 'languageProfileId', - label: 'Language Profile', - isSortable: true, - isVisible: false - }, - { - name: 'metadataProfileId', - label: 'Metadata Profile', - isSortable: true, - isVisible: false - }, - { - name: 'nextAiring', - label: 'Next Airing', - isSortable: true, - isVisible: true - }, - { - name: 'previousAiring', - label: 'Previous Airing', - isSortable: true, - isVisible: false - }, - { - name: 'added', - label: 'Added', - isSortable: true, - isVisible: false - }, - { - name: 'albumCount', - label: 'Albums', - isSortable: true, - isVisible: true - }, - { - name: 'trackProgress', - label: 'Tracks', - isSortable: true, - isVisible: true - }, - { - name: 'trackCount', - label: 'Track Count', - isSortable: true, - isVisible: false - }, - { - name: 'latestAlbum', - label: 'Latest Album', - isSortable: true, - isVisible: false - }, - { - name: 'path', - label: 'Path', - isSortable: true, - isVisible: false - }, - { - name: 'sizeOnDisk', - label: 'Size on Disk', - isSortable: true, - isVisible: false - }, - { - name: 'tags', - label: 'Tags', - isSortable: false, - isVisible: false - }, - { - name: 'actions', - columnLabel: 'Actions', - isVisible: true, - isModifiable: false - } - ], - - sortPredicates: { - nextAiring: function(item, direction) { - const nextAiring = item.nextAiring; - - if (nextAiring) { - return moment(nextAiring).unix(); - } - - if (direction === sortDirections.DESCENDING) { - return 0; - } - - return Number.MAX_VALUE; - }, - - trackProgress: function(item) { - const { - trackCount = 0, - trackFileCount - } = item; - - const progress = trackCount ? trackFileCount / trackCount * 100 : 100; - - return progress + trackCount / 1000000; - } - }, - - filterPredicates: { - missing: function(item) { - return item.trackCount - item.trackFileCount > 0; - } - } -}; - -export const persistState = [ - 'artistIndex.sortKey', - 'artistIndex.sortDirection', - 'artistIndex.filterKey', - 'artistIndex.filterValue', - 'artistIndex.filterType', - 'artistIndex.view', - 'artistIndex.columns', - 'artistIndex.posterOptions', - 'artistIndex.bannerOptions', - 'artistIndex.overviewOptions' -]; - -const reducerSection = 'artistIndex'; - -const artistIndexReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - - [types.SET_ARTIST_SORT]: createSetClientSideCollectionSortReducer(reducerSection), - [types.SET_ARTIST_FILTER]: createSetClientSideCollectionFilterReducer(reducerSection), - - [types.SET_ARTIST_VIEW]: function(state, { payload }) { - return Object.assign({}, state, { view: payload.view }); - }, - - [types.SET_ARTIST_TABLE_OPTION]: createSetTableOptionReducer(reducerSection), - - [types.SET_ARTIST_POSTER_OPTION]: function(state, { payload }) { - const posterOptions = state.posterOptions; - - return { - ...state, - posterOptions: { - ...posterOptions, - ...payload - } - }; - }, - - [types.SET_ARTIST_BANNER_OPTION]: function(state, { payload }) { - const bannerOptions = state.bannerOptions; - - return { - ...state, - bannerOptions: { - ...bannerOptions, - ...payload - } - }; - }, - - [types.SET_ARTIST_OVERVIEW_OPTION]: function(state, { payload }) { - const overviewOptions = state.overviewOptions; - - return { - ...state, - overviewOptions: { - ...overviewOptions, - ...payload - } - }; - } - -}, defaultState); - -export default artistIndexReducers; diff --git a/frontend/src/Store/Reducers/artistReducers.js b/frontend/src/Store/Reducers/artistReducers.js deleted file mode 100644 index c60b6d9e6..000000000 --- a/frontend/src/Store/Reducers/artistReducers.js +++ /dev/null @@ -1,37 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { sortDirections } from 'Helpers/Props'; -import createSetReducer from './Creators/createSetReducer'; -import createSetSettingValueReducer from './Creators/createSetSettingValueReducer'; -import createClearPendingChangesReducer from './Creators/createClearPendingChangesReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createRemoveItemReducer from './Creators/createRemoveItemReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - isSaving: false, - saveError: null, - items: [], - sortKey: 'sortName', - sortDirection: sortDirections.ASCENDING, - pendingChanges: {} -}; - -const reducerSection = 'artist'; - -const artistReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), - [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), - - [types.SET_ARTIST_VALUE]: createSetSettingValueReducer(reducerSection), - [types.CLEAR_PENDING_CHANGES]: createClearPendingChangesReducer(reducerSection) - -}, defaultState); - -export default artistReducers; diff --git a/frontend/src/Store/Reducers/blacklistReducers.js b/frontend/src/Store/Reducers/blacklistReducers.js deleted file mode 100644 index bee1f12d6..000000000 --- a/frontend/src/Store/Reducers/blacklistReducers.js +++ /dev/null @@ -1,80 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { sortDirections } from 'Helpers/Props'; -import createSetReducer from './Creators/createSetReducer'; -import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; - -const reducerSection = 'blacklist'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - pageSize: 20, - sortKey: 'date', - sortDirection: sortDirections.DESCENDING, - error: null, - items: [], - - columns: [ - { - name: 'artist.sortName', - label: 'Artist Name', - isSortable: true, - isVisible: true - }, - { - name: 'sourceTitle', - label: 'Source Title', - isSortable: true, - isVisible: true - }, - { - name: 'language', - label: 'Language', - isVisible: false - }, - { - name: 'quality', - label: 'Quality', - isVisible: true - }, - { - name: 'date', - label: 'Date', - isSortable: true, - isVisible: true - }, - { - name: 'indexer', - label: 'Indexer', - isSortable: true, - isVisible: false - }, - { - name: 'details', - columnLabel: 'Details', - isVisible: true, - isModifiable: false - } - ] -}; - -export const persistState = [ - 'blacklist.pageSize', - 'blacklist.sortKey', - 'blacklist.sortDirection', - 'blacklist.columns' -]; - -const blacklistReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - [types.UPDATE_SERVER_SIDE_COLLECTION]: createUpdateServerSideCollectionReducer(reducerSection), - [types.SET_BLACKLIST_TABLE_OPTION]: createSetTableOptionReducer(reducerSection) - -}, defaultState); - -export default blacklistReducers; diff --git a/frontend/src/Store/Reducers/calendarReducers.js b/frontend/src/Store/Reducers/calendarReducers.js deleted file mode 100644 index 4dea88e97..000000000 --- a/frontend/src/Store/Reducers/calendarReducers.js +++ /dev/null @@ -1,48 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - start: null, - end: null, - dates: [], - dayCount: 7, - view: window.innerWidth > 768 ? 'week' : 'day', - unmonitored: false, - showUpcoming: true, - error: null, - items: [] -}; - -export const persistState = [ - 'calendar.view', - 'calendar.unmonitored', - 'calendar.showUpcoming' -]; - -const section = 'calendar'; - -const calendarReducers = handleActions({ - - [types.SET]: createSetReducer(section), - [types.UPDATE]: createUpdateReducer(section), - [types.UPDATE_ITEM]: createUpdateItemReducer(section), - - [types.CLEAR_CALENDAR]: (state) => { - const { - view, - unmonitored, - showUpcoming, - ...otherDefaultState - } = defaultState; - - return Object.assign({}, state, otherDefaultState); - } - -}, defaultState); - -export default calendarReducers; diff --git a/frontend/src/Store/Reducers/captchaReducers.js b/frontend/src/Store/Reducers/captchaReducers.js deleted file mode 100644 index 67372839f..000000000 --- a/frontend/src/Store/Reducers/captchaReducers.js +++ /dev/null @@ -1,32 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; - -export const defaultState = { - refreshing: false, - token: null, - siteKey: null, - secretToken: null, - ray: null, - stoken: null, - responseUrl: null -}; - -const section = 'captcha'; - -const captchaReducers = handleActions({ - - [types.SET_CAPTCHA_VALUE]: function(state, { payload }) { - const newState = Object.assign(getSectionState(state, section), payload); - - return updateSectionState(state, section, newState); - }, - - [types.RESET_CAPTCHA]: function(state) { - return updateSectionState(state, section, defaultState); - } - -}, defaultState); - -export default captchaReducers; diff --git a/frontend/src/Store/Reducers/commandReducers.js b/frontend/src/Store/Reducers/commandReducers.js deleted file mode 100644 index b2b474e65..000000000 --- a/frontend/src/Store/Reducers/commandReducers.js +++ /dev/null @@ -1,64 +0,0 @@ -import _ from 'lodash'; -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - items: [], - handlers: {} -}; - -const reducerSection = 'commands'; - -const commandReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), - - [types.ADD_COMMAND]: (state, { payload }) => { - const newState = Object.assign({}, state); - newState.items = [...state.items, payload]; - - return newState; - }, - - [types.REMOVE_COMMAND]: (state, { payload }) => { - const newState = Object.assign({}, state); - newState.items = [...state.items]; - - const index = _.findIndex(newState.items, { id: payload.id }); - - if (index > -1) { - newState.items.splice(index, 1); - } - - return newState; - }, - - [types.REGISTER_FINISH_COMMAND_HANDLER]: (state, { payload }) => { - const newState = Object.assign({}, state); - - newState.handlers[payload.key] = { - name: payload.name, - handler: payload.handler - }; - - return newState; - }, - - [types.UNREGISTER_FINISH_COMMAND_HANDLER]: (state, { payload }) => { - const newState = Object.assign({}, state); - delete newState.handlers[payload.key]; - - return newState; - } - -}, defaultState); - -export default commandReducers; diff --git a/frontend/src/Store/Reducers/episodeReducers.js b/frontend/src/Store/Reducers/episodeReducers.js deleted file mode 100644 index dcd572368..000000000 --- a/frontend/src/Store/Reducers/episodeReducers.js +++ /dev/null @@ -1,96 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { sortDirections } from 'Helpers/Props'; -import createSetReducer from './Creators/createSetReducer'; -import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - sortKey: 'releaseDate', - sortDirection: sortDirections.DESCENDING, - items: [], - - columns: [ - { - name: 'monitored', - columnLabel: 'Monitored', - isVisible: true, - isModifiable: false - }, - { - name: 'title', - label: 'Title', - isVisible: true - }, - { - name: 'path', - label: 'Path', - isVisible: false - }, - { - name: 'releaseDate', - label: 'Release Date', - isVisible: true - }, - { - name: 'mediumCount', - label: 'Media Count', - isVisible: false - }, - { - name: 'trackCount', - label: 'Track Count', - isVisible: false - }, - { - name: 'duration', - label: 'Duration', - isVisible: false - }, - { - name: 'status', - label: 'Status', - isVisible: true - }, - { - name: 'actions', - columnLabel: 'Actions', - isVisible: true, - isModifiable: false - } - ] -}; - -export const persistState = [ - 'episodes.columns' -]; - -const reducerSection = 'episodes'; - -const episodeReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), - - [types.SET_EPISODES_TABLE_OPTION]: createSetTableOptionReducer(reducerSection), - - [types.CLEAR_EPISODES]: (state) => { - return Object.assign({}, state, { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }); - }, - - [types.SET_EPISODES_SORT]: createSetClientSideCollectionSortReducer(reducerSection) - -}, defaultState); - -export default episodeReducers; diff --git a/frontend/src/Store/Reducers/historyReducers.js b/frontend/src/Store/Reducers/historyReducers.js deleted file mode 100644 index 8104c3814..000000000 --- a/frontend/src/Store/Reducers/historyReducers.js +++ /dev/null @@ -1,113 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { sortDirections } from 'Helpers/Props'; -import createClearReducer from './Creators/createClearReducer'; -import createSetReducer from './Creators/createSetReducer'; -import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - pageSize: 20, - sortKey: 'date', - sortDirection: sortDirections.DESCENDING, - filterKey: null, - filterValue: null, - items: [], - - columns: [ - { - name: 'eventType', - columnLabel: 'Event Type', - isVisible: true, - isModifiable: false - }, - { - name: 'artist.sortName', - label: 'Artist', - isSortable: true, - isVisible: true - }, - { - name: 'episodeTitle', - label: 'Album Title', - isVisible: true - }, - { - name: 'trackTitle', - label: 'Track Title', - isVisible: true - }, - { - name: 'language', - label: 'Language', - isVisible: false - }, - { - name: 'quality', - label: 'Quality', - isVisible: true - }, - { - name: 'date', - label: 'Date', - isSortable: true, - isVisible: true - }, - { - name: 'downloadClient', - label: 'Download Client', - isVisible: false - }, - { - name: 'indexer', - label: 'Indexer', - isVisible: false - }, - { - name: 'releaseGroup', - label: 'Release Group', - isVisible: false - }, - { - name: 'details', - columnLabel: 'Details', - isVisible: true, - isModifiable: false - } - ] -}; - -export const persistState = [ - 'history.pageSize', - 'history.sortKey', - 'history.sortDirection', - 'history.filterKey', - 'history.filterValue' -]; - -const serverSideCollectionName = 'history'; - -const historyReducers = handleActions({ - - [types.SET]: createSetReducer(serverSideCollectionName), - [types.UPDATE]: createUpdateReducer(serverSideCollectionName), - [types.UPDATE_ITEM]: createUpdateItemReducer(serverSideCollectionName), - [types.UPDATE_SERVER_SIDE_COLLECTION]: createUpdateServerSideCollectionReducer(serverSideCollectionName), - - [types.SET_HISTORY_TABLE_OPTION]: createSetTableOptionReducer(serverSideCollectionName), - - [types.CLEAR_HISTORY]: createClearReducer('history', { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }) - -}, defaultState); - -export default historyReducers; diff --git a/frontend/src/Store/Reducers/importArtistReducers.js b/frontend/src/Store/Reducers/importArtistReducers.js deleted file mode 100644 index fddf32838..000000000 --- a/frontend/src/Store/Reducers/importArtistReducers.js +++ /dev/null @@ -1,35 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createRemoveItemReducer from './Creators/createRemoveItemReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - isImporting: false, - isImported: false, - importError: null, - items: [] -}; - -const reducerSection = 'importArtist'; - -const importArtistReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), - [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), - - [types.CLEAR_IMPORT_ARTIST]: function(state) { - return Object.assign({}, state, defaultState); - }, - - [types.SET_IMPORT_ARTIST_VALUE]: createUpdateItemReducer(reducerSection) - -}, defaultState); - -export default importArtistReducers; diff --git a/frontend/src/Store/Reducers/index.js b/frontend/src/Store/Reducers/index.js deleted file mode 100644 index 917419459..000000000 --- a/frontend/src/Store/Reducers/index.js +++ /dev/null @@ -1,91 +0,0 @@ -import { combineReducers } from 'redux'; -import { enableBatching } from 'redux-batched-actions'; -import { routerReducer } from 'react-router-redux'; -import app, { defaultState as defaultappState } from './appReducers'; -import addArtist, { defaultState as defaultAddArtistState } from './addArtistReducers'; -import importArtist, { defaultState as defaultImportArtistState } from './importArtistReducers'; -import artist, { defaultState as defaultArtistState } from './artistReducers'; -import artistIndex, { defaultState as defaultArtistIndexState } from './artistIndexReducers'; -import artistEditor, { defaultState as defaultArtistEditorState } from './artistEditorReducers'; -import albumStudio, { defaultState as defaultAlbumStudioState } from './albumStudioReducers'; -import calendar, { defaultState as defaultCalendarState } from './calendarReducers'; -import history, { defaultState as defaultHistoryState } from './historyReducers'; -import queue, { defaultState as defaultQueueState } from './queueReducers'; -import blacklist, { defaultState as defaultBlacklistState } from './blacklistReducers'; -import episodes, { defaultState as defaultEpisodesState } from './episodeReducers'; -import tracks, { defaultState as defaultTracksState } from './trackReducers'; -import trackFiles, { defaultState as defaultTrackFilesState } from './trackFileReducers'; -import albumHistory, { defaultState as defaultAlbumHistoryState } from './albumHistoryReducers'; -import releases, { defaultState as defaultReleasesState } from './releaseReducers'; -import wanted, { defaultState as defaultWantedState } from './wantedReducers'; -import settings, { defaultState as defaultSettingsState } from './settingsReducers'; -import system, { defaultState as defaultSystemState } from './systemReducers'; -import commands, { defaultState as defaultCommandsState } from './commandReducers'; -import paths, { defaultState as defaultPathsState } from './pathReducers'; -import tags, { defaultState as defaultTagsState } from './tagReducers'; -import captcha, { defaultState as defaultCaptchaState } from './captchaReducers'; -import oAuth, { defaultState as defaultOAuthState } from './oAuthReducers'; -import interactiveImport, { defaultState as defaultInteractiveImportState } from './interactiveImportReducers'; -import rootFolders, { defaultState as defaultRootFoldersState } from './rootFolderReducers'; -import organizePreview, { defaultState as defaultOrganizePreviewState } from './organizePreviewReducers'; - -export const defaultState = { - app: defaultappState, - addArtist: defaultAddArtistState, - importArtist: defaultImportArtistState, - artist: defaultArtistState, - artistIndex: defaultArtistIndexState, - artistEditor: defaultArtistEditorState, - albumStudio: defaultAlbumStudioState, - calendar: defaultCalendarState, - history: defaultHistoryState, - queue: defaultQueueState, - blacklist: defaultBlacklistState, - episodes: defaultEpisodesState, - tracks: defaultTracksState, - trackFiles: defaultTrackFilesState, - albumHistory: defaultAlbumHistoryState, - releases: defaultReleasesState, - wanted: defaultWantedState, - settings: defaultSettingsState, - system: defaultSystemState, - commands: defaultCommandsState, - paths: defaultPathsState, - tags: defaultTagsState, - captcha: defaultCaptchaState, - oAuth: defaultOAuthState, - interactiveImport: defaultInteractiveImportState, - rootFolders: defaultRootFoldersState, - organizePreview: defaultOrganizePreviewState -}; - -export default enableBatching(combineReducers({ - app, - addArtist, - importArtist, - artist, - artistIndex, - artistEditor, - albumStudio, - calendar, - history, - queue, - blacklist, - episodes, - tracks, - trackFiles, - albumHistory, - releases, - wanted, - settings, - system, - commands, - paths, - tags, - captcha, - oAuth, - interactiveImport, - rootFolders, - organizePreview, - routing: routerReducer -})); diff --git a/frontend/src/Store/Reducers/interactiveImportReducers.js b/frontend/src/Store/Reducers/interactiveImportReducers.js deleted file mode 100644 index b1230befe..000000000 --- a/frontend/src/Store/Reducers/interactiveImportReducers.js +++ /dev/null @@ -1,119 +0,0 @@ -import _ from 'lodash'; -import moment from 'moment'; -import { handleActions } from 'redux-actions'; -import updateSectionState from 'Utilities/State/updateSectionState'; -import * as types from 'Store/Actions/actionTypes'; -import { sortDirections } from 'Helpers/Props'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createReducers from './Creators/createReducers'; -import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - items: [], - sortKey: 'quality', - sortDirection: sortDirections.DESCENDING, - recentFolders: [], - importMode: 'move', - sortPredicates: { - artist: function(item, direction) { - const artist = item.artist; - - return artist ? artist.sortName : ''; - }, - - quality: function(item, direction) { - return item.quality.qualityWeight; - } - }, - - interactiveImportAlbums: { - isFetching: false, - isPopulated: false, - error: null, - sortKey: 'albumTitle', - sortDirection: sortDirections.DESCENDING, - items: [] - } -}; - -export const persistState = [ - 'interactiveImport.recentFolders', - 'interactiveImport.importMode' -]; - -const reducerSection = 'interactiveImport'; -const episodesSection = 'interactiveImportAlbums'; - -const interactiveImportReducers = handleActions({ - - [types.SET]: createReducers([reducerSection, episodesSection], createSetReducer), - [types.UPDATE]: createReducers([reducerSection, episodesSection], createUpdateReducer), - - [types.UPDATE_INTERACTIVE_IMPORT_ITEM]: (state, { payload }) => { - const id = payload.id; - const newState = Object.assign({}, state); - const items = newState.items; - const index = _.findIndex(items, { id }); - const item = Object.assign({}, items[index], payload); - - newState.items = [...items]; - newState.items.splice(index, 1, item); - - return newState; - }, - - [types.ADD_RECENT_FOLDER]: function(state, { payload }) { - const folder = payload.folder; - const recentFolder = { folder, lastUsed: moment().toISOString() }; - const recentFolders = [...state.recentFolders]; - const index = _.findIndex(recentFolders, { folder }); - - if (index > -1) { - recentFolders.splice(index, 1, recentFolder); - } else { - recentFolders.push(recentFolder); - } - - return Object.assign({}, state, { recentFolders }); - }, - - [types.REMOVE_RECENT_FOLDER]: function(state, { payload }) { - const folder = payload.folder; - const recentFolders = _.remove([...state.recentFolders], { folder }); - - return Object.assign({}, state, { recentFolders }); - }, - - [types.CLEAR_INTERACTIVE_IMPORT]: function(state) { - const newState = { - ...defaultState, - recentFolders: state.recentFolders, - importMode: state.importMode - }; - - return newState; - }, - - [types.SET_INTERACTIVE_IMPORT_SORT]: createSetClientSideCollectionSortReducer(reducerSection), - - [types.SET_INTERACTIVE_IMPORT_MODE]: function(state, { payload }) { - return Object.assign({}, state, { importMode: payload.importMode }); - }, - - [types.SET_INTERACTIVE_IMPORT_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(episodesSection), - - [types.CLEAR_INTERACTIVE_IMPORT_ALBUMS]: (state) => { - const section = episodesSection; - - return updateSectionState(state, section, { - ...defaultState.interactiveImportAlbums - }); - } - -}, defaultState); - -export default interactiveImportReducers; diff --git a/frontend/src/Store/Reducers/oAuthReducers.js b/frontend/src/Store/Reducers/oAuthReducers.js deleted file mode 100644 index 291faf285..000000000 --- a/frontend/src/Store/Reducers/oAuthReducers.js +++ /dev/null @@ -1,28 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; - -export const defaultState = { - authorizing: false, - accessToken: null, - accessTokenSecret: null -}; - -const section = 'oAuth'; - -const oAuthReducers = handleActions({ - - [types.SET_OAUTH_VALUE]: function(state, { payload }) { - const newState = Object.assign(getSectionState(state, section), payload); - - return updateSectionState(state, section, newState); - }, - - [types.RESET_OAUTH]: function(state) { - return updateSectionState(state, section, defaultState); - } - -}, defaultState); - -export default oAuthReducers; diff --git a/frontend/src/Store/Reducers/organizePreviewReducers.js b/frontend/src/Store/Reducers/organizePreviewReducers.js deleted file mode 100644 index c4e182c09..000000000 --- a/frontend/src/Store/Reducers/organizePreviewReducers.js +++ /dev/null @@ -1,26 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - items: [] -}; - -const reducerSection = 'organizePreview'; - -const organizePreviewReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - - [types.CLEAR_ORGANIZE_PREVIEW]: (state) => { - return Object.assign({}, state, defaultState); - } - -}, defaultState); - -export default organizePreviewReducers; diff --git a/frontend/src/Store/Reducers/pathReducers.js b/frontend/src/Store/Reducers/pathReducers.js deleted file mode 100644 index c10ad89c7..000000000 --- a/frontend/src/Store/Reducers/pathReducers.js +++ /dev/null @@ -1,45 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; - -export const defaultState = { - currentPath: '', - isPopulated: false, - isFetching: false, - error: null, - directories: [], - files: [], - parent: null -}; - -const reducerSection = 'paths'; - -const pathReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - - [types.UPDATE_PATHS]: (state, { payload }) => { - const newState = Object.assign({}, state); - - newState.currentPath = payload.path; - newState.directories = payload.directories; - newState.files = payload.files; - newState.parent = payload.parent; - - return newState; - }, - - [types.CLEAR_PATHS]: (state, { payload }) => { - const newState = Object.assign({}, state); - - newState.path = ''; - newState.directories = []; - newState.files = []; - newState.parent = ''; - - return newState; - } - -}, defaultState); - -export default pathReducers; diff --git a/frontend/src/Store/Reducers/queueReducers.js b/frontend/src/Store/Reducers/queueReducers.js deleted file mode 100644 index cd129741d..000000000 --- a/frontend/src/Store/Reducers/queueReducers.js +++ /dev/null @@ -1,141 +0,0 @@ -import { handleActions } from 'redux-actions'; -import { sortDirections } from 'Helpers/Props'; -import * as types from 'Store/Actions/actionTypes'; -import createClearReducer from './Creators/createClearReducer'; -import createSetReducer from './Creators/createSetReducer'; -import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createReducers from './Creators/createReducers'; -import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; - -export const defaultState = { - queueStatus: { - isFetching: false, - isPopulated: false, - error: null, - item: {} - }, - - details: { - isFetching: false, - isPopulated: false, - error: null, - items: [], - params: {} - }, - - paged: { - isFetching: false, - isPopulated: false, - pageSize: 20, - sortKey: 'timeleft', - sortDirection: sortDirections.ASCENDING, - error: null, - items: [], - isGrabbing: false, - isRemoving: false, - - columns: [ - { - name: 'status', - columnLabel: 'Status', - isVisible: true, - isModifiable: false - }, - { - name: 'artist.sortName', - label: 'Artist', - isSortable: true, - isVisible: true - }, - { - name: 'episodeTitle', - label: 'Album Title', - isVisible: true - }, - { - name: 'quality', - label: 'Quality', - isSortable: true, - isVisible: true - }, - { - name: 'protocol', - label: 'Protocol', - isVisible: false - }, - { - name: 'indexer', - label: 'Indexer', - isVisible: false - }, - { - name: 'downloadClient', - label: 'Download Client', - isVisible: false - }, - { - name: 'estimatedCompletionTime', - label: 'Timeleft', - isSortable: true, - isVisible: true - }, - { - name: 'progress', - label: 'Progress', - isSortable: true, - isVisible: true - }, - { - name: 'actions', - columnLabel: 'Actions', - isVisible: true, - isModifiable: false - } - ] - }, - - queueEpisodes: { - isPopulated: false, - items: [] - } -}; - -export const persistState = [ - 'queue.paged.pageSize', - 'queue.paged.sortKey', - 'queue.paged.sortDirection', - 'queue.paged.columns' -]; - -const propertyNames = [ - 'queueStatus', - 'details', - 'episodes' -]; - -const paged = 'paged'; - -const queueReducers = handleActions({ - - [types.SET]: createReducers([...propertyNames, paged], createSetReducer), - [types.UPDATE]: createReducers([...propertyNames, paged], createUpdateReducer), - [types.UPDATE_ITEM]: createReducers(['queueEpisodes', paged], createUpdateItemReducer), - - [types.CLEAR_QUEUE_DETAILS]: createClearReducer('details', defaultState.details), - - [types.UPDATE_SERVER_SIDE_COLLECTION]: createUpdateServerSideCollectionReducer(paged), - - [types.SET_QUEUE_TABLE_OPTION]: createSetTableOptionReducer(paged), - - [types.CLEAR_QUEUE]: createClearReducer('paged', { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }) - -}, defaultState); - -export default queueReducers; diff --git a/frontend/src/Store/Reducers/releaseReducers.js b/frontend/src/Store/Reducers/releaseReducers.js deleted file mode 100644 index 237049578..000000000 --- a/frontend/src/Store/Reducers/releaseReducers.js +++ /dev/null @@ -1,71 +0,0 @@ -import _ from 'lodash'; -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { sortDirections } from 'Helpers/Props'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - items: [], - sortKey: 'releaseWeight', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - peers: function(item, direction) { - const seeders = item.seeders || 0; - const leechers = item.leechers || 0; - - return seeders * 1000000 + leechers; - }, - - rejections: function(item, direction) { - const rejections = item.rejections; - const releaseWeight = item.releaseWeight; - - if (rejections.length !== 0) { - return releaseWeight + 1000000; - } - - return releaseWeight; - } - } -}; - -const reducerSection = 'releases'; - -const releaseReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - - [types.CLEAR_RELEASES]: (state) => { - return Object.assign({}, state, defaultState); - }, - - [types.UPDATE_RELEASE]: (state, { payload }) => { - const guid = payload.guid; - const newState = Object.assign({}, state); - const items = newState.items; - - // Return early if there aren't any items (the user closed the modal) - if (!items.length) { - return; - } - - const index = _.findIndex(items, { guid }); - const item = Object.assign({}, items[index], payload); - - newState.items = [...items]; - newState.items.splice(index, 1, item); - - return newState; - }, - - [types.SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(reducerSection) - -}, defaultState); - -export default releaseReducers; diff --git a/frontend/src/Store/Reducers/rootFolderReducers.js b/frontend/src/Store/Reducers/rootFolderReducers.js deleted file mode 100644 index bd5c18bfa..000000000 --- a/frontend/src/Store/Reducers/rootFolderReducers.js +++ /dev/null @@ -1,28 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createRemoveItemReducer from './Creators/createRemoveItemReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - isSaving: false, - saveError: null, - items: [] -}; - -const reducerSection = 'rootFolders'; - -const rootFolderReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), - [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection) - -}, defaultState); - -export default rootFolderReducers; diff --git a/frontend/src/Store/Reducers/settingsReducers.js b/frontend/src/Store/Reducers/settingsReducers.js deleted file mode 100644 index 170897eb0..000000000 --- a/frontend/src/Store/Reducers/settingsReducers.js +++ /dev/null @@ -1,368 +0,0 @@ -import _ from 'lodash'; -import { handleActions } from 'redux-actions'; -import getSectionState from 'Utilities/State/getSectionState'; -import selectProviderSchema from 'Utilities/State/selectProviderSchema'; -import updateSectionState from 'Utilities/State/updateSectionState'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createSetSettingValueReducer from './Creators/createSetSettingValueReducer'; -import createSetProviderFieldValueReducer from './Creators/createSetProviderFieldValueReducer'; -import createClearPendingChangesReducer from './Creators/createClearPendingChangesReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createRemoveItemReducer from './Creators/createRemoveItemReducer'; -import createReducers from './Creators/createReducers'; - -export const defaultState = { - ui: { - isFetching: false, - isPopulated: false, - error: null, - pendingChanges: {}, - isSaving: false, - saveError: null, - item: {} - }, - - mediaManagement: { - isFetching: false, - isPopulated: false, - error: null, - pendingChanges: {}, - isSaving: false, - saveError: null, - item: {} - }, - - naming: { - isFetching: false, - isPopulated: false, - error: null, - pendingChanges: {}, - isSaving: false, - saveError: null, - item: {} - }, - - namingExamples: { - isFetching: false, - isPopulated: false, - error: null, - item: {} - }, - - qualityProfiles: { - isFetching: false, - isPopulated: false, - error: null, - isDeleting: false, - deleteError: null, - isFetchingSchema: false, - schemaPopulated: false, - schemaError: null, - schema: {}, - isSaving: false, - saveError: null, - items: [], - pendingChanges: {} - }, - - languageProfiles: { - isFetching: false, - isPopulated: false, - error: null, - isDeleting: false, - deleteError: null, - isFetchingSchema: false, - schemaPopulated: false, - schemaError: null, - schema: {}, - isSaving: false, - saveError: null, - items: [], - pendingChanges: {} - }, - - metadataProfiles: { - isFetching: false, - isPopulated: false, - error: null, - isDeleting: false, - deleteError: null, - isFetchingSchema: false, - schemaPopulated: false, - schemaError: null, - schema: {}, - isSaving: false, - saveError: null, - items: [], - pendingChanges: {} - }, - - delayProfiles: { - isFetching: false, - isPopulated: false, - error: null, - items: [], - isSaving: false, - saveError: null, - pendingChanges: {} - }, - - qualityDefinitions: { - isFetching: false, - isPopulated: false, - error: null, - items: [], - isSaving: false, - saveError: null, - pendingChanges: {} - }, - - indexers: { - isFetching: false, - isPopulated: false, - error: null, - isFetchingSchema: false, - schemaPopulated: false, - schemaError: null, - schema: [], - selectedSchema: {}, - isSaving: false, - saveError: null, - isTesting: false, - items: [], - pendingChanges: {} - }, - - indexerOptions: { - isFetching: false, - isPopulated: false, - error: null, - pendingChanges: {}, - isSaving: false, - saveError: null, - item: {} - }, - - restrictions: { - isFetching: false, - isPopulated: false, - error: null, - isSaving: false, - saveError: null, - items: [], - pendingChanges: {} - }, - - downloadClients: { - isFetching: false, - isPopulated: false, - error: null, - isFetchingSchema: false, - schemaPopulated: false, - schemaError: null, - schema: [], - selectedSchema: {}, - isSaving: false, - saveError: null, - isTesting: false, - items: [], - pendingChanges: {} - }, - - downloadClientOptions: { - isFetching: false, - isPopulated: false, - error: null, - pendingChanges: {}, - isSaving: false, - saveError: null, - item: {} - }, - - remotePathMappings: { - isFetching: false, - isPopulated: false, - error: null, - items: [], - isSaving: false, - saveError: null, - pendingChanges: {} - }, - - notifications: { - isFetching: false, - isPopulated: false, - error: null, - isFetchingSchema: false, - schemaPopulated: false, - schemaError: null, - schema: [], - selectedSchema: {}, - isSaving: false, - saveError: null, - isTesting: false, - items: [], - pendingChanges: {} - }, - - metadata: { - isFetching: false, - isPopulated: false, - error: null, - isSaving: false, - saveError: null, - items: [], - pendingChanges: {} - }, - - metadataProvider: { - isFetching: false, - isPopulated: false, - error: null, - pendingChanges: {}, - isSaving: false, - saveError: null, - item: {} - }, - - general: { - isFetching: false, - isPopulated: false, - error: null, - pendingChanges: {}, - isSaving: false, - saveError: null, - item: {} - }, - - advancedSettings: false -}; - -export const persistState = [ - 'settings.advancedSettings' -]; - -const propertyNames = [ - 'ui', - 'mediaManagement', - 'naming', - 'namingExamples', - 'qualityDefinitions', - 'indexerOptions', - 'downloadClientOptions', - 'general', - 'metadataProvider' -]; - -const providerPropertyNames = [ - 'qualityProfiles', - 'languageProfiles', - 'metadataProfiles', - 'delayProfiles', - 'indexers', - 'restrictions', - 'downloadClients', - 'remotePathMappings', - 'notifications', - 'metadata' -]; - -const settingsReducers = handleActions({ - - [types.TOGGLE_ADVANCED_SETTINGS]: (state, { payload }) => { - return Object.assign({}, state, { advancedSettings: !state.advancedSettings }); - }, - - [types.SET]: createReducers([...propertyNames, ...providerPropertyNames], createSetReducer), - [types.UPDATE]: createReducers([...propertyNames, ...providerPropertyNames], createUpdateReducer), - [types.UPDATE_ITEM]: createReducers(providerPropertyNames, createUpdateItemReducer), - [types.CLEAR_PENDING_CHANGES]: createReducers([...propertyNames, ...providerPropertyNames], createClearPendingChangesReducer), - - [types.REMOVE_ITEM]: createReducers(providerPropertyNames, createRemoveItemReducer), - - [types.SET_UI_SETTINGS_VALUE]: createSetSettingValueReducer('ui'), - [types.SET_MEDIA_MANAGEMENT_SETTINGS_VALUE]: createSetSettingValueReducer('mediaManagement'), - [types.SET_NAMING_SETTINGS_VALUE]: createSetSettingValueReducer('naming'), - [types.SET_QUALITY_PROFILE_VALUE]: createSetSettingValueReducer('qualityProfiles'), - [types.SET_LANGUAGE_PROFILE_VALUE]: createSetSettingValueReducer('languageProfiles'), - [types.SET_METADATA_PROFILE_VALUE]: createSetSettingValueReducer('metadataProfiles'), - [types.SET_DELAY_PROFILE_VALUE]: createSetSettingValueReducer('delayProfiles'), - - [types.SET_QUALITY_DEFINITION_VALUE]: function(state, { payload }) { - const section = 'qualityDefinitions'; - const { id, name, value } = payload; - const newState = getSectionState(state, section); - newState.pendingChanges = _.cloneDeep(newState.pendingChanges); - - const pendingState = newState.pendingChanges[id] || {}; - const currentValue = _.find(newState.items, { id })[name]; - - if (currentValue === value) { - delete pendingState[name]; - } else { - pendingState[name] = value; - } - - if (_.isEmpty(pendingState)) { - delete newState.pendingChanges[id]; - } else { - newState.pendingChanges[id] = pendingState; - } - - return updateSectionState(state, section, newState); - }, - - [types.SET_INDEXER_VALUE]: createSetSettingValueReducer('indexers'), - [types.SET_INDEXER_FIELD_VALUE]: createSetProviderFieldValueReducer('indexers'), - [types.SET_INDEXER_OPTIONS_VALUE]: createSetSettingValueReducer('indexerOptions'), - [types.SET_RESTRICTION_VALUE]: createSetSettingValueReducer('restrictions'), - - [types.SELECT_INDEXER_SCHEMA]: function(state, { payload }) { - return selectProviderSchema(state, 'indexers', payload, (selectedSchema) => { - selectedSchema.enableRss = selectedSchema.supportsRss; - selectedSchema.enableSearch = selectedSchema.supportsSearch; - - return selectedSchema; - }); - }, - - [types.SET_DOWNLOAD_CLIENT_VALUE]: createSetSettingValueReducer('downloadClients'), - [types.SET_DOWNLOAD_CLIENT_FIELD_VALUE]: createSetProviderFieldValueReducer('downloadClients'), - - [types.SELECT_DOWNLOAD_CLIENT_SCHEMA]: function(state, { payload }) { - return selectProviderSchema(state, 'downloadClients', payload, (selectedSchema) => { - selectedSchema.enable = true; - - return selectedSchema; - }); - }, - - [types.SET_DOWNLOAD_CLIENT_OPTIONS_VALUE]: createSetSettingValueReducer('downloadClientOptions'), - [types.SET_REMOTE_PATH_MAPPING_VALUE]: createSetSettingValueReducer('remotePathMappings'), - - [types.SET_NOTIFICATION_VALUE]: createSetSettingValueReducer('notifications'), - [types.SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer('notifications'), - - [types.SELECT_NOTIFICATION_SCHEMA]: function(state, { payload }) { - return selectProviderSchema(state, 'notifications', payload, (selectedSchema) => { - selectedSchema.onGrab = selectedSchema.supportsOnGrab; - selectedSchema.onDownload = selectedSchema.supportsOnDownload; - selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade; - selectedSchema.onRename = selectedSchema.supportsOnRename; - - return selectedSchema; - }); - }, - - [types.SET_METADATA_VALUE]: createSetSettingValueReducer('metadata'), - [types.SET_METADATA_FIELD_VALUE]: createSetProviderFieldValueReducer('metadata'), - - [types.SET_METADATA_PROVIDER_VALUE]: createSetSettingValueReducer('metadataProvider'), - - [types.SET_GENERAL_SETTINGS_VALUE]: createSetSettingValueReducer('general') - -}, defaultState); - -export default settingsReducers; diff --git a/frontend/src/Store/Reducers/systemReducers.js b/frontend/src/Store/Reducers/systemReducers.js deleted file mode 100644 index 31aa2ab08..000000000 --- a/frontend/src/Store/Reducers/systemReducers.js +++ /dev/null @@ -1,146 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { sortDirections } from 'Helpers/Props'; -import createSetReducer from './Creators/createSetReducer'; -import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; -import createReducers from './Creators/createReducers'; - -export const defaultState = { - status: { - isFetching: false, - isPopulated: false, - error: null, - item: {} - }, - - health: { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }, - - diskSpace: { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }, - - tasks: { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }, - - backups: { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }, - - updates: { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }, - - logs: { - isFetching: false, - isPopulated: false, - pageSize: 50, - sortKey: 'time', - sortDirection: sortDirections.DESCENDING, - filterKey: null, - filterValue: null, - error: null, - items: [], - - columns: [ - { - name: 'level', - isSortable: true, - isVisible: true - }, - { - name: 'logger', - label: 'Component', - isSortable: true, - isVisible: true - }, - { - name: 'message', - label: 'Message', - isVisible: true - }, - { - name: 'time', - label: 'Time', - isSortable: true, - isVisible: true - }, - { - name: 'actions', - columnLabel: 'Actions', - isSortable: true, - isVisible: true, - isModifiable: false - } - ] - }, - - logFiles: { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }, - - updateLogFiles: { - isFetching: false, - isPopulated: false, - error: null, - items: [] - } -}; - -export const persistState = [ - 'system.logs.pageSize', - 'system.logs.sortKey', - 'system.logs.sortDirection', - 'system.logs.filterKey', - 'system.logs.filterValue' -]; - -const collectionNames = [ - 'health', - 'diskSpace', - 'tasks', - 'backups', - 'updates', - 'logFiles', - 'updateLogFiles' -]; - -const serverSideCollectionNames = [ - 'logs' -]; - -const systemReducers = handleActions({ - - [types.SET]: createReducers(['status', ...collectionNames, ...serverSideCollectionNames], createSetReducer), - [types.UPDATE]: createReducers(['status', ...collectionNames, ...serverSideCollectionNames], createUpdateReducer), - [types.UPDATE_ITEM]: createUpdateItemReducer('tasks'), - [types.UPDATE_SERVER_SIDE_COLLECTION]: createReducers(serverSideCollectionNames, createUpdateServerSideCollectionReducer), - - [types.SET_LOGS_TABLE_OPTION]: createSetTableOptionReducer('logs') - -}, defaultState); - -export default systemReducers; diff --git a/frontend/src/Store/Reducers/tagReducers.js b/frontend/src/Store/Reducers/tagReducers.js deleted file mode 100644 index 6aa822fd7..000000000 --- a/frontend/src/Store/Reducers/tagReducers.js +++ /dev/null @@ -1,22 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - items: [] -}; - -const reducerSection = 'tags'; - -const tagReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection) - -}, defaultState); - -export default tagReducers; diff --git a/frontend/src/Store/Reducers/trackFileReducers.js b/frontend/src/Store/Reducers/trackFileReducers.js deleted file mode 100644 index 0db17a70f..000000000 --- a/frontend/src/Store/Reducers/trackFileReducers.js +++ /dev/null @@ -1,34 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import createSetReducer from './Creators/createSetReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createRemoveItemReducer from './Creators/createRemoveItemReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - isDeleting: false, - deleteError: null, - isSaving: false, - saveError: null, - items: [] -}; - -const reducerSection = 'trackFiles'; - -const trackFileReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), - [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), - - [types.CLEAR_TRACK_FILES]: (state) => { - return Object.assign({}, state, defaultState); - } - -}, defaultState); - -export default trackFileReducers; diff --git a/frontend/src/Store/Reducers/trackReducers.js b/frontend/src/Store/Reducers/trackReducers.js deleted file mode 100644 index 0a7558691..000000000 --- a/frontend/src/Store/Reducers/trackReducers.js +++ /dev/null @@ -1,85 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { sortDirections } from 'Helpers/Props'; -import createSetReducer from './Creators/createSetReducer'; -import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - sortKey: 'mediumNumber', - sortDirection: sortDirections.DESCENDING, - items: [], - - columns: [ - { - name: 'medium', - label: 'Medium', - isVisible: true - }, - { - name: 'absoluteTrackNumber', - label: 'Track', - isVisible: true - }, - { - name: 'title', - label: 'Title', - isVisible: true - }, - { - name: 'duration', - label: 'Duration', - isVisible: true - }, - { - name: 'audioInfo', - label: 'Audio Info', - isVisible: true - }, - { - name: 'status', - label: 'Status', - isVisible: true - }, - { - name: 'actions', - columnLabel: 'Actions', - isVisible: true, - isModifiable: false - } - ] -}; - -export const persistState = [ - 'tracks.columns' -]; - -const reducerSection = 'tracks'; - -const trackReducers = handleActions({ - - [types.SET]: createSetReducer(reducerSection), - [types.UPDATE]: createUpdateReducer(reducerSection), - [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), - - [types.SET_TRACKS_TABLE_OPTION]: createSetTableOptionReducer(reducerSection), - - [types.CLEAR_TRACKS]: (state) => { - return Object.assign({}, state, { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }); - }, - - [types.SET_TRACKS_SORT]: createSetClientSideCollectionSortReducer(reducerSection) - -}, defaultState); - -export default trackReducers; diff --git a/frontend/src/Store/Reducers/wantedReducers.js b/frontend/src/Store/Reducers/wantedReducers.js deleted file mode 100644 index de6f9f211..000000000 --- a/frontend/src/Store/Reducers/wantedReducers.js +++ /dev/null @@ -1,173 +0,0 @@ -import { handleActions } from 'redux-actions'; -import * as types from 'Store/Actions/actionTypes'; -import { sortDirections } from 'Helpers/Props'; -import createClearReducer from './Creators/createClearReducer'; -import createSetReducer from './Creators/createSetReducer'; -import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; -import createUpdateReducer from './Creators/createUpdateReducer'; -import createUpdateItemReducer from './Creators/createUpdateItemReducer'; -import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; -import createReducers from './Creators/createReducers'; - -export const defaultState = { - missing: { - isFetching: false, - isPopulated: false, - pageSize: 20, - sortKey: 'releaseDate', - sortDirection: sortDirections.DESCENDING, - filterKey: 'monitored', - filterValue: 'true', - error: null, - items: [], - - columns: [ - { - name: 'artist.sortName', - label: 'Artist Name', - isSortable: true, - isVisible: true - }, - // { - // name: 'episode', - // label: 'Episode', - // isVisible: true - // }, - { - name: 'albumTitle', - label: 'Album Title', - isVisible: true - }, - { - name: 'albumType', - label: 'Album Type', - isSortable: true, - isVisible: true - }, - { - name: 'releaseDate', - label: 'Release Date', - isSortable: true, - isVisible: true - }, - // { - // name: 'status', - // label: 'Status', - // isVisible: true - // }, - { - name: 'actions', - columnLabel: 'Actions', - isVisible: true, - isModifiable: false - } - ] - }, - - cutoffUnmet: { - isFetching: false, - isPopulated: false, - pageSize: 20, - sortKey: 'airDateUtc', - sortDirection: sortDirections.DESCENDING, - filterKey: 'monitored', - filterValue: true, - error: null, - items: [], - - columns: [ - { - name: 'artist.sortName', - label: 'Artist Name', - isSortable: true, - isVisible: true - }, - // { - // name: 'episode', - // label: 'Episode', - // isVisible: true - // }, - { - name: 'albumTitle', - label: 'Album Title', - isVisible: true - }, - { - name: 'albumType', - label: 'Album Type', - isSortable: true, - isVisible: true - }, - { - name: 'releaseDate', - label: 'Release Date', - isSortable: true, - isVisible: true - }, - { - name: 'language', - label: 'Language', - isVisible: false - }, - // { - // name: 'status', - // label: 'Status', - // isVisible: true - // }, - { - name: 'actions', - columnLabel: 'Actions', - isVisible: true, - isModifiable: false - } - ] - } -}; - -export const persistState = [ - 'wanted.missing.pageSize', - 'wanted.missing.sortKey', - 'wanted.missing.sortDirection', - 'wanted.missing.filterKey', - 'wanted.missing.filterValue', - 'wanted.missing.columns', - 'wanted.cutoffUnmet.pageSize', - 'wanted.cutoffUnmet.sortKey', - 'wanted.cutoffUnmet.sortDirection', - 'wanted.cutoffUnmet.filterKey', - 'wanted.cutoffUnmet.filterValue', - 'wanted.cutoffUnmet.columns' -]; - -const serverSideCollectionNames = [ - 'missing', - 'cutoffUnmet' -]; - -const wantedReducers = handleActions({ - - [types.SET]: createReducers(serverSideCollectionNames, createSetReducer), - [types.UPDATE]: createReducers(serverSideCollectionNames, createUpdateReducer), - [types.UPDATE_ITEM]: createReducers(serverSideCollectionNames, createUpdateItemReducer), - [types.UPDATE_SERVER_SIDE_COLLECTION]: createReducers(serverSideCollectionNames, createUpdateServerSideCollectionReducer), - - [types.SET_MISSING_TABLE_OPTION]: createSetTableOptionReducer('missing'), - [types.SET_CUTOFF_UNMET_TABLE_OPTION]: createSetTableOptionReducer('cutoffUnmet'), - - [types.CLEAR_MISSING]: createClearReducer('missing', { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }), - - [types.CLEAR_CUTOFF_UNMET]: createClearReducer('cutoffUnmet', { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }) - -}, defaultState); - -export default wantedReducers; diff --git a/frontend/src/Store/Selectors/createArtistCountSelector.js b/frontend/src/Store/Selectors/createArtistCountSelector.js new file mode 100644 index 000000000..71910cb44 --- /dev/null +++ b/frontend/src/Store/Selectors/createArtistCountSelector.js @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect'; +import createAllArtistSelector from './createAllArtistSelector'; + +function createArtistCountSelector() { + return createSelector( + createAllArtistSelector(), + (series) => { + return series.length; + } + ); +} + +export default createArtistCountSelector; diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.js b/frontend/src/Store/Selectors/createSettingsSectionSelector.js index 7ff0d2708..14af65a01 100644 --- a/frontend/src/Store/Selectors/createSettingsSectionSelector.js +++ b/frontend/src/Store/Selectors/createSettingsSectionSelector.js @@ -1,9 +1,10 @@ +import _ from 'lodash'; import { createSelector } from 'reselect'; import selectSettings from 'Store/Selectors/selectSettings'; function createSettingsSectionSelector() { return createSelector( - (state, { section }) => state.settings[section], + (state, { section }) => _.get(state, section), (sectionSettings) => { const { isFetching, diff --git a/frontend/src/Store/createAppStore.js b/frontend/src/Store/createAppStore.js index 168bbc954..e05c80323 100644 --- a/frontend/src/Store/createAppStore.js +++ b/frontend/src/Store/createAppStore.js @@ -1,5 +1,5 @@ import { createStore } from 'redux'; -import reducers, { defaultState } from 'Store/Reducers'; +import reducers, { defaultState } from 'Store/Actions/reducers'; import middlewares from 'Store/Middleware/middlewares'; function createAppStore(history) { diff --git a/frontend/src/Store/thunks.js b/frontend/src/Store/thunks.js new file mode 100644 index 000000000..f46ee3a23 --- /dev/null +++ b/frontend/src/Store/thunks.js @@ -0,0 +1,24 @@ +const thunks = {}; + +export function createThunk(type) { + return function(payload = {}) { + return function(dispatch, getState) { + const thunk = thunks[type]; + + if (thunk) { + return thunk(getState, payload, dispatch); + } + + throw Error(`Thunk handler has not been registered for ${type}`); + }; + }; +} + +export function handleThunks(handlers) { + const types = Object.keys(handlers); + + types.forEach((type) => { + thunks[type] = handlers[type]; + }); +} + diff --git a/frontend/src/Utilities/State/getProviderState.js b/frontend/src/Utilities/State/getProviderState.js index 7d6e5cb1d..cc18af711 100644 --- a/frontend/src/Utilities/State/getProviderState.js +++ b/frontend/src/Utilities/State/getProviderState.js @@ -1,8 +1,9 @@ import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; -function getProviderState(payload, getState, getFromState) { +function getProviderState(payload, getState, section) { const id = payload.id; - const state = getFromState(getState()); + const state = getSectionState(getState(), section, true); const pendingChanges = Object.assign({}, state.pendingChanges); const pendingFields = state.pendingChanges.fields || {}; delete pendingChanges.fields; diff --git a/frontend/src/Utilities/State/getSectionState.js b/frontend/src/Utilities/State/getSectionState.js index c188d9eaa..00871bed2 100644 --- a/frontend/src/Utilities/State/getSectionState.js +++ b/frontend/src/Utilities/State/getSectionState.js @@ -1,4 +1,17 @@ -function getSectionState(state, section) { +import _ from 'lodash'; + +function getSectionState(state, section, isFullStateTree = false) { + if (isFullStateTree) { + return _.get(state, section); + } + + const [, subSection] = section.split('.'); + + if (subSection) { + return Object.assign({}, state[subSection]); + } + + // TODO: Remove in favour of using subSection if (state.hasOwnProperty(section)) { return Object.assign({}, state[section]); } diff --git a/frontend/src/Utilities/State/updateSectionState.js b/frontend/src/Utilities/State/updateSectionState.js index c7407257d..81b33ecaf 100644 --- a/frontend/src/Utilities/State/updateSectionState.js +++ b/frontend/src/Utilities/State/updateSectionState.js @@ -1,4 +1,11 @@ function updateSectionState(state, section, newState) { + const [, subSection] = section.split('.'); + + if (subSection) { + return Object.assign({}, state, { [subSection]: newState }); + } + + // TODO: Remove in favour of using subSection if (state.hasOwnProperty(section)) { return Object.assign({}, state, { [section]: newState }); } diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js index 76ec74085..85b76e23f 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js @@ -150,6 +150,7 @@ class CutoffUnmet extends Component { diff --git a/frontend/src/Wanted/Missing/Missing.js b/frontend/src/Wanted/Missing/Missing.js index 0914e3c3a..b13a34c2b 100644 --- a/frontend/src/Wanted/Missing/Missing.js +++ b/frontend/src/Wanted/Missing/Missing.js @@ -160,6 +160,7 @@ class Missing extends Component { diff --git a/src/Lidarr.Api.V1/Artist/ArtistEditorResource.cs b/src/Lidarr.Api.V1/Artist/ArtistEditorResource.cs index edff77346..d50352cd8 100644 --- a/src/Lidarr.Api.V1/Artist/ArtistEditorResource.cs +++ b/src/Lidarr.Api.V1/Artist/ArtistEditorResource.cs @@ -10,7 +10,6 @@ namespace Lidarr.Api.V1.Artist public int? QualityProfileId { get; set; } public int? LanguageProfileId { get; set; } public int? MetadataProfileId { get; set; } - //public SeriesTypes? SeriesType { get; set; } public bool? AlbumFolder { get; set; } public string RootFolderPath { get; set; } public List Tags { get; set; } diff --git a/src/Lidarr.Api.V1/Artist/ArtistModule.cs b/src/Lidarr.Api.V1/Artist/ArtistModule.cs index b19b7e527..44d8fa173 100644 --- a/src/Lidarr.Api.V1/Artist/ArtistModule.cs +++ b/src/Lidarr.Api.V1/Artist/ArtistModule.cs @@ -98,7 +98,6 @@ namespace Lidarr.Api.V1.Artist var resource = artist.ToResource(); MapCoversToLocal(resource); - //MapAlbums(resource); FetchAndLinkArtistStatistics(resource); //PopulateAlternateTitles(resource); @@ -149,14 +148,6 @@ namespace Lidarr.Api.V1.Artist } } - private void MapAlbums(params ArtistResource[] artists) - { - foreach (var artistResource in artists) - { - artistResource.Albums = _albumService.GetAlbumsByArtist(artistResource.Id).ToResource(); - } - } - private void FetchAndLinkArtistStatistics(ArtistResource resource) { LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id)); diff --git a/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileResource.cs b/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileResource.cs index d144ed30c..1e64242a6 100644 --- a/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileResource.cs +++ b/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileResource.cs @@ -36,7 +36,7 @@ namespace Lidarr.Api.V1.Profiles.Quality Id = model.Id, Name = model.Name, Cutoff = model.Cutoff, - Items = model.Items.ConvertAll(ToResource), + Items = model.Items.ConvertAll(ToResource) }; } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 222ef0b0c..f9ad93dc4 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -876,8 +876,8 @@ - - + + @@ -992,11 +992,11 @@ - - - + + + - + diff --git a/src/NzbDrone.Core/Profiles/Quality/Profile.cs b/src/NzbDrone.Core/Profiles/Qualities/Profile.cs similarity index 100% rename from src/NzbDrone.Core/Profiles/Quality/Profile.cs rename to src/NzbDrone.Core/Profiles/Qualities/Profile.cs diff --git a/src/NzbDrone.Core/Profiles/Quality/ProfileInUseException.cs b/src/NzbDrone.Core/Profiles/Qualities/ProfileInUseException.cs similarity index 100% rename from src/NzbDrone.Core/Profiles/Quality/ProfileInUseException.cs rename to src/NzbDrone.Core/Profiles/Qualities/ProfileInUseException.cs diff --git a/src/NzbDrone.Core/Profiles/Quality/ProfileQualityItem.cs b/src/NzbDrone.Core/Profiles/Qualities/ProfileQualityItem.cs similarity index 100% rename from src/NzbDrone.Core/Profiles/Quality/ProfileQualityItem.cs rename to src/NzbDrone.Core/Profiles/Qualities/ProfileQualityItem.cs diff --git a/src/NzbDrone.Core/Profiles/Quality/ProfileRepository.cs b/src/NzbDrone.Core/Profiles/Qualities/ProfileRepository.cs similarity index 100% rename from src/NzbDrone.Core/Profiles/Quality/ProfileRepository.cs rename to src/NzbDrone.Core/Profiles/Qualities/ProfileRepository.cs diff --git a/src/NzbDrone.Core/Profiles/Quality/ProfileService.cs b/src/NzbDrone.Core/Profiles/Qualities/ProfileService.cs similarity index 100% rename from src/NzbDrone.Core/Profiles/Quality/ProfileService.cs rename to src/NzbDrone.Core/Profiles/Qualities/ProfileService.cs diff --git a/src/NzbDrone.Core/Profiles/Quality/QualityIndex.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityIndex.cs similarity index 100% rename from src/NzbDrone.Core/Profiles/Quality/QualityIndex.cs rename to src/NzbDrone.Core/Profiles/Qualities/QualityIndex.cs