mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-07 13:32:17 -07:00
New: Server Side UI Filtering, Error Boundaries (#501)
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
parent
a95191dc3b
commit
64a8d02f77
110 changed files with 1564 additions and 431 deletions
|
@ -1,4 +1,4 @@
|
||||||
// will download and run sonarr (server) in a non-windows enviroment
|
// will download and run lidarr (server) in a non-windows enviroment
|
||||||
// you can use this if you don't care about the server code and just want to work
|
// you can use this if you don't care about the server code and just want to work
|
||||||
// with the web code.
|
// with the web code.
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||||
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||||
import styles from './BlacklistRow.css';
|
import styles from './BlacklistRow.css';
|
||||||
|
@ -103,7 +103,7 @@ class BlacklistRow extends Component {
|
||||||
key={name}
|
key={name}
|
||||||
className={styles.quality}
|
className={styles.quality}
|
||||||
>
|
>
|
||||||
<EpisodeQuality
|
<TrackQuality
|
||||||
quality={quality}
|
quality={quality}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import TableRow from 'Components/Table/TableRow';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||||
|
@ -142,7 +142,7 @@ class HistoryRow extends Component {
|
||||||
if (name === 'quality') {
|
if (name === 'quality') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
<EpisodeQuality
|
<TrackQuality
|
||||||
quality={quality}
|
quality={quality}
|
||||||
isCutoffMet={qualityCutoffNotMet}
|
isCutoffMet={qualityCutoffNotMet}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||||
import QueueStatusCell from './QueueStatusCell';
|
import QueueStatusCell from './QueueStatusCell';
|
||||||
|
@ -177,7 +177,7 @@ class QueueRow extends Component {
|
||||||
if (name === 'quality') {
|
if (name === 'quality') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
<EpisodeQuality
|
<TrackQuality
|
||||||
quality={quality}
|
quality={quality}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { icons } from 'Helpers/Props';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import InteractiveAlbumSearchModal from './Search/InteractiveAlbumSearchModal';
|
import InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
|
||||||
import styles from './AlbumSearchCell.css';
|
import styles from './AlbumSearchCell.css';
|
||||||
|
|
||||||
class AlbumSearchCell extends Component {
|
class AlbumSearchCell extends Component {
|
||||||
|
@ -55,7 +55,7 @@ class AlbumSearchCell extends Component {
|
||||||
onPress={this.onManualSearchPress}
|
onPress={this.onManualSearchPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InteractiveAlbumSearchModal
|
<InteractiveSearchModal
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
isOpen={this.state.isDetailsModalOpen}
|
||||||
albumId={albumId}
|
albumId={albumId}
|
||||||
onModalClose={this.onDetailsModalClose}
|
onModalClose={this.onDetailsModalClose}
|
||||||
|
|
|
@ -22,7 +22,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
|
import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
|
||||||
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
||||||
import InteractiveAlbumSearchModal from 'Album/Search/InteractiveAlbumSearchModal';
|
import InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
|
||||||
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
|
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
|
||||||
|
|
||||||
import styles from './AlbumDetails.css';
|
import styles from './AlbumDetails.css';
|
||||||
|
@ -415,7 +415,7 @@ class AlbumDetails extends Component {
|
||||||
onModalClose={this.onManageTracksModalClose}
|
onModalClose={this.onManageTracksModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InteractiveAlbumSearchModal
|
<InteractiveSearchModal
|
||||||
isOpen={isInteractiveSearchModalOpen}
|
isOpen={isInteractiveSearchModalOpen}
|
||||||
albumId={id}
|
albumId={id}
|
||||||
onModalClose={this.onInteractiveSearchModalClose}
|
onModalClose={this.onInteractiveSearchModalClose}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ProgressBar from 'Components/ProgressBar';
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
import QueueDetails from 'Activity/Queue/QueueDetails';
|
import QueueDetails from 'Activity/Queue/QueueDetails';
|
||||||
import EpisodeQuality from './EpisodeQuality';
|
import TrackQuality from './TrackQuality';
|
||||||
import styles from './EpisodeStatus.css';
|
import styles from './EpisodeStatus.css';
|
||||||
|
|
||||||
function EpisodeStatus(props) {
|
function EpisodeStatus(props) {
|
||||||
|
@ -63,7 +63,7 @@ function EpisodeStatus(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.center}>
|
<div className={styles.center}>
|
||||||
<EpisodeQuality
|
<TrackQuality
|
||||||
quality={quality}
|
quality={quality}
|
||||||
size={trackFile.size}
|
size={trackFile.size}
|
||||||
isCutoffNotMet={isCutoffNotMet}
|
isCutoffNotMet={isCutoffNotMet}
|
||||||
|
|
|
@ -22,7 +22,7 @@ function getTooltip(title, quality, size) {
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EpisodeQuality(props) {
|
function TrackQuality(props) {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
|
@ -42,7 +42,7 @@ function EpisodeQuality(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EpisodeQuality.propTypes = {
|
TrackQuality.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
|
@ -50,8 +50,8 @@ EpisodeQuality.propTypes = {
|
||||||
isCutoffNotMet: PropTypes.bool
|
isCutoffNotMet: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeQuality.defaultProps = {
|
TrackQuality.defaultProps = {
|
||||||
title: ''
|
title: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EpisodeQuality;
|
export default TrackQuality;
|
|
@ -13,6 +13,7 @@ import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import NoArtist from 'Artist/NoArtist';
|
import NoArtist from 'Artist/NoArtist';
|
||||||
|
import AlbumStudioFilterModalConnector from './AlbumStudioFilterModalConnector';
|
||||||
import AlbumStudioRowConnector from './AlbumStudioRowConnector';
|
import AlbumStudioRowConnector from './AlbumStudioRowConnector';
|
||||||
import AlbumStudioFooter from './AlbumStudioFooter';
|
import AlbumStudioFooter from './AlbumStudioFooter';
|
||||||
|
|
||||||
|
@ -130,6 +131,7 @@ class AlbumStudio extends Component {
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={customFilters}
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={AlbumStudioFilterModalConnector}
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
@ -202,7 +204,7 @@ AlbumStudio.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
|
24
frontend/src/AlbumStudio/AlbumStudioFilterModalConnector.js
Normal file
24
frontend/src/AlbumStudio/AlbumStudioFilterModalConnector.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setAlbumStudioFilter } from 'Store/Actions/albumStudioActions';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.artist.items,
|
||||||
|
(state) => state.albumStudio.filterBuilderProps,
|
||||||
|
(sectionItems, filterBuilderProps) => {
|
||||||
|
return {
|
||||||
|
sectionItems,
|
||||||
|
filterBuilderProps,
|
||||||
|
customFilterType: 'albumStudio'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchSetFilter: setAlbumStudioFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
|
@ -9,7 +9,7 @@ function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createArtistSelector(),
|
createArtistSelector(),
|
||||||
createTrackFileSelector(),
|
createTrackFileSelector(),
|
||||||
(artist, trackFile) => {
|
(artist = {}, trackFile) => {
|
||||||
return {
|
return {
|
||||||
foreignArtistId: artist.foreignArtistId,
|
foreignArtistId: artist.foreignArtistId,
|
||||||
artistMonitored: artist.monitored,
|
artistMonitored: artist.monitored,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import HeartRating from 'Components/HeartRating';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
|
|
|
@ -273,7 +273,7 @@ ArtistEditor.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import * as artistEditorActions from 'Store/Actions/artistEditorActions';
|
import { setArtistEditorFilter } from 'Store/Actions/artistEditorActions';
|
||||||
import FilterModal from 'Components/Filter/FilterModal';
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -10,22 +10,15 @@ function createMapStateToProps() {
|
||||||
(sectionItems, filterBuilderProps) => {
|
(sectionItems, filterBuilderProps) => {
|
||||||
return {
|
return {
|
||||||
sectionItems,
|
sectionItems,
|
||||||
filterBuilderProps
|
filterBuilderProps,
|
||||||
|
customFilterType: 'artistEditor'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
const mapDispatchToProps = {
|
||||||
return {
|
dispatchSetFilter: setArtistEditorFilter
|
||||||
onRemoveCustomFilterPress(payload) {
|
};
|
||||||
dispatch(artistEditorActions.removeArtistEditorCustomFilter(payload));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSaveCustomFilterPress(payload) {
|
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||||
dispatch(artistEditorActions.saveArtistEditorCustomFilter(payload));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
|
||||||
|
|
|
@ -119,4 +119,8 @@ ArtistEditorRow.propTypes = {
|
||||||
onSelectedChange: PropTypes.func.isRequired
|
onSelectedChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ArtistEditorRow.defaultProps = {
|
||||||
|
tags: []
|
||||||
|
};
|
||||||
|
|
||||||
export default ArtistEditorRow;
|
export default ArtistEditorRow;
|
||||||
|
|
|
@ -9,7 +9,7 @@ import TableRow from 'Components/Table/TableRow';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||||
import styles from './ArtistHistoryRow.css';
|
import styles from './ArtistHistoryRow.css';
|
||||||
|
@ -100,7 +100,7 @@ class ArtistHistoryRow extends Component {
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
<EpisodeQuality
|
<TrackQuality
|
||||||
quality={quality}
|
quality={quality}
|
||||||
isCutoffNotMet={qualityCutoffNotMet}
|
isCutoffNotMet={qualityCutoffNotMet}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -215,7 +215,7 @@ class ArtistIndex extends Component {
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const ViewComponent = getViewComponent(view);
|
const ViewComponent = getViewComponent(view);
|
||||||
const isLoaded = !error && isPopulated && !!items.length && contentBody;
|
const isLoaded = !!(!error && isPopulated && items.length && contentBody);
|
||||||
const hasNoArtist = !totalItems;
|
const hasNoArtist = !totalItems;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -382,7 +382,7 @@ ArtistIndex.propTypes = {
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
totalItems: PropTypes.number.isRequired,
|
totalItems: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import * as artistIndexActions from 'Store/Actions/artistIndexActions';
|
import { setArtistFilter } from 'Store/Actions/artistIndexActions';
|
||||||
import FilterModal from 'Components/Filter/FilterModal';
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -10,22 +10,15 @@ function createMapStateToProps() {
|
||||||
(sectionItems, filterBuilderProps) => {
|
(sectionItems, filterBuilderProps) => {
|
||||||
return {
|
return {
|
||||||
sectionItems,
|
sectionItems,
|
||||||
filterBuilderProps
|
filterBuilderProps,
|
||||||
|
customFilterType: 'artistIndex'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
const mapDispatchToProps = {
|
||||||
return {
|
dispatchSetFilter: setArtistFilter
|
||||||
onRemoveCustomFilterPress(payload) {
|
};
|
||||||
dispatch(artistIndexActions.removeArtistCustomFilter(payload));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSaveCustomFilterPress(payload) {
|
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||||
dispatch(artistIndexActions.saveArtistCustomFilter(payload));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import { sortDirections } from 'Helpers/Props';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||||
import ArtistIndexBanner from './ArtistIndexBanner';
|
import ArtistIndexBanner from './ArtistIndexBanner';
|
||||||
import styles from './ArtistIndexBanners.css';
|
import styles from './ArtistIndexBanners.css';
|
||||||
|
|
|
@ -27,7 +27,7 @@ function ArtistIndexFilterMenu(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistIndexFilterMenu.propTypes = {
|
ArtistIndexFilterMenu.propTypes = {
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -2,12 +2,12 @@ import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import { sortDirections } from 'Helpers/Props';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||||
import ArtistIndexOverview from './ArtistIndexOverview';
|
import ArtistIndexOverview from './ArtistIndexOverview';
|
||||||
import styles from './ArtistIndexOverviews.css';
|
import styles from './ArtistIndexOverviews.css';
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import { sortDirections } from 'Helpers/Props';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||||
import ArtistIndexPoster from './ArtistIndexPoster';
|
import ArtistIndexPoster from './ArtistIndexPoster';
|
||||||
import styles from './ArtistIndexPosters.css';
|
import styles from './ArtistIndexPosters.css';
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import { align, icons } from 'Helpers/Props';
|
import { align, icons } from 'Helpers/Props';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
|
|
@ -56,6 +56,10 @@ class CalendarEvent extends Component {
|
||||||
colorImpairedMode
|
colorImpairedMode
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
if (!artist) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const startTime = moment(releaseDate);
|
const startTime = moment(releaseDate);
|
||||||
// const endTime = startTime.add(artist.runtime, 'minutes');
|
// const endTime = startTime.add(artist.runtime, 'minutes');
|
||||||
const downloading = !!(queueItem || grabbed);
|
const downloading = !!(queueItem || grabbed);
|
||||||
|
|
62
frontend/src/Components/Error/ErrorBoundary.js
Normal file
62
frontend/src/Components/Error/ErrorBoundary.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import * as sentry from '@sentry/browser';
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
info: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error, info) {
|
||||||
|
this.setState({
|
||||||
|
error,
|
||||||
|
info
|
||||||
|
});
|
||||||
|
|
||||||
|
sentry.captureException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
errorComponent: ErrorComponent,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
error,
|
||||||
|
info
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<ErrorComponent
|
||||||
|
error={error}
|
||||||
|
info={info}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorBoundary.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
errorComponent: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
38
frontend/src/Components/Error/ErrorBoundaryError.css
Normal file
38
frontend/src/Components/Error/ErrorBoundaryError.css
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin: 50px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin: 20px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
|
.image {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.image {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
60
frontend/src/Components/Error/ErrorBoundaryError.js
Normal file
60
frontend/src/Components/Error/ErrorBoundaryError.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './ErrorBoundaryError.css';
|
||||||
|
|
||||||
|
function ErrorBoundaryError(props) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
messageClassName,
|
||||||
|
detailsClassName,
|
||||||
|
message,
|
||||||
|
error,
|
||||||
|
info
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className={messageClassName}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.imageContainer}>
|
||||||
|
<img
|
||||||
|
className={styles.image}
|
||||||
|
src={`${window.Lidarr.urlBase}/Content/Images/error.png`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details className={detailsClassName}>
|
||||||
|
{
|
||||||
|
error &&
|
||||||
|
<div>
|
||||||
|
{error.toString()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
{info.componentStack}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorBoundaryError.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
messageClassName: PropTypes.string.isRequired,
|
||||||
|
detailsClassName: PropTypes.string.isRequired,
|
||||||
|
message: PropTypes.string.isRequired,
|
||||||
|
error: PropTypes.object.isRequired,
|
||||||
|
info: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ErrorBoundaryError.defaultProps = {
|
||||||
|
className: styles.container,
|
||||||
|
messageClassName: styles.message,
|
||||||
|
detailsClassName: styles.details,
|
||||||
|
message: 'There was an error loading this content'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorBoundaryError;
|
|
@ -35,7 +35,7 @@ function createMapStateToProps() {
|
||||||
directories,
|
directories,
|
||||||
files,
|
files,
|
||||||
paths: filteredPaths,
|
paths: filteredPaths,
|
||||||
isWindowsService: true || systemStatus.isWindows && systemStatus.mode === 'service'
|
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
@ -34,6 +35,28 @@ class FilterBuilderModalContent extends Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
customFilters,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
dispatchSetFilter,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||||
|
if (id) {
|
||||||
|
dispatchSetFilter({ selectedFilterKey: id });
|
||||||
|
} else {
|
||||||
|
const last = customFilters[customFilters.length -1];
|
||||||
|
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
@ -70,9 +93,9 @@ class FilterBuilderModalContent extends Component {
|
||||||
|
|
||||||
onSaveFilterPress = () => {
|
onSaveFilterPress = () => {
|
||||||
const {
|
const {
|
||||||
customFilterKey: key,
|
id,
|
||||||
onSaveCustomFilterPress,
|
customFilterType,
|
||||||
onModalClose
|
onSaveCustomFilterPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -92,8 +115,12 @@ class FilterBuilderModalContent extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSaveCustomFilterPress({ key, label, filters });
|
onSaveCustomFilterPress({
|
||||||
onModalClose();
|
id,
|
||||||
|
type: customFilterType,
|
||||||
|
label,
|
||||||
|
filters
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -103,6 +130,8 @@ class FilterBuilderModalContent extends Component {
|
||||||
const {
|
const {
|
||||||
sectionItems,
|
sectionItems,
|
||||||
filterBuilderProps,
|
filterBuilderProps,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -161,17 +190,17 @@ class FilterBuilderModalContent extends Component {
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button onPress={onModalClose}>
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
onPress={this.onSaveFilterPress}
|
onPress={this.onSaveFilterPress}
|
||||||
>
|
>
|
||||||
Apply
|
Save
|
||||||
</Button>
|
</SpinnerErrorButton>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
);
|
);
|
||||||
|
@ -179,13 +208,18 @@ class FilterBuilderModalContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterBuilderModalContent.propTypes = {
|
FilterBuilderModalContent.propTypes = {
|
||||||
customFilterKey: PropTypes.string,
|
id: PropTypes.number,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
|
customFilterType: PropTypes.string.isRequired,
|
||||||
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onRemoveCustomFilterPress: PropTypes.func.isRequired,
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
||||||
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
||||||
|
dispatchSetFilter: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,42 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { saveCustomFilter, deleteCustomFilter } from 'Store/Actions/customFilterActions';
|
||||||
import FilterBuilderModalContent from './FilterBuilderModalContent';
|
import FilterBuilderModalContent from './FilterBuilderModalContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { customFilters }) => customFilters,
|
(state, { customFilters }) => customFilters,
|
||||||
(state, { customFilterKey }) => customFilterKey,
|
(state, { id }) => id,
|
||||||
(customFilters, customFilterKey) => {
|
(state) => state.customFilters.isSaving,
|
||||||
if (customFilterKey) {
|
(state) => state.customFilters.saveError,
|
||||||
const customFilter = customFilters.find((c) => c.key === customFilterKey);
|
(customFilters, id, isSaving, saveError) => {
|
||||||
|
if (id) {
|
||||||
|
const customFilter = customFilters.find((c) => c.id === id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customFilterKey: customFilter.key,
|
id: customFilter.id,
|
||||||
label: customFilter.label,
|
label: customFilter.label,
|
||||||
filters: customFilter.filters
|
filters: customFilter.filters,
|
||||||
|
customFilters,
|
||||||
|
isSaving,
|
||||||
|
saveError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: '',
|
label: '',
|
||||||
filters: []
|
filters: [],
|
||||||
|
customFilters,
|
||||||
|
isSaving,
|
||||||
|
saveError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(FilterBuilderModalContent);
|
const mapDispatchToProps = {
|
||||||
|
onSaveCustomFilterPress: saveCustomFilter,
|
||||||
|
dispatchDeleteCustomFilter: deleteCustomFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(FilterBuilderModalContent);
|
||||||
|
|
|
@ -1,11 +1,64 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { kinds, filterBuilderTypes } from 'Helpers/Props';
|
import convertToBytes from 'Utilities/Number/convertToBytes';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import { kinds, filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
|
||||||
import TagInput, { tagShape } from 'Components/Form/TagInput';
|
import TagInput, { tagShape } from 'Components/Form/TagInput';
|
||||||
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
|
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
|
||||||
|
|
||||||
export const NAME = 'value';
|
export const NAME = 'value';
|
||||||
|
|
||||||
|
function getTagDisplayValue(value, selectedFilterBuilderProp) {
|
||||||
|
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
|
||||||
|
return formatBytes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValue(input, selectedFilterBuilderProp) {
|
||||||
|
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
|
||||||
|
const match = input.match(/^(\d+)([kmgt](i?b)?)$/i);
|
||||||
|
if (match && match.length > 1) {
|
||||||
|
const [, value, unit] = input.match(/^(\d+)([kmgt](i?b)?)$/i);
|
||||||
|
switch (unit.toLowerCase()) {
|
||||||
|
case 'k':
|
||||||
|
return convertToBytes(value, 1, true);
|
||||||
|
case 'm':
|
||||||
|
return convertToBytes(value, 2, true);
|
||||||
|
case 'g':
|
||||||
|
return convertToBytes(value, 3, true);
|
||||||
|
case 't':
|
||||||
|
return convertToBytes(value, 4, true);
|
||||||
|
case 'kb':
|
||||||
|
return convertToBytes(value, 1, true);
|
||||||
|
case 'mb':
|
||||||
|
return convertToBytes(value, 2, true);
|
||||||
|
case 'gb':
|
||||||
|
return convertToBytes(value, 3, true);
|
||||||
|
case 'tb':
|
||||||
|
return convertToBytes(value, 4, true);
|
||||||
|
case 'kib':
|
||||||
|
return convertToBytes(value, 1, true);
|
||||||
|
case 'mib':
|
||||||
|
return convertToBytes(value, 2, true);
|
||||||
|
case 'gib':
|
||||||
|
return convertToBytes(value, 3, true);
|
||||||
|
case 'tib':
|
||||||
|
return convertToBytes(value, 4, true);
|
||||||
|
default:
|
||||||
|
return parseInt(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
||||||
|
return parseInt(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
class FilterBuilderRowValue extends Component {
|
class FilterBuilderRowValue extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -18,17 +71,15 @@ class FilterBuilderRowValue extends Component {
|
||||||
onChange
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let id = tag.id;
|
let value = tag.id;
|
||||||
|
|
||||||
if (id == null) {
|
if (value == null) {
|
||||||
id = selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ?
|
value = getValue(tag.name, selectedFilterBuilderProp);
|
||||||
parseInt(tag.name) :
|
|
||||||
tag.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
name: NAME,
|
name: NAME,
|
||||||
value: [...filterValue, id]
|
value: [...filterValue, value]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +103,7 @@ class FilterBuilderRowValue extends Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
filterValue,
|
filterValue,
|
||||||
|
selectedFilterBuilderProp,
|
||||||
tagList
|
tagList
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -68,7 +120,7 @@ class FilterBuilderRowValue extends Component {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: id
|
name: getTagDisplayValue(id, selectedFilterBuilderProp)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,29 +2,70 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
import styles from './CustomFilter.css';
|
import styles from './CustomFilter.css';
|
||||||
|
|
||||||
class CustomFilter extends Component {
|
class CustomFilter extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isDeleting: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
isDeleting,
|
||||||
|
deleteError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (prevProps.isDeleting && !isDeleting && this.state.isDeleting && deleteError) {
|
||||||
|
this.setState({ isDeleting: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
selectedFilterKey,
|
||||||
|
dispatchSetFilter
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// Assume that delete and then unmounting means the delete was successful.
|
||||||
|
// Moving this check to a ancestor would be more accurate, but would have
|
||||||
|
// more boilerplate.
|
||||||
|
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||||
|
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onEditPress = () => {
|
onEditPress = () => {
|
||||||
const {
|
const {
|
||||||
customFilterKey,
|
id,
|
||||||
onEditPress
|
onEditPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
onEditPress(customFilterKey);
|
onEditPress(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemovePress = () => {
|
onRemovePress = () => {
|
||||||
const {
|
const {
|
||||||
customFilterKey,
|
id,
|
||||||
onRemovePress
|
dispatchDeleteCustomFilter
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
onRemovePress({ key: customFilterKey });
|
this.setState({ isDeleting: true }, () => {
|
||||||
|
dispatchDeleteCustomFilter({ id });
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -47,8 +88,9 @@ class CustomFilter extends Component {
|
||||||
onPress={this.onEditPress}
|
onPress={this.onEditPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<SpinnerIconButton
|
||||||
name={icons.REMOVE}
|
name={icons.REMOVE}
|
||||||
|
isSpinning={this.state.isDeleting}
|
||||||
onPress={this.onRemovePress}
|
onPress={this.onRemovePress}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,10 +100,14 @@ class CustomFilter extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomFilter.propTypes = {
|
CustomFilter.propTypes = {
|
||||||
customFilterKey: PropTypes.string.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
|
deleteError: PropTypes.object,
|
||||||
|
dispatchSetFilter: PropTypes.func.isRequired,
|
||||||
onEditPress: PropTypes.func.isRequired,
|
onEditPress: PropTypes.func.isRequired,
|
||||||
onRemovePress: PropTypes.func.isRequired
|
dispatchDeleteCustomFilter: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomFilter;
|
export default CustomFilter;
|
||||||
|
|
|
@ -10,9 +10,13 @@ import styles from './CustomFiltersModalContent.css';
|
||||||
|
|
||||||
function CustomFiltersModalContent(props) {
|
function CustomFiltersModalContent(props) {
|
||||||
const {
|
const {
|
||||||
|
selectedFilterKey,
|
||||||
customFilters,
|
customFilters,
|
||||||
|
isDeleting,
|
||||||
|
deleteError,
|
||||||
|
dispatchDeleteCustomFilter,
|
||||||
|
dispatchSetFilter,
|
||||||
onAddCustomFilter,
|
onAddCustomFilter,
|
||||||
onRemoveCustomFilterPress,
|
|
||||||
onEditCustomFilter,
|
onEditCustomFilter,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -29,10 +33,14 @@ function CustomFiltersModalContent(props) {
|
||||||
return (
|
return (
|
||||||
<CustomFilter
|
<CustomFilter
|
||||||
key={index}
|
key={index}
|
||||||
customFilterKey={customFilter.key}
|
id={customFilter.id}
|
||||||
label={customFilter.label}
|
label={customFilter.label}
|
||||||
filters={customFilter.filters}
|
filters={customFilter.filters}
|
||||||
onRemovePress={onRemoveCustomFilterPress}
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
isDeleting={isDeleting}
|
||||||
|
deleteError={deleteError}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||||
onEditPress={onEditCustomFilter}
|
onEditPress={onEditCustomFilter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -58,9 +66,13 @@ function CustomFiltersModalContent(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomFiltersModalContent.propTypes = {
|
CustomFiltersModalContent.propTypes = {
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
|
deleteError: PropTypes.object,
|
||||||
|
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
||||||
|
dispatchSetFilter: PropTypes.func.isRequired,
|
||||||
onAddCustomFilter: PropTypes.func.isRequired,
|
onAddCustomFilter: PropTypes.func.isRequired,
|
||||||
onRemoveCustomFilterPress: PropTypes.func.isRequired,
|
|
||||||
onEditCustomFilter: PropTypes.func.isRequired,
|
onEditCustomFilter: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { deleteCustomFilter } from 'Store/Actions/customFilterActions';
|
||||||
|
import CustomFiltersModalContent from './CustomFiltersModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.customFilters.isDeleting,
|
||||||
|
(state) => state.customFilters.deleteError,
|
||||||
|
(isDeleting, deleteError) => {
|
||||||
|
return {
|
||||||
|
isDeleting,
|
||||||
|
deleteError
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchDeleteCustomFilter: deleteCustomFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(CustomFiltersModalContent);
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import FilterBuilderModalContentConnector from './Builder/FilterBuilderModalContentConnector';
|
import FilterBuilderModalContentConnector from './Builder/FilterBuilderModalContentConnector';
|
||||||
import CustomFiltersModalContent from './CustomFilters/CustomFiltersModalContent';
|
import CustomFiltersModalContentConnector from './CustomFilters/CustomFiltersModalContentConnector';
|
||||||
|
|
||||||
class FilterModal extends Component {
|
class FilterModal extends Component {
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ class FilterModal extends Component {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
filterBuilder: !props.customFilters.length,
|
filterBuilder: !props.customFilters.length,
|
||||||
customFilterKey: null
|
id: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,17 +27,17 @@ class FilterModal extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditCustomFilter = (customFilterKey) => {
|
onEditCustomFilter = (id) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
filterBuilder: true,
|
filterBuilder: true,
|
||||||
customFilterKey
|
id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onModalClose = () => {
|
onModalClose = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
filterBuilder: false,
|
filterBuilder: false,
|
||||||
customFilterKey: null
|
id: null
|
||||||
}, () => {
|
}, () => {
|
||||||
this.props.onModalClose();
|
this.props.onModalClose();
|
||||||
});
|
});
|
||||||
|
@ -54,7 +54,7 @@ class FilterModal extends Component {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filterBuilder,
|
filterBuilder,
|
||||||
customFilterKey
|
id
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -66,10 +66,10 @@ class FilterModal extends Component {
|
||||||
filterBuilder ?
|
filterBuilder ?
|
||||||
<FilterBuilderModalContentConnector
|
<FilterBuilderModalContentConnector
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
customFilterKey={customFilterKey}
|
id={id}
|
||||||
onModalClose={this.onModalClose}
|
onModalClose={this.onModalClose}
|
||||||
/> :
|
/> :
|
||||||
<CustomFiltersModalContent
|
<CustomFiltersModalContentConnector
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
onAddCustomFilter={this.onAddCustomFilter}
|
onAddCustomFilter={this.onAddCustomFilter}
|
||||||
onEditCustomFilter={this.onEditCustomFilter}
|
onEditCustomFilter={this.onEditCustomFilter}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import TetherComponent from 'react-tether';
|
import TetherComponent from 'react-tether';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import isMobileUtil from 'Utilities/isMobile';
|
import isMobileUtil from 'Utilities/isMobile';
|
||||||
|
@ -10,6 +9,7 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
import { icons, scrollDirections } from 'Helpers/Props';
|
import { icons, scrollDirections } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
|
|
@ -62,6 +62,7 @@ class PathInput extends Component {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const path = this.props.paths[0];
|
const path = this.props.paths[0];
|
||||||
|
|
||||||
|
if (path) {
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
name: this.props.name,
|
name: this.props.name,
|
||||||
value: path.path
|
value: path.path
|
||||||
|
@ -72,6 +73,7 @@ class PathInput extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onInputBlur = () => {
|
onInputBlur = () => {
|
||||||
this.props.onClearPaths();
|
this.props.onClearPaths();
|
||||||
|
|
|
@ -39,6 +39,10 @@ class TagInput extends Component {
|
||||||
this._autosuggestRef = null;
|
this._autosuggestRef = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.addTag.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
|
|
38
frontend/src/Components/Measure.js
Normal file
38
frontend/src/Components/Measure.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import ReactMeasure from 'react-measure';
|
||||||
|
|
||||||
|
class Measure extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.onMeasure.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMeasure = _.debounce((payload) => {
|
||||||
|
this.props.onMeasure(payload);
|
||||||
|
}, 250, { leading: true, trailing: false })
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ReactMeasure
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Measure.propTypes = {
|
||||||
|
onMeasure: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Measure;
|
|
@ -42,6 +42,7 @@ class FilterMenu extends Component {
|
||||||
customFilters,
|
customFilters,
|
||||||
buttonComponent: ButtonComponent,
|
buttonComponent: ButtonComponent,
|
||||||
filterModalConnectorComponent: FilterModalConnectorComponent,
|
filterModalConnectorComponent: FilterModalConnectorComponent,
|
||||||
|
filterModalConnectorComponentProps,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -74,6 +75,7 @@ class FilterMenu extends Component {
|
||||||
{
|
{
|
||||||
showCustomFilters &&
|
showCustomFilters &&
|
||||||
<FilterModalConnectorComponent
|
<FilterModalConnectorComponent
|
||||||
|
{...filterModalConnectorComponentProps}
|
||||||
isOpen={this.state.isFilterModalOpen}
|
isOpen={this.state.isFilterModalOpen}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
@ -90,11 +92,12 @@ class FilterMenu extends Component {
|
||||||
FilterMenu.propTypes = {
|
FilterMenu.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
buttonComponent: PropTypes.func.isRequired,
|
buttonComponent: PropTypes.func.isRequired,
|
||||||
filterModalConnectorComponent: PropTypes.func,
|
filterModalConnectorComponent: PropTypes.func,
|
||||||
|
filterModalConnectorComponentProps: PropTypes.object,
|
||||||
onFilterSelect: PropTypes.func.isRequired
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -42,8 +42,8 @@ class FilterMenuContent extends Component {
|
||||||
customFilters.map((filter) => {
|
customFilters.map((filter) => {
|
||||||
return (
|
return (
|
||||||
<FilterMenuItem
|
<FilterMenuItem
|
||||||
key={filter.key}
|
key={filter.id}
|
||||||
filterKey={filter.key}
|
filterKey={filter.id}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
onPress={onFilterSelect}
|
onPress={onFilterSelect}
|
||||||
>
|
>
|
||||||
|
@ -70,7 +70,7 @@ class FilterMenuContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterMenuContent.propTypes = {
|
FilterMenuContent.propTypes = {
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
showCustomFilters: PropTypes.bool.isRequired,
|
showCustomFilters: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -37,8 +37,8 @@ class FilterMenuItem extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterMenuItem.propTypes = {
|
FilterMenuItem.propTypes = {
|
||||||
filterKey: PropTypes.string.isRequired,
|
filterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
onPress: PropTypes.func.isRequired
|
onPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ import elementClass from 'element-class';
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
import { sizes } from 'Helpers/Props';
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||||
|
import ModalError from './ModalError';
|
||||||
import styles from './Modal.css';
|
import styles from './Modal.css';
|
||||||
|
|
||||||
const openModals = [];
|
const openModals = [];
|
||||||
|
@ -153,7 +155,8 @@ class Modal extends Component {
|
||||||
backdropClassName,
|
backdropClassName,
|
||||||
size,
|
size,
|
||||||
children,
|
children,
|
||||||
isOpen
|
isOpen,
|
||||||
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
|
@ -176,8 +179,13 @@ class Modal extends Component {
|
||||||
styles[size]
|
styles[size]
|
||||||
)}
|
)}
|
||||||
style={style}
|
style={style}
|
||||||
|
>
|
||||||
|
<ErrorBoundary
|
||||||
|
errorComponent={ModalError}
|
||||||
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
|
|
15
frontend/src/Components/Modal/ModalError.css
Normal file
15
frontend/src/Components/Modal/ModalError.css
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
.message {
|
||||||
|
composes: message from 'Components/Error/ErrorBoundaryError.css';
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
composes: details from 'Components/Error/ErrorBoundaryError.css';
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
46
frontend/src/Components/Modal/ModalError.js
Normal file
46
frontend/src/Components/Modal/ModalError.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import styles from './ModalError.css';
|
||||||
|
|
||||||
|
function ModalError(props) {
|
||||||
|
const {
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Error
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<ErrorBoundaryError
|
||||||
|
messageClassName={styles.message}
|
||||||
|
detailsClassName={styles.details}
|
||||||
|
{...otherProps}
|
||||||
|
message='There was an error loading this item'
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModalError.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalError;
|
|
@ -8,8 +8,11 @@ function ErrorPage(props) {
|
||||||
version,
|
version,
|
||||||
isLocalStorageSupported,
|
isLocalStorageSupported,
|
||||||
artistError,
|
artistError,
|
||||||
|
customFiltersError,
|
||||||
tagsError,
|
tagsError,
|
||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
|
languageProfilesError,
|
||||||
|
metadataProfilesError,
|
||||||
uiSettingsError
|
uiSettingsError
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -19,10 +22,16 @@ function ErrorPage(props) {
|
||||||
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
|
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
|
||||||
} else if (artistError) {
|
} else if (artistError) {
|
||||||
errorMessage = getErrorMessage(artistError, 'Failed to load artist from API');
|
errorMessage = getErrorMessage(artistError, 'Failed to load artist from API');
|
||||||
|
} else if (customFiltersError) {
|
||||||
|
errorMessage = getErrorMessage(customFiltersError, 'Failed to load custom filters from API');
|
||||||
} else if (tagsError) {
|
} else if (tagsError) {
|
||||||
errorMessage = getErrorMessage(artistError, 'Failed to load artist from API');
|
errorMessage = getErrorMessage(tagsError, 'Failed to load tags from API');
|
||||||
} else if (qualityProfilesError) {
|
} else if (qualityProfilesError) {
|
||||||
errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API');
|
errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API');
|
||||||
|
} else if (languageProfilesError) {
|
||||||
|
errorMessage = getErrorMessage(languageProfilesError, 'Failed to load language profiles from API');
|
||||||
|
} else if (metadataProfilesError) {
|
||||||
|
errorMessage = getErrorMessage(metadataProfilesError, 'Failed to load metadata profiles from API');
|
||||||
} else if (uiSettingsError) {
|
} else if (uiSettingsError) {
|
||||||
errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API');
|
errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API');
|
||||||
}
|
}
|
||||||
|
@ -44,8 +53,11 @@ ErrorPage.propTypes = {
|
||||||
version: PropTypes.string.isRequired,
|
version: PropTypes.string.isRequired,
|
||||||
isLocalStorageSupported: PropTypes.bool.isRequired,
|
isLocalStorageSupported: PropTypes.bool.isRequired,
|
||||||
artistError: PropTypes.object,
|
artistError: PropTypes.object,
|
||||||
|
customFiltersError: PropTypes.object,
|
||||||
tagsError: PropTypes.object,
|
tagsError: PropTypes.object,
|
||||||
qualityProfilesError: PropTypes.object,
|
qualityProfilesError: PropTypes.object,
|
||||||
|
languageProfilesError: PropTypes.object,
|
||||||
|
metadataProfilesError: PropTypes.object,
|
||||||
uiSettingsError: PropTypes.object
|
uiSettingsError: PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { withRouter } from 'react-router-dom';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||||
|
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||||
import { fetchArtist } from 'Store/Actions/artistActions';
|
import { fetchArtist } from 'Store/Actions/artistActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import { fetchQualityProfiles, fetchLanguageProfiles, fetchMetadataProfiles, fetchUISettings, fetchImportLists } from 'Store/Actions/settingsActions';
|
import { fetchQualityProfiles, fetchLanguageProfiles, fetchMetadataProfiles, fetchUISettings, fetchImportLists } from 'Store/Actions/settingsActions';
|
||||||
|
@ -30,13 +31,15 @@ function testLocalStorage() {
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.artist,
|
(state) => state.artist,
|
||||||
|
(state) => state.customFilters,
|
||||||
(state) => state.tags,
|
(state) => state.tags,
|
||||||
(state) => state.settings,
|
(state) => state.settings,
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(artist, tags, settings, app, dimensions) => {
|
(artist, customFilters, tags, settings, app, dimensions) => {
|
||||||
const isPopulated = (
|
const isPopulated = (
|
||||||
artist.isPopulated &&
|
artist.isPopulated &&
|
||||||
|
customFilters.isPopulated &&
|
||||||
tags.isPopulated &&
|
tags.isPopulated &&
|
||||||
settings.qualityProfiles.isPopulated &&
|
settings.qualityProfiles.isPopulated &&
|
||||||
settings.languageProfiles.isPopulated &&
|
settings.languageProfiles.isPopulated &&
|
||||||
|
@ -47,6 +50,7 @@ function createMapStateToProps() {
|
||||||
|
|
||||||
const hasError = !!(
|
const hasError = !!(
|
||||||
artist.error ||
|
artist.error ||
|
||||||
|
customFilters.error ||
|
||||||
tags.error ||
|
tags.error ||
|
||||||
settings.qualityProfiles.error ||
|
settings.qualityProfiles.error ||
|
||||||
settings.languageProfiles.error ||
|
settings.languageProfiles.error ||
|
||||||
|
@ -59,6 +63,7 @@ function createMapStateToProps() {
|
||||||
isPopulated,
|
isPopulated,
|
||||||
hasError,
|
hasError,
|
||||||
artistError: artist.error,
|
artistError: artist.error,
|
||||||
|
customFiltersError: tags.error,
|
||||||
tagsError: tags.error,
|
tagsError: tags.error,
|
||||||
qualityProfilesError: settings.qualityProfiles.error,
|
qualityProfilesError: settings.qualityProfiles.error,
|
||||||
languageProfilesError: settings.languageProfiles.error,
|
languageProfilesError: settings.languageProfiles.error,
|
||||||
|
@ -80,6 +85,9 @@ function createMapDispatchToProps(dispatch, props) {
|
||||||
dispatchFetchArtist() {
|
dispatchFetchArtist() {
|
||||||
dispatch(fetchArtist());
|
dispatch(fetchArtist());
|
||||||
},
|
},
|
||||||
|
dispatchFetchCustomFilters() {
|
||||||
|
dispatch(fetchCustomFilters());
|
||||||
|
},
|
||||||
dispatchFetchTags() {
|
dispatchFetchTags() {
|
||||||
dispatch(fetchTags());
|
dispatch(fetchTags());
|
||||||
},
|
},
|
||||||
|
@ -126,6 +134,7 @@ class PageConnector extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!this.props.isPopulated) {
|
if (!this.props.isPopulated) {
|
||||||
this.props.dispatchFetchArtist();
|
this.props.dispatchFetchArtist();
|
||||||
|
this.props.dispatchFetchCustomFilters();
|
||||||
this.props.dispatchFetchTags();
|
this.props.dispatchFetchTags();
|
||||||
this.props.dispatchFetchQualityProfiles();
|
this.props.dispatchFetchQualityProfiles();
|
||||||
this.props.dispatchFetchLanguageProfiles();
|
this.props.dispatchFetchLanguageProfiles();
|
||||||
|
@ -190,6 +199,7 @@ PageConnector.propTypes = {
|
||||||
hasError: PropTypes.bool.isRequired,
|
hasError: PropTypes.bool.isRequired,
|
||||||
isSidebarVisible: PropTypes.bool.isRequired,
|
isSidebarVisible: PropTypes.bool.isRequired,
|
||||||
dispatchFetchArtist: PropTypes.func.isRequired,
|
dispatchFetchArtist: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
||||||
dispatchFetchTags: PropTypes.func.isRequired,
|
dispatchFetchTags: PropTypes.func.isRequired,
|
||||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||||
dispatchFetchLanguageProfiles: PropTypes.func.isRequired,
|
dispatchFetchLanguageProfiles: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DocumentTitle from 'react-document-title';
|
import DocumentTitle from 'react-document-title';
|
||||||
|
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||||
|
import PageContentError from './PageContentError';
|
||||||
import styles from './PageContent.css';
|
import styles from './PageContent.css';
|
||||||
|
|
||||||
function PageContent(props) {
|
function PageContent(props) {
|
||||||
|
@ -11,11 +13,13 @@ function PageContent(props) {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ErrorBoundary errorComponent={PageContentError}>
|
||||||
<DocumentTitle title={title ? `${title} - Lidarr` : 'Lidarr'}>
|
<DocumentTitle title={title ? `${title} - Lidarr` : 'Lidarr'}>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
frontend/src/Components/Page/PageContentError.css
Normal file
3
frontend/src/Components/Page/PageContentError.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.content {
|
||||||
|
composes: content from './PageContent.css';
|
||||||
|
}
|
19
frontend/src/Components/Page/PageContentError.js
Normal file
19
frontend/src/Components/Page/PageContentError.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
||||||
|
import PageContentBodyConnector from './PageContentBodyConnector';
|
||||||
|
import styles from './PageContentError.css';
|
||||||
|
|
||||||
|
function PageContentError(props) {
|
||||||
|
return (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<PageContentBodyConnector>
|
||||||
|
<ErrorBoundaryError
|
||||||
|
{...props}
|
||||||
|
message='There was an error loading this page'
|
||||||
|
/>
|
||||||
|
</PageContentBodyConnector>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageContentError;
|
|
@ -1,8 +1,8 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import PageJumpBarItem from './PageJumpBarItem';
|
import PageJumpBarItem from './PageJumpBarItem';
|
||||||
import styles from './PageJumpBar.css';
|
import styles from './PageJumpBar.css';
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { forEach } from 'Helpers/elementChildren';
|
import { forEach } from 'Helpers/elementChildren';
|
||||||
import { align, icons } from 'Helpers/Props';
|
import { align, icons } from 'Helpers/Props';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import Menu from 'Components/Menu/Menu';
|
import Menu from 'Components/Menu/Menu';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
import MenuItem from 'Components/Menu/MenuItem';
|
import MenuItem from 'Components/Menu/MenuItem';
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import { WindowScroller } from 'react-virtualized';
|
import { WindowScroller } from 'react-virtualized';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
import VirtualTableBody from './VirtualTableBody';
|
import VirtualTableBody from './VirtualTableBody';
|
||||||
import styles from './VirtualTable.css';
|
import styles from './VirtualTable.css';
|
||||||
|
|
BIN
frontend/src/Content/Images/error.png
Normal file
BIN
frontend/src/Content/Images/error.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
|
@ -1,4 +1,5 @@
|
||||||
export const BOOL = 'bool';
|
export const BOOL = 'bool';
|
||||||
|
export const BYTES = 'bytes';
|
||||||
export const DATE = 'date';
|
export const DATE = 'date';
|
||||||
export const DEFAULT = 'default';
|
export const DEFAULT = 'default';
|
||||||
export const INDEXER = 'indexer';
|
export const INDEXER = 'indexer';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
|
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||||
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
|
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
|
||||||
import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
|
import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
|
||||||
|
@ -248,7 +248,7 @@ class InteractiveImportRow extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
!showQualityPlaceholder && !!quality &&
|
!showQualityPlaceholder && !!quality &&
|
||||||
<EpisodeQuality
|
<TrackQuality
|
||||||
className={styles.label}
|
className={styles.label}
|
||||||
quality={quality}
|
quality={quality}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import * as releaseActions from 'Store/Actions/releaseActions';
|
import { setReleasesFilter } from 'Store/Actions/releaseActions';
|
||||||
import FilterModal from 'Components/Filter/FilterModal';
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -10,7 +10,8 @@ function createMapStateToProps() {
|
||||||
(sectionItems, filterBuilderProps) => {
|
(sectionItems, filterBuilderProps) => {
|
||||||
return {
|
return {
|
||||||
sectionItems,
|
sectionItems,
|
||||||
filterBuilderProps
|
filterBuilderProps,
|
||||||
|
customFilterType: 'releases'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -18,12 +19,10 @@ function createMapStateToProps() {
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
return {
|
return {
|
||||||
onRemoveCustomFilterPress(payload) {
|
dispatchSetFilter(payload) {
|
||||||
dispatch(releaseActions.removeReleasesCustomFilter(payload));
|
const action = setReleasesFilter;
|
||||||
},
|
|
||||||
|
|
||||||
onSaveCustomFilterPress(payload) {
|
dispatch(action(payload));
|
||||||
dispatch(releaseActions.saveReleasesCustomFilter(payload));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import InteractiveAlbumSearchModalContentConnector from './InteractiveAlbumSearchModalContentConnector';
|
import InteractiveSearchModalContentConnector from './InteractiveSearchModalContentConnector';
|
||||||
|
|
||||||
function InteractiveAlbumSearchModal(props) {
|
function InteractiveSearchModal(props) {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
|
@ -15,7 +15,7 @@ function InteractiveAlbumSearchModal(props) {
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
<InteractiveAlbumSearchModalContentConnector
|
<InteractiveSearchModalContentConnector
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
/>
|
/>
|
||||||
|
@ -23,9 +23,9 @@ function InteractiveAlbumSearchModal(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveAlbumSearchModal.propTypes = {
|
InteractiveSearchModal.propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InteractiveAlbumSearchModal;
|
export default InteractiveSearchModal;
|
|
@ -13,8 +13,8 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||||
import InteractiveAlbumSearchRow from './InteractiveAlbumSearchRow';
|
import InteractiveSearchRow from './InteractiveSearchRow';
|
||||||
import styles from './InteractiveAlbumSearchModalContent.css';
|
import styles from './InteractiveSearchModalContent.css';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
|
@ -75,7 +75,7 @@ const columns = [
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
class InteractiveAlbumSearchModalContent extends Component {
|
class InteractiveSearchModalContent extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
@ -161,7 +161,7 @@ class InteractiveAlbumSearchModalContent extends Component {
|
||||||
{
|
{
|
||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<InteractiveAlbumSearchRow
|
<InteractiveSearchRow
|
||||||
key={item.guid}
|
key={item.guid}
|
||||||
{...item}
|
{...item}
|
||||||
longDateFormat={longDateFormat}
|
longDateFormat={longDateFormat}
|
||||||
|
@ -195,7 +195,7 @@ class InteractiveAlbumSearchModalContent extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveAlbumSearchModalContent.propTypes = {
|
InteractiveSearchModalContent.propTypes = {
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
|
@ -203,7 +203,7 @@ InteractiveAlbumSearchModalContent.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string.isRequired,
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
|
@ -214,4 +214,4 @@ InteractiveAlbumSearchModalContent.propTypes = {
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InteractiveAlbumSearchModalContent;
|
export default InteractiveSearchModalContent;
|
|
@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
|
||||||
import * as releaseActions from 'Store/Actions/releaseActions';
|
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import InteractiveAlbumSearchModalContent from './InteractiveAlbumSearchModalContent';
|
import InteractiveSearchModalContent from './InteractiveSearchModalContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
@ -51,7 +51,7 @@ function createMapDispatchToProps(dispatch, props) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class InteractiveAlbumSearchModalContentConnector extends Component {
|
class InteractiveSearchModalContentConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
@ -81,18 +81,18 @@ class InteractiveAlbumSearchModalContentConnector extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InteractiveAlbumSearchModalContent
|
<InteractiveSearchModalContent
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveAlbumSearchModalContentConnector.propTypes = {
|
InteractiveSearchModalContentConnector.propTypes = {
|
||||||
albumId: PropTypes.number,
|
albumId: PropTypes.number,
|
||||||
dispatchFetchReleases: PropTypes.func.isRequired,
|
dispatchFetchReleases: PropTypes.func.isRequired,
|
||||||
dispatchClearReleases: PropTypes.func.isRequired,
|
dispatchClearReleases: PropTypes.func.isRequired,
|
||||||
dispatchCancelFetchReleases: PropTypes.func.isRequired
|
dispatchCancelFetchReleases: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveAlbumSearchModalContentConnector);
|
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchModalContentConnector);
|
|
@ -10,10 +10,10 @@ import Link from 'Components/Link/Link';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||||
import Peers from './Peers';
|
import Peers from './Peers';
|
||||||
import styles from './InteractiveAlbumSearchRow.css';
|
import styles from './InteractiveSearchRow.css';
|
||||||
|
|
||||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||||
if (isGrabbing) {
|
if (isGrabbing) {
|
||||||
|
@ -39,7 +39,7 @@ function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||||
return 'Add to downloaded queue';
|
return 'Add to downloaded queue';
|
||||||
}
|
}
|
||||||
|
|
||||||
class InteractiveAlbumSearchRow extends Component {
|
class InteractiveSearchRow extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
@ -120,7 +120,7 @@ class InteractiveAlbumSearchRow extends Component {
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.quality}>
|
<TableRowCell className={styles.quality}>
|
||||||
<EpisodeQuality
|
<TrackQuality
|
||||||
quality={quality}
|
quality={quality}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
@ -171,7 +171,7 @@ class InteractiveAlbumSearchRow extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveAlbumSearchRow.propTypes = {
|
InteractiveSearchRow.propTypes = {
|
||||||
guid: PropTypes.string.isRequired,
|
guid: PropTypes.string.isRequired,
|
||||||
protocol: PropTypes.string.isRequired,
|
protocol: PropTypes.string.isRequired,
|
||||||
age: PropTypes.number.isRequired,
|
age: PropTypes.number.isRequired,
|
||||||
|
@ -196,9 +196,9 @@ InteractiveAlbumSearchRow.propTypes = {
|
||||||
onGrabPress: PropTypes.func.isRequired
|
onGrabPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
InteractiveAlbumSearchRow.defaultProps = {
|
InteractiveSearchRow.defaultProps = {
|
||||||
isGrabbing: false,
|
isGrabbing: false,
|
||||||
isGrabbed: false
|
isGrabbed: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InteractiveAlbumSearchRow;
|
export default InteractiveSearchRow;
|
|
@ -1,10 +1,10 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import FieldSet from 'Components/FieldSet';
|
import FieldSet from 'Components/FieldSet';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
import DelayProfileDragSource from './DelayProfileDragSource';
|
import DelayProfileDragSource from './DelayProfileDragSource';
|
||||||
import DelayProfileDragPreview from './DelayProfileDragPreview';
|
import DelayProfileDragPreview from './DelayProfileDragPreview';
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Measure from 'react-measure';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
import QualityProfileItemDragSource from './QualityProfileItemDragSource';
|
import QualityProfileItemDragSource from './QualityProfileItemDragSource';
|
||||||
import QualityProfileItemDragPreview from './QualityProfileItemDragPreview';
|
import QualityProfileItemDragPreview from './QualityProfileItemDragPreview';
|
||||||
import styles from './QualityProfileItems.css';
|
import styles from './QualityProfileItems.css';
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
import customFilterHandlers from 'Utilities/customFilterHandlers';
|
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
|
||||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
|
||||||
import generateUUIDv4 from 'Utilities/String/generateUUIDv4';
|
|
||||||
|
|
||||||
function createRemoveCustomFilterReducer(section) {
|
|
||||||
return (state, { payload }) => {
|
|
||||||
const newState = getSectionState(state, section);
|
|
||||||
const index = newState.customFilters.findIndex((c) => c.key === payload.key);
|
|
||||||
|
|
||||||
newState.customFilters = [...newState.customFilters];
|
|
||||||
newState.customFilters.splice(index, 1);
|
|
||||||
|
|
||||||
// Reset the selected filter to the first filter if the selected filter
|
|
||||||
// is being deleted.
|
|
||||||
// TODO: Server side collections need to have their collections refetched
|
|
||||||
|
|
||||||
if (newState.selectedFilterKey === payload.key) {
|
|
||||||
newState.selectedFilterKey = newState.filters[0].key;
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateSectionState(state, section, newState);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSaveCustomFilterReducer(section) {
|
|
||||||
return (state, { payload }) => {
|
|
||||||
const newState = getSectionState(state, section);
|
|
||||||
|
|
||||||
const {
|
|
||||||
label,
|
|
||||||
filters
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
let key = payload.key;
|
|
||||||
|
|
||||||
newState.customFilters = [...newState.customFilters];
|
|
||||||
|
|
||||||
if (key) {
|
|
||||||
const index = newState.customFilters.findIndex((c) => c.key === key);
|
|
||||||
|
|
||||||
newState.customFilters.splice(index, 1, { key, label, filters });
|
|
||||||
} else {
|
|
||||||
key = generateUUIDv4();
|
|
||||||
|
|
||||||
newState.customFilters.push({
|
|
||||||
key,
|
|
||||||
label,
|
|
||||||
filters
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Server side collections need to have their collections refetched
|
|
||||||
newState.selectedFilterKey = key;
|
|
||||||
|
|
||||||
return updateSectionState(state, section, newState);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function createCustomFilterReducers(section, handlers) {
|
|
||||||
return {
|
|
||||||
[handlers[customFilterHandlers.REMOVE]]: createRemoveCustomFilterReducer(section),
|
|
||||||
[handlers[customFilterHandlers.SAVE]]: createSaveCustomFilterReducer(section)
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -20,13 +20,13 @@ function createRemoveItemHandler(section, url) {
|
||||||
|
|
||||||
promise.done((data) => {
|
promise.done((data) => {
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
removeItem({ section, id }),
|
|
||||||
|
|
||||||
set({
|
set({
|
||||||
section,
|
section,
|
||||||
isDeleting: false,
|
isDeleting: false,
|
||||||
deleteError: null
|
deleteError: null
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
removeItem({ section, id })
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -21,10 +21,11 @@ function createSaveProviderHandler(section, url, options = {}) {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
queryParams = {}
|
queryParams = {},
|
||||||
|
...otherPayload
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const saveData = getProviderState(payload, getState, section);
|
const saveData = getProviderState({ id, ...otherPayload }, getState, section);
|
||||||
|
|
||||||
const ajaxOptions = {
|
const ajaxOptions = {
|
||||||
url: `${url}?${$.param(queryParams, true)}`,
|
url: `${url}?${$.param(queryParams, true)}`,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import getMonitoringOptions from 'Utilities/Artist/getMonitoringOptions';
|
import getMonitoringOptions from 'Utilities/Artist/getMonitoringOptions';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||||
|
@ -29,7 +29,55 @@ export const defaultState = {
|
||||||
selectedFilterKey: 'all',
|
selectedFilterKey: 'all',
|
||||||
filters,
|
filters,
|
||||||
filterPredicates,
|
filterPredicates,
|
||||||
customFilters: []
|
|
||||||
|
filterBuilderProps: [
|
||||||
|
{
|
||||||
|
name: 'monitored',
|
||||||
|
label: 'Monitored',
|
||||||
|
type: filterBuilderTypes.EXACT,
|
||||||
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: 'Status',
|
||||||
|
type: filterBuilderTypes.EXACT,
|
||||||
|
valueType: filterBuilderValueTypes.ARTIST_STATUS
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'artistType',
|
||||||
|
label: 'Artist Type',
|
||||||
|
type: filterBuilderTypes.EXACT
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'qualityProfileId',
|
||||||
|
label: 'Quality Profile',
|
||||||
|
type: filterBuilderTypes.EXACT,
|
||||||
|
valueType: filterBuilderValueTypes.QUALITY_PROFILE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'languageProfileId',
|
||||||
|
label: 'Language Profile',
|
||||||
|
type: filterBuilderTypes.EXACT,
|
||||||
|
valueType: filterBuilderValueTypes.LANGUAGE_PROFILE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'metadataProfileId',
|
||||||
|
label: 'Metadata Profile',
|
||||||
|
type: filterBuilderTypes.EXACT,
|
||||||
|
valueType: filterBuilderValueTypes.METADATA_PROFILE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rootFolderPath',
|
||||||
|
label: 'Root Folder Path',
|
||||||
|
type: filterBuilderTypes.EXACT
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
label: 'Tags',
|
||||||
|
type: filterBuilderTypes.ARRAY,
|
||||||
|
valueType: filterBuilderValueTypes.TAG
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
|
|
|
@ -103,6 +103,20 @@ export const filterPredicates = {
|
||||||
const predicate = filterTypePredicates[type];
|
const predicate = filterTypePredicates[type];
|
||||||
|
|
||||||
return predicate(item.ratings.value * 10, filterValue);
|
return predicate(item.ratings.value * 10, filterValue);
|
||||||
|
},
|
||||||
|
|
||||||
|
albumCount: function(item, filterValue, type) {
|
||||||
|
const predicate = filterTypePredicates[type];
|
||||||
|
const albumCount = item.statistics ? item.statistics.albumCount : 0;
|
||||||
|
|
||||||
|
return predicate(albumCount, filterValue);
|
||||||
|
},
|
||||||
|
|
||||||
|
sizeOnDisk: function(item, filterValue, type) {
|
||||||
|
const predicate = filterTypePredicates[type];
|
||||||
|
const sizeOnDisk = item.statistics ? item.statistics.sizeOnDisk : 0;
|
||||||
|
|
||||||
|
return predicate(sizeOnDisk, filterValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
import customFilterHandlers from 'Utilities/customFilterHandlers';
|
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||||
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
|
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
import { set, updateItem } from './baseActions';
|
import { set, updateItem } from './baseActions';
|
||||||
import { filters, filterPredicates } from './artistActions';
|
import { filters, filterPredicates } from './artistActions';
|
||||||
|
@ -79,8 +77,7 @@ export const defaultState = {
|
||||||
type: filterBuilderTypes.ARRAY,
|
type: filterBuilderTypes.ARRAY,
|
||||||
valueType: filterBuilderValueTypes.TAG
|
valueType: filterBuilderValueTypes.TAG
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
customFilters: []
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
|
@ -97,8 +94,6 @@ export const SET_ARTIST_EDITOR_SORT = 'artistEditor/setArtistEditorSort';
|
||||||
export const SET_ARTIST_EDITOR_FILTER = 'artistEditor/setArtistEditorFilter';
|
export const SET_ARTIST_EDITOR_FILTER = 'artistEditor/setArtistEditorFilter';
|
||||||
export const SAVE_ARTIST_EDITOR = 'artistEditor/saveArtistEditor';
|
export const SAVE_ARTIST_EDITOR = 'artistEditor/saveArtistEditor';
|
||||||
export const BULK_DELETE_ARTIST = 'artistEditor/bulkDeleteArtist';
|
export const BULK_DELETE_ARTIST = 'artistEditor/bulkDeleteArtist';
|
||||||
export const REMOVE_ARTIST_EDITOR_CUSTOM_FILTER = 'artistEditor/removeArtistEditorCustomFilter';
|
|
||||||
export const SAVE_ARTIST_EDITOR_CUSTOM_FILTER = 'artistEditor/saveArtistEditorCustomFilter';
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -107,8 +102,6 @@ export const setArtistEditorSort = createAction(SET_ARTIST_EDITOR_SORT);
|
||||||
export const setArtistEditorFilter = createAction(SET_ARTIST_EDITOR_FILTER);
|
export const setArtistEditorFilter = createAction(SET_ARTIST_EDITOR_FILTER);
|
||||||
export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR);
|
export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR);
|
||||||
export const bulkDeleteArtist = createThunk(BULK_DELETE_ARTIST);
|
export const bulkDeleteArtist = createThunk(BULK_DELETE_ARTIST);
|
||||||
export const removeArtistEditorCustomFilter = createAction(REMOVE_ARTIST_EDITOR_CUSTOM_FILTER);
|
|
||||||
export const saveArtistEditorCustomFilter = createAction(SAVE_ARTIST_EDITOR_CUSTOM_FILTER);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Handlers
|
// Action Handlers
|
||||||
|
@ -193,11 +186,6 @@ export const actionHandlers = handleThunks({
|
||||||
export const reducers = createHandleActions({
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
[SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
|
[SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||||
[SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section),
|
[SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)
|
||||||
|
|
||||||
...createCustomFilterReducers(section, {
|
|
||||||
[customFilterHandlers.REMOVE]: REMOVE_ARTIST_EDITOR_CUSTOM_FILTER,
|
|
||||||
[customFilterHandlers.SAVE]: SAVE_ARTIST_EDITOR_CUSTOM_FILTER
|
|
||||||
})
|
|
||||||
|
|
||||||
}, defaultState, section);
|
}, defaultState, section);
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import customFilterHandlers from 'Utilities/customFilterHandlers';
|
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||||
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
||||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||||
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
|
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
import { filters, filterPredicates } from './artistActions';
|
import { filters, filterPredicates } from './artistActions';
|
||||||
|
|
||||||
|
@ -292,7 +290,8 @@ export const defaultState = {
|
||||||
{
|
{
|
||||||
name: 'sizeOnDisk',
|
name: 'sizeOnDisk',
|
||||||
label: 'Size on Disk',
|
label: 'Size on Disk',
|
||||||
type: filterBuilderTypes.NUMBER
|
type: filterBuilderTypes.NUMBER,
|
||||||
|
valueType: filterBuilderValueTypes.BYTES
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'genres',
|
name: 'genres',
|
||||||
|
@ -324,8 +323,7 @@ export const defaultState = {
|
||||||
type: filterBuilderTypes.ARRAY,
|
type: filterBuilderTypes.ARRAY,
|
||||||
valueType: filterBuilderValueTypes.TAG
|
valueType: filterBuilderValueTypes.TAG
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
customFilters: []
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
|
@ -350,8 +348,6 @@ export const SET_ARTIST_TABLE_OPTION = 'artistIndex/setArtistTableOption';
|
||||||
export const SET_ARTIST_POSTER_OPTION = 'artistIndex/setArtistPosterOption';
|
export const SET_ARTIST_POSTER_OPTION = 'artistIndex/setArtistPosterOption';
|
||||||
export const SET_ARTIST_BANNER_OPTION = 'artistIndex/setArtistBannerOption';
|
export const SET_ARTIST_BANNER_OPTION = 'artistIndex/setArtistBannerOption';
|
||||||
export const SET_ARTIST_OVERVIEW_OPTION = 'artistIndex/setArtistOverviewOption';
|
export const SET_ARTIST_OVERVIEW_OPTION = 'artistIndex/setArtistOverviewOption';
|
||||||
export const REMOVE_ARTIST_CUSTOM_FILTER = 'artistIndex/removeArtistCustomFilter';
|
|
||||||
export const SAVE_ARTIST_CUSTOM_FILTER = 'artistIndex/saveArtistCustomFilter';
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -363,8 +359,7 @@ export const setArtistTableOption = createAction(SET_ARTIST_TABLE_OPTION);
|
||||||
export const setArtistPosterOption = createAction(SET_ARTIST_POSTER_OPTION);
|
export const setArtistPosterOption = createAction(SET_ARTIST_POSTER_OPTION);
|
||||||
export const setArtistBannerOption = createAction(SET_ARTIST_BANNER_OPTION);
|
export const setArtistBannerOption = createAction(SET_ARTIST_BANNER_OPTION);
|
||||||
export const setArtistOverviewOption = createAction(SET_ARTIST_OVERVIEW_OPTION);
|
export const setArtistOverviewOption = createAction(SET_ARTIST_OVERVIEW_OPTION);
|
||||||
export const removeArtistCustomFilter = createAction(REMOVE_ARTIST_CUSTOM_FILTER);
|
|
||||||
export const saveArtistCustomFilter = createAction(SAVE_ARTIST_CUSTOM_FILTER);
|
|
||||||
//
|
//
|
||||||
// Reducers
|
// Reducers
|
||||||
|
|
||||||
|
@ -413,11 +408,6 @@ export const reducers = createHandleActions({
|
||||||
...payload
|
...payload
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
...createCustomFilterReducers(section, {
|
|
||||||
[customFilterHandlers.REMOVE]: REMOVE_ARTIST_CUSTOM_FILTER,
|
|
||||||
[customFilterHandlers.SAVE]: SAVE_ARTIST_CUSTOM_FILTER
|
|
||||||
})
|
|
||||||
|
|
||||||
}, defaultState, section);
|
}, defaultState, section);
|
||||||
|
|
55
frontend/src/Store/Actions/customFilterActions.js
Normal file
55
frontend/src/Store/Actions/customFilterActions.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import createFetchHandler from './Creators/createFetchHandler';
|
||||||
|
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||||
|
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'customFilters';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: null,
|
||||||
|
items: [],
|
||||||
|
pendingChanges: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_CUSTOM_FILTERS = 'customFilters/fetchCustomFilters';
|
||||||
|
export const SAVE_CUSTOM_FILTER = 'customFilters/saveCustomFilter';
|
||||||
|
export const DELETE_CUSTOM_FILTER = 'customFilters/deleteCustomFilter';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchCustomFilters = createThunk(FETCH_CUSTOM_FILTERS);
|
||||||
|
export const saveCustomFilter = createThunk(SAVE_CUSTOM_FILTER);
|
||||||
|
export const deleteCustomFilter = createThunk(DELETE_CUSTOM_FILTER);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
[FETCH_CUSTOM_FILTERS]: createFetchHandler(section, '/customFilter'),
|
||||||
|
|
||||||
|
[SAVE_CUSTOM_FILTER]: createSaveProviderHandler(section, '/customFilter'),
|
||||||
|
|
||||||
|
[DELETE_CUSTOM_FILTER]: createRemoveItemHandler(section, '/customFilter')
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
export const reducers = createHandleActions({}, defaultState, section);
|
|
@ -2,6 +2,7 @@ import * as addArtist from './addArtistActions';
|
||||||
import * as app from './appActions';
|
import * as app from './appActions';
|
||||||
import * as blacklist from './blacklistActions';
|
import * as blacklist from './blacklistActions';
|
||||||
import * as captcha from './captchaActions';
|
import * as captcha from './captchaActions';
|
||||||
|
import * as customFilters from './customFilterActions';
|
||||||
import * as devices from './deviceActions';
|
import * as devices from './deviceActions';
|
||||||
import * as calendar from './calendarActions';
|
import * as calendar from './calendarActions';
|
||||||
import * as commands from './commandActions';
|
import * as commands from './commandActions';
|
||||||
|
@ -35,6 +36,7 @@ export default [
|
||||||
captcha,
|
captcha,
|
||||||
calendar,
|
calendar,
|
||||||
commands,
|
commands,
|
||||||
|
customFilters,
|
||||||
devices,
|
devices,
|
||||||
albums,
|
albums,
|
||||||
trackFiles,
|
trackFiles,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import createHandleActions from './Creators/createHandleActions';
|
||||||
// Variables
|
// Variables
|
||||||
|
|
||||||
export const section = 'oAuth';
|
export const section = 'oAuth';
|
||||||
|
const callbackUrl = `${window.location.origin}${window.Lidarr.urlBase}/oauth.html`;
|
||||||
|
|
||||||
//
|
//
|
||||||
// State
|
// State
|
||||||
|
@ -64,6 +65,19 @@ function showOAuthWindow(url) {
|
||||||
return deferred.promise();
|
return deferred.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function executeIntermediateRequest(payload, ajaxOptions) {
|
||||||
|
return $.ajax(ajaxOptions).then((data) => {
|
||||||
|
return requestAction({
|
||||||
|
action: 'continueOAuth',
|
||||||
|
queryParams: {
|
||||||
|
...data,
|
||||||
|
callbackUrl
|
||||||
|
},
|
||||||
|
...payload
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Handlers
|
// Action Handlers
|
||||||
|
|
||||||
|
@ -72,7 +86,7 @@ export const actionHandlers = handleThunks({
|
||||||
[START_OAUTH]: function(getState, payload, dispatch) {
|
[START_OAUTH]: function(getState, payload, dispatch) {
|
||||||
const actionPayload = {
|
const actionPayload = {
|
||||||
action: 'startOAuth',
|
action: 'startOAuth',
|
||||||
queryParams: { callbackUrl: `${window.location.origin}${window.Lidarr.urlBase}/oauth.html` },
|
queryParams: { callbackUrl },
|
||||||
...payload
|
...payload
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -85,7 +99,16 @@ export const actionHandlers = handleThunks({
|
||||||
const promise = requestAction(actionPayload)
|
const promise = requestAction(actionPayload)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
startResponse = response;
|
startResponse = response;
|
||||||
|
|
||||||
|
if (response.oauthUrl) {
|
||||||
return showOAuthWindow(response.oauthUrl);
|
return showOAuthWindow(response.oauthUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return executeIntermediateRequest(payload, response).then((intermediateResponse) => {
|
||||||
|
startResponse = intermediateResponse;
|
||||||
|
|
||||||
|
return showOAuthWindow(intermediateResponse.oauthUrl);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then((queryParams) => {
|
.then((queryParams) => {
|
||||||
return requestAction({
|
return requestAction({
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import customFilterHandlers from 'Utilities/customFilterHandlers';
|
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||||
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
|
|
||||||
import createFetchHandler from './Creators/createFetchHandler';
|
import createFetchHandler from './Creators/createFetchHandler';
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
|
||||||
|
@ -45,8 +43,6 @@ export const defaultState = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
selectedFilterKey: 'all',
|
|
||||||
|
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'all',
|
key: 'all',
|
||||||
|
@ -143,9 +139,7 @@ export const defaultState = {
|
||||||
label: 'Rejections',
|
label: 'Rejections',
|
||||||
type: filterBuilderTypes.NUMBER
|
type: filterBuilderTypes.NUMBER
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
|
||||||
customFilters: []
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
|
@ -163,9 +157,6 @@ export const CLEAR_RELEASES = 'releases/clearReleases';
|
||||||
export const GRAB_RELEASE = 'releases/grabRelease';
|
export const GRAB_RELEASE = 'releases/grabRelease';
|
||||||
export const UPDATE_RELEASE = 'releases/updateRelease';
|
export const UPDATE_RELEASE = 'releases/updateRelease';
|
||||||
export const SET_RELEASES_FILTER = 'releases/setReleasesFilter';
|
export const SET_RELEASES_FILTER = 'releases/setReleasesFilter';
|
||||||
export const ADD_RELEASES_CUSTOM_FILTER = 'releases/addReleasesCustomFilter';
|
|
||||||
export const REMOVE_RELEASES_CUSTOM_FILTER = 'releases/removeReleasesCustomFilter';
|
|
||||||
export const SAVE_RELEASES_CUSTOM_FILTER = 'releases/saveReleasesCustomFilter';
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -177,9 +168,6 @@ export const clearReleases = createAction(CLEAR_RELEASES);
|
||||||
export const grabRelease = createThunk(GRAB_RELEASE);
|
export const grabRelease = createThunk(GRAB_RELEASE);
|
||||||
export const updateRelease = createAction(UPDATE_RELEASE);
|
export const updateRelease = createAction(UPDATE_RELEASE);
|
||||||
export const setReleasesFilter = createAction(SET_RELEASES_FILTER);
|
export const setReleasesFilter = createAction(SET_RELEASES_FILTER);
|
||||||
export const addReleasesCustomFilter = createAction(ADD_RELEASES_CUSTOM_FILTER);
|
|
||||||
export const removeReleasesCustomFilter = createAction(REMOVE_RELEASES_CUSTOM_FILTER);
|
|
||||||
export const saveReleasesCustomFilter = createAction(SAVE_RELEASES_CUSTOM_FILTER);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Helpers
|
// Helpers
|
||||||
|
@ -266,11 +254,6 @@ export const reducers = createHandleActions({
|
||||||
},
|
},
|
||||||
|
|
||||||
[SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section),
|
[SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||||
[SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section),
|
[SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section)
|
||||||
|
|
||||||
...createCustomFilterReducers(section, {
|
|
||||||
[customFilterHandlers.REMOVE]: REMOVE_RELEASES_CUSTOM_FILTER,
|
|
||||||
[customFilterHandlers.SAVE]: SAVE_RELEASES_CUSTOM_FILTER
|
|
||||||
})
|
|
||||||
|
|
||||||
}, defaultState, section);
|
}, defaultState, section);
|
||||||
|
|
91
frontend/src/Store/Middleware/createSentryMiddleware.js
Normal file
91
frontend/src/Store/Middleware/createSentryMiddleware.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import * as sentry from '@sentry/browser';
|
||||||
|
import parseUrl from 'Utilities/String/parseUrl';
|
||||||
|
|
||||||
|
function cleanseUrl(url) {
|
||||||
|
const properties = parseUrl(url);
|
||||||
|
|
||||||
|
return `${properties.pathname}${properties.search}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanseData(data) {
|
||||||
|
const result = _.cloneDeep(data);
|
||||||
|
|
||||||
|
result.transaction = cleanseUrl(result.transaction);
|
||||||
|
|
||||||
|
if (result.exception) {
|
||||||
|
result.exception.values.forEach((exception) => {
|
||||||
|
const stacktrace = exception.stacktrace;
|
||||||
|
|
||||||
|
if (stacktrace) {
|
||||||
|
stacktrace.frames.forEach((frame) => {
|
||||||
|
frame.filename = cleanseUrl(frame.filename);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.request.url = cleanseUrl(result.request.url);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function identity(stuff) {
|
||||||
|
return stuff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMiddleware() {
|
||||||
|
return (store) => (next) => (action) => {
|
||||||
|
try {
|
||||||
|
// Adds a breadcrumb for reporting later (if necessary).
|
||||||
|
sentry.addBreadcrumb({
|
||||||
|
category: 'redux',
|
||||||
|
message: action.type
|
||||||
|
});
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[sentry] Reporting error to Sentry: ${err}`);
|
||||||
|
|
||||||
|
// Send the report including breadcrumbs.
|
||||||
|
sentry.captureException(err, {
|
||||||
|
extra: {
|
||||||
|
action: identity(action),
|
||||||
|
state: identity(store.getState())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createSentryMiddleware() {
|
||||||
|
const {
|
||||||
|
analytics,
|
||||||
|
branch,
|
||||||
|
version,
|
||||||
|
release,
|
||||||
|
isProduction
|
||||||
|
} = window.Lidarr;
|
||||||
|
|
||||||
|
if (!analytics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dsn = isProduction ? 'https://c3a5b33e08de4e18b7d0505e942dbc95@sentry.io/216290' :
|
||||||
|
'https://baede6f14da54cf48ff431479e400adf@sentry.io/1249427';
|
||||||
|
|
||||||
|
sentry.init({
|
||||||
|
dsn,
|
||||||
|
environment: isProduction ? 'production' : 'development',
|
||||||
|
release,
|
||||||
|
sendDefaultPii: true,
|
||||||
|
beforeSend: cleanseData
|
||||||
|
});
|
||||||
|
|
||||||
|
sentry.configureScope((scope) => {
|
||||||
|
scope.setTag('branch', branch);
|
||||||
|
scope.setTag('version', version);
|
||||||
|
});
|
||||||
|
|
||||||
|
return createMiddleware();
|
||||||
|
}
|
|
@ -1,15 +1,15 @@
|
||||||
import { applyMiddleware, compose } from 'redux';
|
import { applyMiddleware, compose } from 'redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { routerMiddleware } from 'react-router-redux';
|
import { routerMiddleware } from 'react-router-redux';
|
||||||
import sentryMiddleware from './sentryMiddleware';
|
import createSentryMiddleware from './createSentryMiddleware';
|
||||||
import persistState from './persistState';
|
import persistState from './persistState';
|
||||||
|
|
||||||
export default function(history) {
|
export default function(history) {
|
||||||
const middlewares = [];
|
const middlewares = [];
|
||||||
const ravenMiddleware = sentryMiddleware();
|
const sentryMiddleware = createSentryMiddleware();
|
||||||
|
|
||||||
if (ravenMiddleware) {
|
if (sentryMiddleware) {
|
||||||
middlewares.push(ravenMiddleware);
|
middlewares.push(sentryMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
middlewares.push(routerMiddleware(history));
|
middlewares.push(routerMiddleware(history));
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import Raven from 'raven-js';
|
|
||||||
import createRavenMiddleware from 'raven-for-redux';
|
|
||||||
import parseUrl from 'Utilities/String/parseUrl';
|
|
||||||
|
|
||||||
function cleanseUrl(url) {
|
|
||||||
const properties = parseUrl(url);
|
|
||||||
|
|
||||||
return `${properties.pathname}${properties.search}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanseData(data) {
|
|
||||||
const result = _.cloneDeep(data);
|
|
||||||
|
|
||||||
result.culprit = cleanseUrl(result.culprit);
|
|
||||||
result.request.url = cleanseUrl(result.request.url);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function sentryMiddleware() {
|
|
||||||
const {
|
|
||||||
analytics,
|
|
||||||
branch,
|
|
||||||
version,
|
|
||||||
release,
|
|
||||||
isProduction
|
|
||||||
} = window.Lidarr;
|
|
||||||
|
|
||||||
if (!analytics) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dsn = isProduction ? 'https://c3a5b33e08de4e18b7d0505e942dbc95@sentry.io/216290' :
|
|
||||||
'https://baede6f14da54cf48ff431479e400adf@sentry.io/1249427';
|
|
||||||
|
|
||||||
Raven.config(
|
|
||||||
dsn,
|
|
||||||
{
|
|
||||||
environment: isProduction ? 'production' : 'development',
|
|
||||||
release,
|
|
||||||
tags: {
|
|
||||||
branch,
|
|
||||||
version
|
|
||||||
},
|
|
||||||
dataCallback: cleanseData
|
|
||||||
}
|
|
||||||
).install();
|
|
||||||
|
|
||||||
return createRavenMiddleware(Raven);
|
|
||||||
}
|
|
|
@ -94,12 +94,24 @@ function sort(items, state) {
|
||||||
return _.orderBy(items, clauses, orders);
|
return _.orderBy(items, clauses, orders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createCustomFiltersSelector(type, alternateType) {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.customFilters.items,
|
||||||
|
(customFilters) => {
|
||||||
|
return customFilters.filter((customFilter) => {
|
||||||
|
return customFilter.type === type || customFilter.type === alternateType;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function createClientSideCollectionSelector(section, uiSection) {
|
function createClientSideCollectionSelector(section, uiSection) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => _.get(state, section),
|
(state) => _.get(state, section),
|
||||||
(state) => _.get(state, uiSection),
|
(state) => _.get(state, uiSection),
|
||||||
(sectionState, uiSectionState = {}) => {
|
createCustomFiltersSelector(section, uiSection),
|
||||||
const state = Object.assign({}, sectionState, uiSectionState);
|
(sectionState, uiSectionState = {}, customFilters) => {
|
||||||
|
const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
|
||||||
|
|
||||||
const filtered = filter(state.items, state);
|
const filtered = filter(state.items, state);
|
||||||
const sorted = sort(filtered, state);
|
const sorted = sort(filtered, state);
|
||||||
|
@ -107,6 +119,7 @@ function createClientSideCollectionSelector(section, uiSection) {
|
||||||
return {
|
return {
|
||||||
...sectionState,
|
...sectionState,
|
||||||
...uiSectionState,
|
...uiSectionState,
|
||||||
|
customFilters,
|
||||||
items: sorted,
|
items: sorted,
|
||||||
totalItems: state.items.length
|
totalItems: state.items.length
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import titleCase from 'Utilities/String/titleCase';
|
||||||
import FieldSet from 'Components/FieldSet';
|
import FieldSet from 'Components/FieldSet';
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import StartTime from './StartTime';
|
||||||
import styles from './About.css';
|
import styles from './About.css';
|
||||||
|
|
||||||
class About extends Component {
|
class About extends Component {
|
||||||
|
@ -19,7 +20,10 @@ class About extends Component {
|
||||||
migrationVersion,
|
migrationVersion,
|
||||||
appData,
|
appData,
|
||||||
startupPath,
|
startupPath,
|
||||||
mode
|
mode,
|
||||||
|
startTime,
|
||||||
|
timeFormat,
|
||||||
|
longDateFormat
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -57,6 +61,17 @@ class About extends Component {
|
||||||
title="Mode"
|
title="Mode"
|
||||||
data={titleCase(mode)}
|
data={titleCase(mode)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Uptime"
|
||||||
|
data={
|
||||||
|
<StartTime
|
||||||
|
startTime={startTime}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
longDateFormat={longDateFormat}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
);
|
);
|
||||||
|
@ -65,13 +80,16 @@ class About extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
About.propTypes = {
|
About.propTypes = {
|
||||||
version: PropTypes.string,
|
version: PropTypes.string.isRequired,
|
||||||
isMonoRuntime: PropTypes.bool,
|
isMonoRuntime: PropTypes.bool.isRequired,
|
||||||
runtimeVersion: PropTypes.string,
|
runtimeVersion: PropTypes.string.isRequired,
|
||||||
migrationVersion: PropTypes.number,
|
migrationVersion: PropTypes.number.isRequired,
|
||||||
appData: PropTypes.string,
|
appData: PropTypes.string.isRequired,
|
||||||
startupPath: PropTypes.string,
|
startupPath: PropTypes.string.isRequired,
|
||||||
mode: PropTypes.string
|
mode: PropTypes.string.isRequired,
|
||||||
|
startTime: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default About;
|
export default About;
|
||||||
|
|
|
@ -3,14 +3,18 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import About from './About';
|
import About from './About';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.system.status,
|
(state) => state.system.status,
|
||||||
(status) => {
|
createUISettingsSelector(),
|
||||||
|
(status, uiSettings) => {
|
||||||
return {
|
return {
|
||||||
...status.item
|
...status.item,
|
||||||
|
timeFormat: uiSettings.timeFormat,
|
||||||
|
longDateFormat: uiSettings.longDateFormat
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
93
frontend/src/System/Status/About/StartTime.js
Normal file
93
frontend/src/System/Status/About/StartTime.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
|
|
||||||
|
function getUptime(startTime) {
|
||||||
|
return formatTimeSpan(moment().diff(startTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
class StartTime extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const {
|
||||||
|
startTime,
|
||||||
|
timeFormat,
|
||||||
|
longDateFormat
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
this._timeoutId = null;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
uptime: getUptime(startTime),
|
||||||
|
startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._timeoutId = setTimeout(this.onTimeout, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
startTime,
|
||||||
|
timeFormat,
|
||||||
|
longDateFormat
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
startTime !== prevProps.startTime ||
|
||||||
|
timeFormat !== prevProps.timeFormat ||
|
||||||
|
longDateFormat !== prevProps.longDateFormat
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
uptime: getUptime(startTime),
|
||||||
|
startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this._timeoutId) {
|
||||||
|
this._timeoutId = clearTimeout(this._timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onTimeout = () => {
|
||||||
|
this.setState({ uptime: getUptime(this.props.startTime) });
|
||||||
|
this._timeoutId = setTimeout(this.onTimeout, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
uptime,
|
||||||
|
startTime
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span title={startTime}>
|
||||||
|
{uptime}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StartTime.propTypes = {
|
||||||
|
startTime: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StartTime;
|
|
@ -70,9 +70,15 @@ class TrackFileEditorModalContent extends Component {
|
||||||
getSelectedIds = () => {
|
getSelectedIds = () => {
|
||||||
const selectedIds = getSelectedIds(this.state.selectedState);
|
const selectedIds = getSelectedIds(this.state.selectedState);
|
||||||
|
|
||||||
return _.uniq(_.map(selectedIds, (id) => {
|
return selectedIds.reduce((acc, id) => {
|
||||||
return _.find(this.props.items, { id }).trackFileId;
|
const matchingItem = this.props.items.find((item) => item.id === id);
|
||||||
}));
|
|
||||||
|
if (matchingItem && !acc.includes(matchingItem.trackFileID)) {
|
||||||
|
acc.push(matchingItem.trackFileID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Label from 'Components/Label';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import styles from './TrackFileEditorRow';
|
import styles from './TrackFileEditorRow';
|
||||||
|
|
||||||
function TrackFileEditorRow(props) {
|
function TrackFileEditorRow(props) {
|
||||||
|
@ -42,7 +42,7 @@ function TrackFileEditorRow(props) {
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
<EpisodeQuality
|
<TrackQuality
|
||||||
quality={quality}
|
quality={quality}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
|
@ -3,7 +3,11 @@ export default function findSelectedFilters(selectedFilterKey, filters = [], cus
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedFilter = [...filters, ...customFilters].find((f) => f.key === selectedFilterKey);
|
let selectedFilter = filters.find((f) => f.key === selectedFilterKey);
|
||||||
|
|
||||||
|
if (!selectedFilter) {
|
||||||
|
selectedFilter = customFilters.find((f) => f.id === selectedFilterKey);
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedFilter) {
|
if (!selectedFilter) {
|
||||||
// TODO: throw in dev
|
// TODO: throw in dev
|
||||||
|
|
15
frontend/src/Utilities/Number/convertToBytes.js
Normal file
15
frontend/src/Utilities/Number/convertToBytes.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
function convertToBytes(input, power, binaryPrefix) {
|
||||||
|
const size = Number(input);
|
||||||
|
|
||||||
|
if (isNaN(size)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = binaryPrefix ? 1024 : 1000;
|
||||||
|
const multiplier = Math.pow(prefix, power);
|
||||||
|
const result = size * multiplier;
|
||||||
|
|
||||||
|
return Math.round(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default convertToBytes;
|
|
@ -2,9 +2,12 @@ import _ from 'lodash';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
|
||||||
function getProviderState(payload, getState, section) {
|
function getProviderState(payload, getState, section) {
|
||||||
const id = payload.id;
|
const {
|
||||||
|
id,
|
||||||
|
...otherPayload
|
||||||
|
} = payload;
|
||||||
const state = getSectionState(getState(), section, true);
|
const state = getSectionState(getState(), section, true);
|
||||||
const pendingChanges = Object.assign({}, state.pendingChanges);
|
const pendingChanges = Object.assign({}, state.pendingChanges, otherPayload);
|
||||||
const pendingFields = state.pendingChanges.fields || {};
|
const pendingFields = state.pendingChanges.fields || {};
|
||||||
delete pendingChanges.fields;
|
delete pendingChanges.fields;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
const customFilterHandlers = {
|
|
||||||
REMOVE: 'remove',
|
|
||||||
SAVE: 'save'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default customFilterHandlers;
|
|
|
@ -26,6 +26,10 @@ function MissingRow(props) {
|
||||||
onSelectedChange
|
onSelectedChange
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
if (!artist) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableSelectCell
|
<TableSelectCell
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"@fortawesome/free-regular-svg-icons": "5.3.1",
|
"@fortawesome/free-regular-svg-icons": "5.3.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.3.1",
|
"@fortawesome/free-solid-svg-icons": "5.3.1",
|
||||||
"@fortawesome/react-fontawesome": "0.1.3",
|
"@fortawesome/react-fontawesome": "0.1.3",
|
||||||
|
"@sentry/browser": "4.0.4",
|
||||||
"autoprefixer": "9.1.5",
|
"autoprefixer": "9.1.5",
|
||||||
"babel-core": "6.26.3",
|
"babel-core": "6.26.3",
|
||||||
"babel-eslint": "9.0.0",
|
"babel-eslint": "9.0.0",
|
||||||
|
@ -73,17 +74,15 @@
|
||||||
"postcss-simple-vars": "5.0.1",
|
"postcss-simple-vars": "5.0.1",
|
||||||
"prop-types": "15.6.2",
|
"prop-types": "15.6.2",
|
||||||
"qs": "6.5.2",
|
"qs": "6.5.2",
|
||||||
"raven-for-redux": "1.3.1",
|
"react": "16.5.2",
|
||||||
"raven-js": "3.27.0",
|
|
||||||
"react": "16.5.1",
|
|
||||||
"react-addons-shallow-compare": "15.6.2",
|
"react-addons-shallow-compare": "15.6.2",
|
||||||
"react-async-script": "1.0.0",
|
"react-async-script": "1.0.0",
|
||||||
"react-autosuggest": "9.4.1",
|
"react-autosuggest": "9.4.2",
|
||||||
"react-custom-scrollbars": "4.2.1",
|
"react-custom-scrollbars": "4.2.1",
|
||||||
"react-dnd": "5.0.0",
|
"react-dnd": "5.0.0",
|
||||||
"react-dnd-html5-backend": "5.0.1",
|
"react-dnd-html5-backend": "5.0.1",
|
||||||
"react-document-title": "2.0.3",
|
"react-document-title": "2.0.3",
|
||||||
"react-dom": "16.5.1",
|
"react-dom": "16.5.2",
|
||||||
"react-google-recaptcha": "1.0.2",
|
"react-google-recaptcha": "1.0.2",
|
||||||
"react-lazyload": "2.3.0",
|
"react-lazyload": "2.3.0",
|
||||||
"react-measure": "1.4.7",
|
"react-measure": "1.4.7",
|
||||||
|
|
49
src/Lidarr.Api.V1/CustomFilters/CustomFilterModule.cs
Normal file
49
src/Lidarr.Api.V1/CustomFilters/CustomFilterModule.cs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.CustomFilters;
|
||||||
|
using Lidarr.Http;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.CustomFilters
|
||||||
|
{
|
||||||
|
public class CustomFilterModule : LidarrRestModule<CustomFilterResource>
|
||||||
|
{
|
||||||
|
private readonly ICustomFilterService _customFilterService;
|
||||||
|
|
||||||
|
public CustomFilterModule(ICustomFilterService customFilterService)
|
||||||
|
{
|
||||||
|
_customFilterService = customFilterService;
|
||||||
|
|
||||||
|
GetResourceById = GetCustomFilter;
|
||||||
|
GetResourceAll = GetCustomFilters;
|
||||||
|
CreateResource = AddCustomFilter;
|
||||||
|
UpdateResource = UpdateCustomFilter;
|
||||||
|
DeleteResource = DeleteCustomResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CustomFilterResource GetCustomFilter(int id)
|
||||||
|
{
|
||||||
|
return _customFilterService.Get(id).ToResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CustomFilterResource> GetCustomFilters()
|
||||||
|
{
|
||||||
|
return _customFilterService.All().ToResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int AddCustomFilter(CustomFilterResource resource)
|
||||||
|
{
|
||||||
|
var customFilter = _customFilterService.Add(resource.ToModel());
|
||||||
|
|
||||||
|
return customFilter.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCustomFilter(CustomFilterResource resource)
|
||||||
|
{
|
||||||
|
_customFilterService.Update(resource.ToModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteCustomResource(int id)
|
||||||
|
{
|
||||||
|
_customFilterService.Delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
src/Lidarr.Api.V1/CustomFilters/CustomFilterResource.cs
Normal file
49
src/Lidarr.Api.V1/CustomFilters/CustomFilterResource.cs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.CustomFilters;
|
||||||
|
using Lidarr.Http.REST;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.CustomFilters
|
||||||
|
{
|
||||||
|
public class CustomFilterResource : RestResource
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Label { get; set; }
|
||||||
|
public List<dynamic> Filters { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CustomFilterResourceMapper
|
||||||
|
{
|
||||||
|
public static CustomFilterResource ToResource(this CustomFilter model)
|
||||||
|
{
|
||||||
|
if (model == null) return null;
|
||||||
|
|
||||||
|
return new CustomFilterResource
|
||||||
|
{
|
||||||
|
Id = model.Id,
|
||||||
|
Type = model.Type,
|
||||||
|
Label = model.Label,
|
||||||
|
Filters = Json.Deserialize<List<dynamic>>(model.Filters)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CustomFilter ToModel(this CustomFilterResource resource)
|
||||||
|
{
|
||||||
|
if (resource == null) return null;
|
||||||
|
|
||||||
|
return new CustomFilter
|
||||||
|
{
|
||||||
|
Id = resource.Id,
|
||||||
|
Type = resource.Type,
|
||||||
|
Label = resource.Label,
|
||||||
|
Filters = Json.ToJson(resource.Filters)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<CustomFilterResource> ToResource(this IEnumerable<CustomFilter> filters)
|
||||||
|
{
|
||||||
|
return filters.Select(ToResource).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -97,6 +97,8 @@
|
||||||
<Compile Include="Commands\CommandResource.cs" />
|
<Compile Include="Commands\CommandResource.cs" />
|
||||||
<Compile Include="Config\MetadataProviderConfigModule.cs" />
|
<Compile Include="Config\MetadataProviderConfigModule.cs" />
|
||||||
<Compile Include="Config\MetadataProviderConfigResource.cs" />
|
<Compile Include="Config\MetadataProviderConfigResource.cs" />
|
||||||
|
<Compile Include="CustomFilters\CustomFilterModule.cs" />
|
||||||
|
<Compile Include="CustomFilters\CustomFilterResource.cs" />
|
||||||
<Compile Include="ImportLists\ImportListModule.cs" />
|
<Compile Include="ImportLists\ImportListModule.cs" />
|
||||||
<Compile Include="ImportLists\ImportListResource.cs" />
|
<Compile Include="ImportLists\ImportListResource.cs" />
|
||||||
<Compile Include="Profiles\Metadata\MetadataProfileModule.cs" />
|
<Compile Include="Profiles\Metadata\MetadataProfileModule.cs" />
|
||||||
|
|
|
@ -54,8 +54,8 @@
|
||||||
<HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\Org.Mentalis.dll</HintPath>
|
<HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\Org.Mentalis.dll</HintPath>
|
||||||
<Private>True</Private>
|
<Private>True</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="SharpRaven, Version=2.2.0.0, Culture=neutral, processorArchitecture=MSIL">
|
<Reference Include="SharpRaven, Version=2.4.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
<HintPath>..\packages\SharpRaven.2.2.0\lib\net45\SharpRaven.dll</HintPath>
|
<HintPath>..\packages\SharpRaven.2.4.0\lib\net45\SharpRaven.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="SocksWebProxy, Version=1.3.4.0, Culture=neutral, processorArchitecture=MSIL">
|
<Reference Include="SocksWebProxy, Version=1.3.4.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
<HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\SocksWebProxy.dll</HintPath>
|
<HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\SocksWebProxy.dll</HintPath>
|
||||||
|
@ -66,6 +66,7 @@
|
||||||
<Reference Include="System.Configuration.Install" />
|
<Reference Include="System.Configuration.Install" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
<Reference Include="System.ServiceProcess" />
|
<Reference Include="System.ServiceProcess" />
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="Microsoft.CSharp" />
|
||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
|
|
|
@ -4,5 +4,5 @@
|
||||||
<package id="ICSharpCode.SharpZipLib.Patched" version="0.86.5" targetFramework="net461" />
|
<package id="ICSharpCode.SharpZipLib.Patched" version="0.86.5" targetFramework="net461" />
|
||||||
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net461" />
|
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net461" />
|
||||||
<package id="NLog" version="4.5.4" targetFramework="net461" />
|
<package id="NLog" version="4.5.4" targetFramework="net461" />
|
||||||
<package id="SharpRaven" version="2.2.0" targetFramework="net461" />
|
<package id="SharpRaven" version="2.4.0" targetFramework="net461" />
|
||||||
</packages>
|
</packages>
|
11
src/NzbDrone.Core/CustomFilters/CustomFilter.cs
Normal file
11
src/NzbDrone.Core/CustomFilters/CustomFilter.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.CustomFilters
|
||||||
|
{
|
||||||
|
public class CustomFilter : ModelBase
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Label { get; set; }
|
||||||
|
public string Filters { get; set; }
|
||||||
|
}
|
||||||
|
}
|
17
src/NzbDrone.Core/CustomFilters/CustomFilterRepository.cs
Normal file
17
src/NzbDrone.Core/CustomFilters/CustomFilterRepository.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.CustomFilters
|
||||||
|
{
|
||||||
|
public interface ICustomFilterRepository : IBasicRepository<CustomFilter>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomFilterRepository : BasicRepository<CustomFilter>, ICustomFilterRepository
|
||||||
|
{
|
||||||
|
public CustomFilterRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||||
|
: base(database, eventAggregator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue