mirror of https://github.com/lidarr/Lidarr
New: Custom Filtering for UI (#234)
This commit is contained in:
parent
c6873014c7
commit
7354e02bff
|
@ -12,8 +12,6 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
|
||||||
import FilterMenuItem from 'Components/Menu/FilterMenuItem';
|
|
||||||
import HistoryRowConnector from './HistoryRowConnector';
|
import HistoryRowConnector from './HistoryRowConnector';
|
||||||
|
|
||||||
class History extends Component {
|
class History extends Component {
|
||||||
|
@ -49,8 +47,8 @@ class History extends Component {
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
filterKey,
|
selectedFilterKey,
|
||||||
filterValue,
|
filters,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isAlbumsFetching,
|
isAlbumsFetching,
|
||||||
isAlbumsPopulated,
|
isAlbumsPopulated,
|
||||||
|
@ -77,67 +75,13 @@ class History extends Component {
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
<FilterMenu alignMenu={align.RIGHT}>
|
<FilterMenu
|
||||||
<MenuContent>
|
alignMenu={align.RIGHT}
|
||||||
<FilterMenuItem
|
selectedFilterKey={selectedFilterKey}
|
||||||
filterKey={filterKey}
|
filters={filters}
|
||||||
filterValue={filterValue}
|
customFilters={[]}
|
||||||
onPress={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
>
|
/>
|
||||||
All
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="eventType"
|
|
||||||
value="1"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Grabbed
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="eventType"
|
|
||||||
value="3"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Imported
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="eventType"
|
|
||||||
value="4"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Failed
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="eventType"
|
|
||||||
value="5"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Deleted
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="eventType"
|
|
||||||
value="6"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Renamed
|
|
||||||
</FilterMenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</FilterMenu>
|
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
|
@ -204,8 +148,8 @@ History.propTypes = {
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterKey: PropTypes.string,
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
filterValue: PropTypes.string,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
isAlbumsFetching: PropTypes.bool.isRequired,
|
isAlbumsFetching: PropTypes.bool.isRequired,
|
||||||
isAlbumsPopulated: PropTypes.bool.isRequired,
|
isAlbumsPopulated: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -105,8 +105,8 @@ class HistoryConnector extends Component {
|
||||||
this.props.setHistorySort({ sortKey });
|
this.props.setHistorySort({ sortKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterSelect = (filterKey, filterValue) => {
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
this.props.setHistoryFilter({ filterKey, filterValue });
|
this.props.setHistoryFilter({ selectedFilterKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
onTableOptionChange = (payload) => {
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $menuItemHoverColor;
|
background-color: $menuItemHoverBackgroundColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
.filterMenuContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { icons, sortDirections } from 'Helpers/Props';
|
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
@ -10,7 +12,9 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
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 InteractiveAlbumSearchRow from './InteractiveAlbumSearchRow';
|
import InteractiveAlbumSearchRow from './InteractiveAlbumSearchRow';
|
||||||
|
import styles from './InteractiveAlbumSearchModalContent.css';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
|
@ -81,12 +85,17 @@ class InteractiveAlbumSearchModalContent extends Component {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
|
totalReleasesCount,
|
||||||
items,
|
items,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
longDateFormat,
|
longDateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
onSortPress,
|
onSortPress,
|
||||||
|
onFilterSelect,
|
||||||
onGrabPress,
|
onGrabPress,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -117,28 +126,59 @@ class InteractiveAlbumSearchModalContent extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && hasItems && !error &&
|
isPopulated && hasItems && !error &&
|
||||||
<Table
|
<div>
|
||||||
columns={columns}
|
<div className={styles.filterMenuContainer}>
|
||||||
sortKey={sortKey}
|
<FilterMenu
|
||||||
sortDirection={sortDirection}
|
alignMenu={align.RIGHT}
|
||||||
onSortPress={onSortPress}
|
selectedFilterKey={selectedFilterKey}
|
||||||
>
|
filters={filters}
|
||||||
<TableBody>
|
customFilters={customFilters}
|
||||||
{
|
onFilterSelect={onFilterSelect}
|
||||||
items.map((item) => {
|
buttonComponent={PageMenuButton}
|
||||||
return (
|
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
|
||||||
<InteractiveAlbumSearchRow
|
/>
|
||||||
key={item.guid}
|
</div>
|
||||||
{...item}
|
|
||||||
longDateFormat={longDateFormat}
|
{
|
||||||
timeFormat={timeFormat}
|
!!totalReleasesCount && !items.length &&
|
||||||
onGrabPress={onGrabPress}
|
<div>
|
||||||
/>
|
All results are hidden by {filters.length > 1 ? 'filters' : 'a filter'}.
|
||||||
);
|
</div>
|
||||||
})
|
}
|
||||||
}
|
|
||||||
</TableBody>
|
{
|
||||||
</Table>
|
!!items.length &&
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<InteractiveAlbumSearchRow
|
||||||
|
key={item.guid}
|
||||||
|
{...item}
|
||||||
|
longDateFormat={longDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
onGrabPress={onGrabPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
totalReleasesCount !== items.length && !!items.length &&
|
||||||
|
<div>
|
||||||
|
Some results are hidden by {filters.length > 1 ? 'filters' : 'a filter'}.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
|
@ -156,12 +196,17 @@ InteractiveAlbumSearchModalContent.propTypes = {
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
|
totalReleasesCount: PropTypes.number.isRequired,
|
||||||
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,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.string,
|
sortDirection: PropTypes.string,
|
||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
onGrabPress: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import connectSection from 'Store/connectSection';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchReleases, clearReleases, cancelFetchReleases, setReleasesSort, grabRelease } from 'Store/Actions/releaseActions';
|
import connectSection from 'Store/connectSection';
|
||||||
|
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 InteractiveAlbumSearchModalContent from './InteractiveAlbumSearchModalContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
(state) => state.releases.items.length,
|
||||||
createClientSideCollectionSelector(),
|
createClientSideCollectionSelector(),
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
(releases, uiSettings) => {
|
(totalReleasesCount, releases, uiSettings) => {
|
||||||
return {
|
return {
|
||||||
|
totalReleasesCount,
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
longDateFormat: uiSettings.longDateFormat,
|
||||||
timeFormat: uiSettings.timeFormat,
|
timeFormat: uiSettings.timeFormat,
|
||||||
...releases
|
...releases
|
||||||
|
@ -25,23 +26,27 @@ function createMapStateToProps() {
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
return {
|
return {
|
||||||
dispatchFetchReleases({ albumId }) {
|
dispatchFetchReleases({ albumId }) {
|
||||||
dispatch(fetchReleases({ albumId }));
|
dispatch(releaseActions.fetchReleases({ albumId }));
|
||||||
},
|
},
|
||||||
|
|
||||||
dispatchCancelFetchReleases() {
|
dispatchCancelFetchReleases() {
|
||||||
dispatch(cancelFetchReleases());
|
dispatch(releaseActions.cancelFetchReleases());
|
||||||
},
|
},
|
||||||
|
|
||||||
dispatchClearReleases() {
|
dispatchClearReleases() {
|
||||||
dispatch(clearReleases());
|
dispatch(releaseActions.clearReleases());
|
||||||
},
|
},
|
||||||
|
|
||||||
dispatchSetReleasesSort({ sortKey, sortDirection }) {
|
onSortPress(sortKey, sortDirection) {
|
||||||
dispatch(setReleasesSort({ sortKey, sortDirection }));
|
dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection }));
|
||||||
},
|
},
|
||||||
|
|
||||||
dispatchGrabRelease({ guid, indexerId }) {
|
onFilterSelect(selectedFilterKey) {
|
||||||
dispatch(grabRelease({ guid, indexerId }));
|
dispatch(releaseActions.setReleasesFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
|
||||||
|
onGrabPress({ guid, indexerId }) {
|
||||||
|
dispatch(releaseActions.grabRelease({ guid, indexerId }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -66,26 +71,18 @@ class InteractiveAlbumSearchModalContentConnector extends Component {
|
||||||
this.props.dispatchClearReleases();
|
this.props.dispatchClearReleases();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSortPress = (sortKey, sortDirection) => {
|
|
||||||
this.props.dispatchSetReleasesSort({ sortKey, sortDirection });
|
|
||||||
}
|
|
||||||
|
|
||||||
onGrabPress = (guid, indexerId) => {
|
|
||||||
this.props.dispatchGrabRelease({ guid, indexerId });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
dispatchFetchReleases,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InteractiveAlbumSearchModalContent
|
<InteractiveAlbumSearchModalContent
|
||||||
{...this.props}
|
{...otherProps}
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onGrabPress={this.onGrabPress}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -95,9 +92,7 @@ InteractiveAlbumSearchModalContentConnector.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
|
||||||
dispatchSetReleasesSort: PropTypes.func.isRequired,
|
|
||||||
dispatchGrabRelease: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connectSection(
|
export default connectSection(
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.releases.items,
|
||||||
|
(state) => state.releases.filterBuilderProps,
|
||||||
|
(sectionItems, filterBuilderProps) => {
|
||||||
|
return {
|
||||||
|
sectionItems,
|
||||||
|
filterBuilderProps
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onRemoveCustomFilterPress(index) {
|
||||||
|
dispatch(releaseActions.removeReleasesCustomFilter({ index }));
|
||||||
|
},
|
||||||
|
|
||||||
|
onSaveCustomFilterPress(payload) {
|
||||||
|
dispatch(releaseActions.saveReleasesCustomFilter(payload));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
|
@ -10,8 +10,6 @@ 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';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
|
||||||
import FilterMenuItem from 'Components/Menu/FilterMenuItem';
|
|
||||||
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';
|
||||||
|
@ -104,8 +102,9 @@ class AlbumStudio extends Component {
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
filterKey,
|
selectedFilterKey,
|
||||||
filterValue,
|
filters,
|
||||||
|
customFilters,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
@ -125,57 +124,13 @@ class AlbumStudio extends Component {
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection />
|
<PageToolbarSection />
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
<FilterMenu alignMenu={align.RIGHT}>
|
<FilterMenu
|
||||||
<MenuContent>
|
alignMenu={align.RIGHT}
|
||||||
<FilterMenuItem
|
selectedFilterKey={selectedFilterKey}
|
||||||
filterKey={filterKey}
|
filters={filters}
|
||||||
filterValue={filterValue}
|
customFilters={customFilters}
|
||||||
onPress={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
>
|
/>
|
||||||
All
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="monitored"
|
|
||||||
value={true}
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Monitored Only
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="status"
|
|
||||||
value="continuing"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Continuing Only
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="status"
|
|
||||||
value="ended"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Ended Only
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="missing"
|
|
||||||
value={true}
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Missing Albums
|
|
||||||
</FilterMenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</FilterMenu>
|
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
|
@ -245,8 +200,9 @@ 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),
|
||||||
filterKey: PropTypes.string,
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -57,8 +57,8 @@ class AlbumStudioConnector extends Component {
|
||||||
this.props.setAlbumStudioSort({ sortKey });
|
this.props.setAlbumStudioSort({ sortKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterSelect = (filterKey, filterValue, filterType) => {
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
this.props.setAlbumStudioFilter({ filterKey, filterValue, filterType });
|
this.props.setAlbumStudioFilter({ selectedFilterKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdateSelectedPress = (payload) => {
|
onUpdateSelectedPress = (payload) => {
|
||||||
|
|
|
@ -47,11 +47,15 @@ class DeleteArtistModalContent extends Component {
|
||||||
const {
|
const {
|
||||||
artistName,
|
artistName,
|
||||||
path,
|
path,
|
||||||
trackFileCount,
|
statistics,
|
||||||
sizeOnDisk,
|
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
trackFileCount,
|
||||||
|
sizeOnDisk
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const deleteFiles = this.state.deleteFiles;
|
const deleteFiles = this.state.deleteFiles;
|
||||||
let deleteFilesLabel = `Delete ${trackFileCount} Track Files`;
|
let deleteFilesLabel = `Delete ${trackFileCount} Track Files`;
|
||||||
let deleteFilesHelpText = 'Delete the track files and artist folder';
|
let deleteFilesHelpText = 'Delete the track files and artist folder';
|
||||||
|
@ -126,8 +130,7 @@ class DeleteArtistModalContent extends Component {
|
||||||
DeleteArtistModalContent.propTypes = {
|
DeleteArtistModalContent.propTypes = {
|
||||||
artistName: PropTypes.string.isRequired,
|
artistName: PropTypes.string.isRequired,
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
trackFileCount: PropTypes.number.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
sizeOnDisk: PropTypes.number,
|
|
||||||
onDeletePress: PropTypes.func.isRequired,
|
onDeletePress: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -161,8 +161,7 @@ class ArtistDetails extends Component {
|
||||||
artistName,
|
artistName,
|
||||||
ratings,
|
ratings,
|
||||||
path,
|
path,
|
||||||
sizeOnDisk,
|
statistics,
|
||||||
trackFileCount,
|
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
monitored,
|
monitored,
|
||||||
albumTypes,
|
albumTypes,
|
||||||
|
@ -178,12 +177,18 @@ class ArtistDetails extends Component {
|
||||||
isPopulated,
|
isPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
trackFilesError,
|
trackFilesError,
|
||||||
|
hasMonitoredAlbums,
|
||||||
previousArtist,
|
previousArtist,
|
||||||
nextArtist,
|
nextArtist,
|
||||||
onRefreshPress,
|
onRefreshPress,
|
||||||
onSearchPress
|
onSearchPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
trackFileCount,
|
||||||
|
sizeOnDisk
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOrganizeModalOpen,
|
isOrganizeModalOpen,
|
||||||
isManageTracksOpen,
|
isManageTracksOpen,
|
||||||
|
@ -230,7 +235,9 @@ class ArtistDetails extends Component {
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Search Monitored"
|
label="Search Monitored"
|
||||||
iconName={icons.SEARCH}
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!monitored || !hasMonitoredAlbums}
|
||||||
isSpinning={isSearching}
|
isSpinning={isSearching}
|
||||||
|
title={hasMonitoredAlbums ? undefined : 'No monitored albums for this artist'}
|
||||||
onPress={onSearchPress}
|
onPress={onSearchPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -583,8 +590,7 @@ ArtistDetails.propTypes = {
|
||||||
artistName: PropTypes.string.isRequired,
|
artistName: PropTypes.string.isRequired,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
sizeOnDisk: PropTypes.number,
|
statistics: PropTypes.object.isRequired,
|
||||||
trackFileCount: PropTypes.number,
|
|
||||||
qualityProfileId: PropTypes.number.isRequired,
|
qualityProfileId: PropTypes.number.isRequired,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
albumTypes: PropTypes.arrayOf(PropTypes.string),
|
albumTypes: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
@ -600,6 +606,7 @@ ArtistDetails.propTypes = {
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
albumsError: PropTypes.object,
|
albumsError: PropTypes.object,
|
||||||
trackFilesError: PropTypes.object,
|
trackFilesError: PropTypes.object,
|
||||||
|
hasMonitoredAlbums: PropTypes.bool.isRequired,
|
||||||
previousArtist: PropTypes.object.isRequired,
|
previousArtist: PropTypes.object.isRequired,
|
||||||
nextArtist: PropTypes.object.isRequired,
|
nextArtist: PropTypes.object.isRequired,
|
||||||
onRefreshPress: PropTypes.func.isRequired,
|
onRefreshPress: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -64,6 +64,8 @@ function createMapStateToProps() {
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const hasMonitoredAlbums = albums.items.some((e) => e.monitored);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...artist,
|
...artist,
|
||||||
albumTypes: sortedAlbumTypes,
|
albumTypes: sortedAlbumTypes,
|
||||||
|
@ -78,6 +80,7 @@ function createMapStateToProps() {
|
||||||
isPopulated,
|
isPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
trackFilesError,
|
trackFilesError,
|
||||||
|
hasMonitoredAlbums,
|
||||||
previousArtist,
|
previousArtist,
|
||||||
nextArtist
|
nextArtist
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,8 +10,6 @@ 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';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
|
||||||
import FilterMenuItem from 'Components/Menu/FilterMenuItem';
|
|
||||||
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';
|
||||||
|
@ -151,8 +149,9 @@ class ArtistEditor extends Component {
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
filterKey,
|
selectedFilterKey,
|
||||||
filterValue,
|
filters,
|
||||||
|
customFilters,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
@ -180,57 +179,13 @@ class ArtistEditor extends Component {
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection />
|
<PageToolbarSection />
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
<FilterMenu alignMenu={align.RIGHT}>
|
<FilterMenu
|
||||||
<MenuContent>
|
alignMenu={align.RIGHT}
|
||||||
<FilterMenuItem
|
selectedFilterKey={selectedFilterKey}
|
||||||
filterKey={filterKey}
|
filters={filters}
|
||||||
filterValue={filterValue}
|
customFilters={customFilters}
|
||||||
onPress={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
>
|
/>
|
||||||
All
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="monitored"
|
|
||||||
value={true}
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Monitored Only
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="status"
|
|
||||||
value="continuing"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Continuing Only
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="status"
|
|
||||||
value="ended"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Ended Only
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="missing"
|
|
||||||
value={true}
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Missing Albums
|
|
||||||
</FilterMenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</FilterMenu>
|
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
|
@ -314,8 +269,9 @@ 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),
|
||||||
filterKey: PropTypes.string,
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -51,8 +51,8 @@ class ArtistEditorConnector extends Component {
|
||||||
this.props.dispatchSetArtistEditorSort({ sortKey });
|
this.props.dispatchSetArtistEditorSort({ sortKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterSelect = (filterKey, filterValue, filterType) => {
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
this.props.dispatchSetArtistEditorFilter({ filterKey, filterValue, filterType });
|
this.props.dispatchSetArtistEditorFilter({ selectedFilterKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
onSaveSelected = (payload) => {
|
onSaveSelected = (payload) => {
|
||||||
|
|
|
@ -49,11 +49,10 @@ class ArtistIndex extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this._viewComponent = null;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
contentBody: null,
|
contentBody: null,
|
||||||
jumpBarItems: [],
|
jumpBarItems: [],
|
||||||
|
jumpToCharacter: null,
|
||||||
isPosterOptionsModalOpen: false,
|
isPosterOptionsModalOpen: false,
|
||||||
isBannerOptionsModalOpen: false,
|
isBannerOptionsModalOpen: false,
|
||||||
isOverviewOptionsModalOpen: false,
|
isOverviewOptionsModalOpen: false,
|
||||||
|
@ -69,7 +68,8 @@ class ArtistIndex extends Component {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection
|
sortDirection,
|
||||||
|
scrollTop
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -79,6 +79,10 @@ class ArtistIndex extends Component {
|
||||||
) {
|
) {
|
||||||
this.setJumpBarItems();
|
this.setJumpBarItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.jumpToCharacter != null && scrollTop !== prevProps.scrollTop) {
|
||||||
|
this.setState({ jumpToCharacter: null });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -88,10 +92,6 @@ class ArtistIndex extends Component {
|
||||||
this.setState({ contentBody: ref });
|
this.setState({ contentBody: ref });
|
||||||
}
|
}
|
||||||
|
|
||||||
setViewComponentRef = (ref) => {
|
|
||||||
this._viewComponent = ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
setJumpBarItems() {
|
setJumpBarItems() {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
|
@ -152,9 +152,8 @@ class ArtistIndex extends Component {
|
||||||
this.setState({ isOverviewOptionsModalOpen: false });
|
this.setState({ isOverviewOptionsModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onJumpBarItemPress = (item) => {
|
onJumpBarItemPress = (jumpToCharacter) => {
|
||||||
const viewComponent = this._viewComponent.getWrappedInstance();
|
this.setState({ jumpToCharacter });
|
||||||
viewComponent.scrollToFirstCharacter(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRender = () => {
|
onRender = () => {
|
||||||
|
@ -187,8 +186,9 @@ class ArtistIndex extends Component {
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
filterKey,
|
selectedFilterKey,
|
||||||
filterValue,
|
filters,
|
||||||
|
customFilters,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
view,
|
view,
|
||||||
|
@ -206,6 +206,7 @@ class ArtistIndex extends Component {
|
||||||
const {
|
const {
|
||||||
contentBody,
|
contentBody,
|
||||||
jumpBarItems,
|
jumpBarItems,
|
||||||
|
jumpToCharacter,
|
||||||
isPosterOptionsModalOpen,
|
isPosterOptionsModalOpen,
|
||||||
isBannerOptionsModalOpen,
|
isBannerOptionsModalOpen,
|
||||||
isOverviewOptionsModalOpen,
|
isOverviewOptionsModalOpen,
|
||||||
|
@ -294,8 +295,9 @@ class ArtistIndex extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ArtistIndexFilterMenu
|
<ArtistIndexFilterMenu
|
||||||
filterKey={filterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filterValue={filterValue}
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
isDisabled={hasNoArtist}
|
isDisabled={hasNoArtist}
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
|
@ -324,9 +326,9 @@ class ArtistIndex extends Component {
|
||||||
isLoaded &&
|
isLoaded &&
|
||||||
<div className={styles.contentBodyContainer}>
|
<div className={styles.contentBodyContainer}>
|
||||||
<ViewComponent
|
<ViewComponent
|
||||||
ref={this.setViewComponentRef}
|
|
||||||
contentBody={contentBody}
|
contentBody={contentBody}
|
||||||
scrollTop={scrollTop}
|
scrollTop={scrollTop}
|
||||||
|
jumpToCharacter={jumpToCharacter}
|
||||||
onRender={this.onRender}
|
onRender={this.onRender}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
@ -378,8 +380,9 @@ ArtistIndex.propTypes = {
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterKey: PropTypes.string,
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
view: PropTypes.string.isRequired,
|
view: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -100,8 +100,8 @@ class ArtistIndexConnector extends Component {
|
||||||
this.props.setArtistSort({ sortKey });
|
this.props.setArtistSort({ sortKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterSelect = (filterKey, filterValue, filterType) => {
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
this.props.setArtistFilter({ filterKey, filterValue, filterType });
|
this.props.setArtistFilter({ selectedFilterKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewSelect = (view) => {
|
onViewSelect = (view) => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
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 styles from './ArtistIndexFooter.css';
|
import styles from './ArtistIndexFooter.css';
|
||||||
|
@ -11,6 +12,7 @@ function ArtistIndexFooter({ artist }) {
|
||||||
let ended = 0;
|
let ended = 0;
|
||||||
let continuing = 0;
|
let continuing = 0;
|
||||||
let monitored = 0;
|
let monitored = 0;
|
||||||
|
let totalFileSize = 0;
|
||||||
|
|
||||||
artist.forEach((s) => {
|
artist.forEach((s) => {
|
||||||
tracks += s.trackCount || 0;
|
tracks += s.trackCount || 0;
|
||||||
|
@ -25,6 +27,8 @@ function ArtistIndexFooter({ artist }) {
|
||||||
if (s.monitored) {
|
if (s.monitored) {
|
||||||
monitored++;
|
monitored++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalFileSize += s.statistics.sizeOnDisk || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -92,6 +96,13 @@ function ArtistIndexFooter({ artist }) {
|
||||||
data={trackFiles}
|
data={trackFiles}
|
||||||
/>
|
/>
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
|
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Total File Size"
|
||||||
|
data={formatBytes(totalFileSize)}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -61,8 +61,7 @@ class ArtistIndexBanner extends Component {
|
||||||
status,
|
status,
|
||||||
foreignArtistId,
|
foreignArtistId,
|
||||||
nextAiring,
|
nextAiring,
|
||||||
trackCount,
|
statistics,
|
||||||
trackFileCount,
|
|
||||||
images,
|
images,
|
||||||
bannerWidth,
|
bannerWidth,
|
||||||
bannerHeight,
|
bannerHeight,
|
||||||
|
@ -79,6 +78,12 @@ class ArtistIndexBanner extends Component {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
trackCount,
|
||||||
|
trackFileCount,
|
||||||
|
totalTrackCount
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEditArtistModalOpen,
|
isEditArtistModalOpen,
|
||||||
isDeleteArtistModalOpen
|
isDeleteArtistModalOpen
|
||||||
|
@ -141,6 +146,7 @@ class ArtistIndexBanner extends Component {
|
||||||
status={status}
|
status={status}
|
||||||
trackCount={trackCount}
|
trackCount={trackCount}
|
||||||
trackFileCount={trackFileCount}
|
trackFileCount={trackFileCount}
|
||||||
|
totalTrackCount={totalTrackCount}
|
||||||
posterWidth={bannerWidth}
|
posterWidth={bannerWidth}
|
||||||
detailedProgressBar={detailedProgressBar}
|
detailedProgressBar={detailedProgressBar}
|
||||||
/>
|
/>
|
||||||
|
@ -165,20 +171,22 @@ class ArtistIndexBanner extends Component {
|
||||||
{qualityProfile.name}
|
{qualityProfile.name}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
{
|
||||||
<div className={styles.nextAiring}>
|
nextAiring &&
|
||||||
{
|
<div className={styles.nextAiring}>
|
||||||
getRelativeDate(
|
|
||||||
nextAiring,
|
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates,
|
|
||||||
{
|
{
|
||||||
timeFormat,
|
getRelativeDate(
|
||||||
timeForToday: true
|
nextAiring,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
{
|
||||||
|
timeFormat,
|
||||||
|
timeForToday: true
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
<ArtistIndexBannerInfo
|
<ArtistIndexBannerInfo
|
||||||
qualityProfile={qualityProfile}
|
qualityProfile={qualityProfile}
|
||||||
|
@ -186,6 +194,7 @@ class ArtistIndexBanner extends Component {
|
||||||
showRelativeDates={showRelativeDates}
|
showRelativeDates={showRelativeDates}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
|
statistics={statistics}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -215,8 +224,7 @@ ArtistIndexBanner.propTypes = {
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
foreignArtistId: PropTypes.string.isRequired,
|
foreignArtistId: PropTypes.string.isRequired,
|
||||||
nextAiring: PropTypes.string,
|
nextAiring: PropTypes.string,
|
||||||
trackCount: PropTypes.number,
|
statistics: PropTypes.object.isRequired,
|
||||||
trackFileCount: PropTypes.number,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
bannerWidth: PropTypes.number.isRequired,
|
bannerWidth: PropTypes.number.isRequired,
|
||||||
bannerHeight: PropTypes.number.isRequired,
|
bannerHeight: PropTypes.number.isRequired,
|
||||||
|
@ -234,7 +242,8 @@ ArtistIndexBanner.propTypes = {
|
||||||
|
|
||||||
ArtistIndexBanner.defaultProps = {
|
ArtistIndexBanner.defaultProps = {
|
||||||
trackCount: 0,
|
trackCount: 0,
|
||||||
trackFileCount: 0
|
trackFileCount: 0,
|
||||||
|
albumCount: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArtistIndexBanner;
|
export default ArtistIndexBanner;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.info {
|
.info {
|
||||||
background-color: $defaultColor;
|
background-color: #fafbfc;
|
||||||
color: $white;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: $smallFontSize;
|
font-size: $smallFontSize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,15 +10,19 @@ function ArtistIndexBannerInfo(props) {
|
||||||
showQualityProfile,
|
showQualityProfile,
|
||||||
previousAiring,
|
previousAiring,
|
||||||
added,
|
added,
|
||||||
albumCount,
|
statistics,
|
||||||
path,
|
path,
|
||||||
sizeOnDisk,
|
|
||||||
sortKey,
|
sortKey,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat
|
timeFormat
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
albumCount,
|
||||||
|
sizeOnDisk
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
if (sortKey === 'qualityProfileId' && !showQualityProfile) {
|
if (sortKey === 'qualityProfileId' && !showQualityProfile) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
|
@ -103,9 +107,8 @@ ArtistIndexBannerInfo.propTypes = {
|
||||||
showQualityProfile: PropTypes.bool.isRequired,
|
showQualityProfile: PropTypes.bool.isRequired,
|
||||||
previousAiring: PropTypes.string,
|
previousAiring: PropTypes.string,
|
||||||
added: PropTypes.string,
|
added: PropTypes.string,
|
||||||
albumCount: PropTypes.number.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
sizeOnDisk: PropTypes.number,
|
|
||||||
sortKey: PropTypes.string.isRequired,
|
sortKey: PropTypes.string.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
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 Measure from 'react-measure';
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
|
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';
|
||||||
|
@ -116,11 +116,11 @@ class ArtistIndexBanners extends Component {
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
filterKey,
|
filters,
|
||||||
filterValue,
|
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
bannerOptions
|
bannerOptions,
|
||||||
|
jumpToCharacter
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const itemsChanged = hasDifferentItems(prevProps.items, items);
|
const itemsChanged = hasDifferentItems(prevProps.items, items);
|
||||||
|
@ -134,44 +134,34 @@ class ArtistIndexBanners extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevProps.filterKey !== filterKey ||
|
prevProps.filters !== filters ||
|
||||||
prevProps.filterValue !== filterValue ||
|
|
||||||
prevProps.sortKey !== sortKey ||
|
prevProps.sortKey !== sortKey ||
|
||||||
prevProps.sortDirection !== sortDirection ||
|
prevProps.sortDirection !== sortDirection ||
|
||||||
itemsChanged
|
itemsChanged
|
||||||
) {
|
) {
|
||||||
this._grid.recomputeGridSize();
|
this._grid.recomputeGridSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||||
|
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||||
|
|
||||||
|
if (index != null) {
|
||||||
|
const {
|
||||||
|
columnCount,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const row = Math.floor(index / columnCount);
|
||||||
|
const scrollTop = rowHeight * row;
|
||||||
|
|
||||||
|
this.props.onScroll({ scrollTop });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
scrollToFirstCharacter(character) {
|
|
||||||
const items = this.props.items;
|
|
||||||
const {
|
|
||||||
columnCount,
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const index = _.findIndex(items, (item) => {
|
|
||||||
const firstCharacter = item.sortName.charAt(0);
|
|
||||||
|
|
||||||
if (character === '#') {
|
|
||||||
return !isNaN(firstCharacter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstCharacter === character;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (index != null) {
|
|
||||||
const row = Math.floor(index / columnCount);
|
|
||||||
const scrollTop = rowHeight * row;
|
|
||||||
|
|
||||||
this.props.onScroll({ scrollTop });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setGridRef = (ref) => {
|
setGridRef = (ref) => {
|
||||||
this._grid = ref;
|
this._grid = ref;
|
||||||
}
|
}
|
||||||
|
@ -319,12 +309,12 @@ class ArtistIndexBanners extends Component {
|
||||||
|
|
||||||
ArtistIndexBanners.propTypes = {
|
ArtistIndexBanners.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterKey: PropTypes.string,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
bannerOptions: PropTypes.object.isRequired,
|
bannerOptions: PropTypes.object.isRequired,
|
||||||
scrollTop: PropTypes.number.isRequired,
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
jumpToCharacter: PropTypes.string,
|
||||||
contentBody: PropTypes.object.isRequired,
|
contentBody: PropTypes.object.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -28,6 +28,6 @@ export default connectSection(
|
||||||
createMapStateToProps,
|
createMapStateToProps,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
{ withRef: true },
|
undefined,
|
||||||
{ section: 'artist', uiSection: 'artistIndex' }
|
{ section: 'artist', uiSection: 'artistIndex' }
|
||||||
)(ArtistIndexBanners);
|
)(ArtistIndexBanners);
|
||||||
|
|
|
@ -2,80 +2,38 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { align } from 'Helpers/Props';
|
import { align } from 'Helpers/Props';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
|
||||||
import FilterMenuItem from 'Components/Menu/FilterMenuItem';
|
|
||||||
|
|
||||||
function ArtistIndexFilterMenu(props) {
|
function ArtistIndexFilterMenu(props) {
|
||||||
const {
|
const {
|
||||||
filterKey,
|
selectedFilterKey,
|
||||||
filterValue,
|
filters,
|
||||||
|
customFilters,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
onFilterSelect
|
onFilterSelect
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterMenu
|
<FilterMenu
|
||||||
isDisabled={isDisabled}
|
|
||||||
alignMenu={align.RIGHT}
|
alignMenu={align.RIGHT}
|
||||||
>
|
isDisabled={isDisabled}
|
||||||
<MenuContent>
|
selectedFilterKey={selectedFilterKey}
|
||||||
<FilterMenuItem
|
filters={filters}
|
||||||
filterKey={filterKey}
|
customFilters={customFilters}
|
||||||
filterValue={filterValue}
|
onFilterSelect={onFilterSelect}
|
||||||
onPress={onFilterSelect}
|
/>
|
||||||
>
|
|
||||||
All
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="monitored"
|
|
||||||
value={true}
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Monitored Only
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="status"
|
|
||||||
value="continuing"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Continuing Only
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="status"
|
|
||||||
value="ended"
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Ended Only
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="missing"
|
|
||||||
value={true}
|
|
||||||
filterKey={filterKey}
|
|
||||||
filterValue={filterValue}
|
|
||||||
onPress={onFilterSelect}
|
|
||||||
>
|
|
||||||
Missing Albums
|
|
||||||
</FilterMenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</FilterMenu>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistIndexFilterMenu.propTypes = {
|
ArtistIndexFilterMenu.propTypes = {
|
||||||
filterKey: PropTypes.string,
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
onFilterSelect: PropTypes.func.isRequired
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ArtistIndexFilterMenu.defaultProps = {
|
||||||
|
showCustomFilters: false
|
||||||
|
};
|
||||||
|
|
||||||
export default ArtistIndexFilterMenu;
|
export default ArtistIndexFilterMenu;
|
||||||
|
|
|
@ -78,8 +78,7 @@ class ArtistIndexOverview extends Component {
|
||||||
status,
|
status,
|
||||||
foreignArtistId,
|
foreignArtistId,
|
||||||
nextAiring,
|
nextAiring,
|
||||||
trackCount,
|
statistics,
|
||||||
trackFileCount,
|
|
||||||
images,
|
images,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
posterHeight,
|
posterHeight,
|
||||||
|
@ -95,6 +94,12 @@ class ArtistIndexOverview extends Component {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
trackCount,
|
||||||
|
trackFileCount,
|
||||||
|
totalTrackCount
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEditArtistModalOpen,
|
isEditArtistModalOpen,
|
||||||
isDeleteArtistModalOpen
|
isDeleteArtistModalOpen
|
||||||
|
@ -144,6 +149,7 @@ class ArtistIndexOverview extends Component {
|
||||||
status={status}
|
status={status}
|
||||||
trackCount={trackCount}
|
trackCount={trackCount}
|
||||||
trackFileCount={trackFileCount}
|
trackFileCount={trackFileCount}
|
||||||
|
totalTrackCount={totalTrackCount}
|
||||||
posterWidth={posterWidth}
|
posterWidth={posterWidth}
|
||||||
detailedProgressBar={overviewOptions.detailedProgressBar}
|
detailedProgressBar={overviewOptions.detailedProgressBar}
|
||||||
/>
|
/>
|
||||||
|
@ -194,6 +200,7 @@ class ArtistIndexOverview extends Component {
|
||||||
showRelativeDates={showRelativeDates}
|
showRelativeDates={showRelativeDates}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
|
statistics={statistics}
|
||||||
{...overviewOptions}
|
{...overviewOptions}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
@ -227,8 +234,7 @@ ArtistIndexOverview.propTypes = {
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
foreignArtistId: PropTypes.string.isRequired,
|
foreignArtistId: PropTypes.string.isRequired,
|
||||||
nextAiring: PropTypes.string,
|
nextAiring: PropTypes.string,
|
||||||
trackCount: PropTypes.number,
|
statistics: PropTypes.object.isRequired,
|
||||||
trackFileCount: PropTypes.number,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
posterWidth: PropTypes.number.isRequired,
|
posterWidth: PropTypes.number.isRequired,
|
||||||
posterHeight: PropTypes.number.isRequired,
|
posterHeight: PropTypes.number.isRequired,
|
||||||
|
@ -245,7 +251,8 @@ ArtistIndexOverview.propTypes = {
|
||||||
|
|
||||||
ArtistIndexOverview.defaultProps = {
|
ArtistIndexOverview.defaultProps = {
|
||||||
trackCount: 0,
|
trackCount: 0,
|
||||||
trackFileCount: 0
|
trackFileCount: 0,
|
||||||
|
albumCount: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArtistIndexOverview;
|
export default ArtistIndexOverview;
|
||||||
|
|
|
@ -31,15 +31,19 @@ function ArtistIndexOverviewInfo(props) {
|
||||||
nextAiring,
|
nextAiring,
|
||||||
qualityProfile,
|
qualityProfile,
|
||||||
added,
|
added,
|
||||||
albumCount,
|
statistics,
|
||||||
path,
|
path,
|
||||||
sizeOnDisk,
|
|
||||||
sortKey,
|
sortKey,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat
|
timeFormat
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
albumCount,
|
||||||
|
sizeOnDisk
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
let albums = '1 album';
|
let albums = '1 album';
|
||||||
|
|
||||||
if (albumCount === 0) {
|
if (albumCount === 0) {
|
||||||
|
@ -203,9 +207,8 @@ ArtistIndexOverviewInfo.propTypes = {
|
||||||
qualityProfile: PropTypes.object.isRequired,
|
qualityProfile: PropTypes.object.isRequired,
|
||||||
previousAiring: PropTypes.string,
|
previousAiring: PropTypes.string,
|
||||||
added: PropTypes.string,
|
added: PropTypes.string,
|
||||||
albumCount: PropTypes.number.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
sizeOnDisk: PropTypes.number,
|
|
||||||
sortKey: PropTypes.string.isRequired,
|
sortKey: PropTypes.string.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import React, { Component } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Measure from 'react-measure';
|
import Measure from 'react-measure';
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
|
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';
|
||||||
|
@ -76,11 +77,11 @@ class ArtistIndexOverviews extends Component {
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
filterKey,
|
filters,
|
||||||
filterValue,
|
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
overviewOptions
|
overviewOptions,
|
||||||
|
jumpToCharacter
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const itemsChanged = hasDifferentItems(prevProps.items, items);
|
const itemsChanged = hasDifferentItems(prevProps.items, items);
|
||||||
|
@ -95,8 +96,7 @@ class ArtistIndexOverviews extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevProps.filterKey !== filterKey ||
|
prevProps.filters !== filters ||
|
||||||
prevProps.filterValue !== filterValue ||
|
|
||||||
prevProps.sortKey !== sortKey ||
|
prevProps.sortKey !== sortKey ||
|
||||||
prevProps.sortDirection !== sortDirection ||
|
prevProps.sortDirection !== sortDirection ||
|
||||||
itemsChanged ||
|
itemsChanged ||
|
||||||
|
@ -104,6 +104,20 @@ class ArtistIndexOverviews extends Component {
|
||||||
) {
|
) {
|
||||||
this._grid.recomputeGridSize();
|
this._grid.recomputeGridSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||||
|
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||||
|
|
||||||
|
if (index != null) {
|
||||||
|
const {
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const scrollTop = rowHeight * index;
|
||||||
|
|
||||||
|
this.props.onScroll({ scrollTop });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -115,15 +129,7 @@ class ArtistIndexOverviews extends Component {
|
||||||
rowHeight
|
rowHeight
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const index = _.findIndex(items, (item) => {
|
const index = getIndexOfFirstCharacter(items, character);
|
||||||
const firstCharacter = item.sortTitle.charAt(0);
|
|
||||||
|
|
||||||
if (character === '#') {
|
|
||||||
return !isNaN(firstCharacter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstCharacter === character;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
const scrollTop = rowHeight * index;
|
const scrollTop = rowHeight * index;
|
||||||
|
@ -263,12 +269,12 @@ class ArtistIndexOverviews extends Component {
|
||||||
|
|
||||||
ArtistIndexOverviews.propTypes = {
|
ArtistIndexOverviews.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterKey: PropTypes.string,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
overviewOptions: PropTypes.object.isRequired,
|
overviewOptions: PropTypes.object.isRequired,
|
||||||
scrollTop: PropTypes.number.isRequired,
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
jumpToCharacter: PropTypes.string,
|
||||||
contentBody: PropTypes.object.isRequired,
|
contentBody: PropTypes.object.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -28,6 +28,6 @@ export default connectSection(
|
||||||
createMapStateToProps,
|
createMapStateToProps,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
{ withRef: true },
|
undefined,
|
||||||
{ section: 'artist', uiSection: 'artistIndex' }
|
{ section: 'artist', uiSection: 'artistIndex' }
|
||||||
)(ArtistIndexOverviews);
|
)(ArtistIndexOverviews);
|
||||||
|
|
|
@ -204,7 +204,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Show Season Count</FormLabel>
|
<FormLabel>Show Album Count</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
|
|
|
@ -61,8 +61,7 @@ class ArtistIndexPoster extends Component {
|
||||||
foreignArtistId,
|
foreignArtistId,
|
||||||
status,
|
status,
|
||||||
nextAiring,
|
nextAiring,
|
||||||
trackCount,
|
statistics,
|
||||||
trackFileCount,
|
|
||||||
images,
|
images,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
posterHeight,
|
posterHeight,
|
||||||
|
@ -79,6 +78,12 @@ class ArtistIndexPoster extends Component {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
trackCount,
|
||||||
|
trackFileCount,
|
||||||
|
totalTrackCount
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEditArtistModalOpen,
|
isEditArtistModalOpen,
|
||||||
isDeleteArtistModalOpen
|
isDeleteArtistModalOpen
|
||||||
|
@ -141,6 +146,7 @@ class ArtistIndexPoster extends Component {
|
||||||
status={status}
|
status={status}
|
||||||
trackCount={trackCount}
|
trackCount={trackCount}
|
||||||
trackFileCount={trackFileCount}
|
trackFileCount={trackFileCount}
|
||||||
|
totalTrackCount={totalTrackCount}
|
||||||
posterWidth={posterWidth}
|
posterWidth={posterWidth}
|
||||||
detailedProgressBar={detailedProgressBar}
|
detailedProgressBar={detailedProgressBar}
|
||||||
/>
|
/>
|
||||||
|
@ -165,26 +171,28 @@ class ArtistIndexPoster extends Component {
|
||||||
{qualityProfile.name}
|
{qualityProfile.name}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
{
|
||||||
<div className={styles.nextAiring}>
|
nextAiring &&
|
||||||
{
|
<div className={styles.nextAiring}>
|
||||||
getRelativeDate(
|
|
||||||
nextAiring,
|
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates,
|
|
||||||
{
|
{
|
||||||
timeFormat,
|
getRelativeDate(
|
||||||
timeForToday: true
|
nextAiring,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
{
|
||||||
|
timeFormat,
|
||||||
|
timeForToday: true
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
<ArtistIndexPosterInfo
|
<ArtistIndexPosterInfo
|
||||||
qualityProfile={qualityProfile}
|
qualityProfile={qualityProfile}
|
||||||
showQualityProfile={showQualityProfile}
|
showQualityProfile={showQualityProfile}
|
||||||
showRelativeDates={showRelativeDates}
|
showRelativeDates={showRelativeDates}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
|
statistics={statistics}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
@ -215,8 +223,7 @@ ArtistIndexPoster.propTypes = {
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
foreignArtistId: PropTypes.string.isRequired,
|
foreignArtistId: PropTypes.string.isRequired,
|
||||||
nextAiring: PropTypes.string,
|
nextAiring: PropTypes.string,
|
||||||
trackCount: PropTypes.number,
|
statistics: PropTypes.object.isRequired,
|
||||||
trackFileCount: PropTypes.number,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
posterWidth: PropTypes.number.isRequired,
|
posterWidth: PropTypes.number.isRequired,
|
||||||
posterHeight: PropTypes.number.isRequired,
|
posterHeight: PropTypes.number.isRequired,
|
||||||
|
@ -234,7 +241,8 @@ ArtistIndexPoster.propTypes = {
|
||||||
|
|
||||||
ArtistIndexPoster.defaultProps = {
|
ArtistIndexPoster.defaultProps = {
|
||||||
trackCount: 0,
|
trackCount: 0,
|
||||||
trackFileCount: 0
|
trackFileCount: 0,
|
||||||
|
albumCount: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArtistIndexPoster;
|
export default ArtistIndexPoster;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.info {
|
.info {
|
||||||
background-color: $defaultColor;
|
background-color: #fafbfc;
|
||||||
color: $white;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: $smallFontSize;
|
font-size: $smallFontSize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,15 +10,19 @@ function ArtistIndexPosterInfo(props) {
|
||||||
showQualityProfile,
|
showQualityProfile,
|
||||||
previousAiring,
|
previousAiring,
|
||||||
added,
|
added,
|
||||||
albumCount,
|
statistics,
|
||||||
path,
|
path,
|
||||||
sizeOnDisk,
|
|
||||||
sortKey,
|
sortKey,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat
|
timeFormat
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
albumCount,
|
||||||
|
sizeOnDisk
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
if (sortKey === 'qualityProfileId' && !showQualityProfile) {
|
if (sortKey === 'qualityProfileId' && !showQualityProfile) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
|
@ -103,9 +107,8 @@ ArtistIndexPosterInfo.propTypes = {
|
||||||
showQualityProfile: PropTypes.bool.isRequired,
|
showQualityProfile: PropTypes.bool.isRequired,
|
||||||
previousAiring: PropTypes.string,
|
previousAiring: PropTypes.string,
|
||||||
added: PropTypes.string,
|
added: PropTypes.string,
|
||||||
albumCount: PropTypes.number,
|
statistics: PropTypes.object.isRequired,
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
sizeOnDisk: PropTypes.number,
|
|
||||||
sortKey: PropTypes.string.isRequired,
|
sortKey: PropTypes.string.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
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 Measure from 'react-measure';
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
|
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';
|
||||||
|
@ -116,11 +116,11 @@ class ArtistIndexPosters extends Component {
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
filterKey,
|
filters,
|
||||||
filterValue,
|
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
posterOptions
|
posterOptions,
|
||||||
|
jumpToCharacter
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const itemsChanged = hasDifferentItems(prevProps.items, items);
|
const itemsChanged = hasDifferentItems(prevProps.items, items);
|
||||||
|
@ -134,44 +134,34 @@ class ArtistIndexPosters extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevProps.filterKey !== filterKey ||
|
prevProps.filters !== filters ||
|
||||||
prevProps.filterValue !== filterValue ||
|
|
||||||
prevProps.sortKey !== sortKey ||
|
prevProps.sortKey !== sortKey ||
|
||||||
prevProps.sortDirection !== sortDirection ||
|
prevProps.sortDirection !== sortDirection ||
|
||||||
itemsChanged
|
itemsChanged
|
||||||
) {
|
) {
|
||||||
this._grid.recomputeGridSize();
|
this._grid.recomputeGridSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||||
|
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||||
|
|
||||||
|
if (index != null) {
|
||||||
|
const {
|
||||||
|
columnCount,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const row = Math.floor(index / columnCount);
|
||||||
|
const scrollTop = rowHeight * row;
|
||||||
|
|
||||||
|
this.props.onScroll({ scrollTop });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
scrollToFirstCharacter(character) {
|
|
||||||
const items = this.props.items;
|
|
||||||
const {
|
|
||||||
columnCount,
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const index = _.findIndex(items, (item) => {
|
|
||||||
const firstCharacter = item.sortName.charAt(0);
|
|
||||||
|
|
||||||
if (character === '#') {
|
|
||||||
return !isNaN(firstCharacter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstCharacter === character;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (index != null) {
|
|
||||||
const row = Math.floor(index / columnCount);
|
|
||||||
const scrollTop = rowHeight * row;
|
|
||||||
|
|
||||||
this.props.onScroll({ scrollTop });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setGridRef = (ref) => {
|
setGridRef = (ref) => {
|
||||||
this._grid = ref;
|
this._grid = ref;
|
||||||
}
|
}
|
||||||
|
@ -319,12 +309,12 @@ class ArtistIndexPosters extends Component {
|
||||||
|
|
||||||
ArtistIndexPosters.propTypes = {
|
ArtistIndexPosters.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterKey: PropTypes.string,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
posterOptions: PropTypes.object.isRequired,
|
posterOptions: PropTypes.object.isRequired,
|
||||||
scrollTop: PropTypes.number.isRequired,
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
jumpToCharacter: PropTypes.string,
|
||||||
contentBody: PropTypes.object.isRequired,
|
contentBody: PropTypes.object.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -28,6 +28,6 @@ export default connectSection(
|
||||||
createMapStateToProps,
|
createMapStateToProps,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
{ withRef: true },
|
undefined,
|
||||||
{ section: 'artist', uiSection: 'artistIndex' }
|
{ section: 'artist', uiSection: 'artistIndex' }
|
||||||
)(ArtistIndexPosters);
|
)(ArtistIndexPosters);
|
||||||
|
|
|
@ -11,6 +11,7 @@ function ArtistIndexProgressBar(props) {
|
||||||
status,
|
status,
|
||||||
trackCount,
|
trackCount,
|
||||||
trackFileCount,
|
trackFileCount,
|
||||||
|
totalTrackCount,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
detailedProgressBar
|
detailedProgressBar
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -27,7 +28,7 @@ function ArtistIndexProgressBar(props) {
|
||||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||||
showText={detailedProgressBar}
|
showText={detailedProgressBar}
|
||||||
text={text}
|
text={text}
|
||||||
title={detailedProgressBar ? null : text}
|
title={`${trackFileCount} / ${trackCount} (Total: ${totalTrackCount})`}
|
||||||
width={posterWidth}
|
width={posterWidth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -38,6 +39,7 @@ ArtistIndexProgressBar.propTypes = {
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
trackCount: PropTypes.number.isRequired,
|
trackCount: PropTypes.number.isRequired,
|
||||||
trackFileCount: PropTypes.number.isRequired,
|
trackFileCount: PropTypes.number.isRequired,
|
||||||
|
totalTrackCount: PropTypes.number.isRequired,
|
||||||
posterWidth: PropTypes.number.isRequired,
|
posterWidth: PropTypes.number.isRequired,
|
||||||
detailedProgressBar: PropTypes.bool.isRequired
|
detailedProgressBar: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
.sizeOnDisk {
|
.sizeOnDisk {
|
||||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
flex: 0 0 110px;
|
flex: 0 0 115px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
|
|
|
@ -74,18 +74,22 @@ class ArtistIndexRow extends Component {
|
||||||
nextAlbum,
|
nextAlbum,
|
||||||
lastAlbum,
|
lastAlbum,
|
||||||
added,
|
added,
|
||||||
albumCount,
|
statistics,
|
||||||
trackCount,
|
|
||||||
trackFileCount,
|
|
||||||
totalTrackCount,
|
|
||||||
path,
|
path,
|
||||||
sizeOnDisk,
|
|
||||||
tags,
|
tags,
|
||||||
columns,
|
columns,
|
||||||
isRefreshingArtist,
|
isRefreshingArtist,
|
||||||
onRefreshArtistPress
|
onRefreshArtistPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
albumCount,
|
||||||
|
trackCount,
|
||||||
|
trackFileCount,
|
||||||
|
totalTrackCount,
|
||||||
|
sizeOnDisk
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEditArtistModalOpen,
|
isEditArtistModalOpen,
|
||||||
isDeleteArtistModalOpen
|
isDeleteArtistModalOpen
|
||||||
|
@ -367,13 +371,9 @@ ArtistIndexRow.propTypes = {
|
||||||
nextAlbum: PropTypes.object,
|
nextAlbum: PropTypes.object,
|
||||||
lastAlbum: PropTypes.object,
|
lastAlbum: PropTypes.object,
|
||||||
added: PropTypes.string,
|
added: PropTypes.string,
|
||||||
albumCount: PropTypes.number,
|
statistics: PropTypes.object.isRequired,
|
||||||
trackCount: PropTypes.number,
|
|
||||||
trackFileCount: PropTypes.number,
|
|
||||||
totalTrackCount: PropTypes.number,
|
|
||||||
latestAlbum: PropTypes.object,
|
latestAlbum: PropTypes.object,
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
sizeOnDisk: PropTypes.number,
|
|
||||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isRefreshingArtist: PropTypes.bool.isRequired,
|
isRefreshingArtist: PropTypes.bool.isRequired,
|
||||||
|
@ -382,7 +382,8 @@ ArtistIndexRow.propTypes = {
|
||||||
|
|
||||||
ArtistIndexRow.defaultProps = {
|
ArtistIndexRow.defaultProps = {
|
||||||
trackCount: 0,
|
trackCount: 0,
|
||||||
trackFileCount: 0
|
trackFileCount: 0,
|
||||||
|
albumCount: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArtistIndexRow;
|
export default ArtistIndexRow;
|
||||||
|
|
|
@ -1,6 +1,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 getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import VirtualTable from 'Components/Table/VirtualTable';
|
import VirtualTable from 'Components/Table/VirtualTable';
|
||||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||||
|
@ -9,40 +9,37 @@ import ArtistIndexRow from './ArtistIndexRow';
|
||||||
import styles from './ArtistIndexTable.css';
|
import styles from './ArtistIndexTable.css';
|
||||||
|
|
||||||
class ArtistIndexTable extends Component {
|
class ArtistIndexTable extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this._table = null;
|
|
||||||
|
this.state = {
|
||||||
|
scrollIndex: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const jumpToCharacter = this.props.jumpToCharacter;
|
||||||
|
|
||||||
|
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||||
|
const items = this.props.items;
|
||||||
|
|
||||||
|
const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||||
|
|
||||||
|
if (scrollIndex != null) {
|
||||||
|
this.setState({ scrollIndex });
|
||||||
|
}
|
||||||
|
} else if (jumpToCharacter == null && prevProps.jumpToCharacter != null) {
|
||||||
|
this.setState({ scrollIndex: null });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the reference to the virtual table
|
|
||||||
* @param ref
|
|
||||||
*/
|
|
||||||
setTableRef = (ref) => {
|
|
||||||
this._table = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
scrollToFirstCharacter(character) {
|
|
||||||
const items = this.props.items;
|
|
||||||
|
|
||||||
const row = _.findIndex(items, (item) => {
|
|
||||||
const firstCharacter = item.sortName.charAt(0);
|
|
||||||
|
|
||||||
if (character === '#') {
|
|
||||||
return !isNaN(firstCharacter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstCharacter === character;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (row != null) {
|
|
||||||
this._table.scrollToRow(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rowRenderer = ({ key, rowIndex, style }) => {
|
rowRenderer = ({ key, rowIndex, style }) => {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
|
@ -72,8 +69,7 @@ class ArtistIndexTable extends Component {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
filterKey,
|
filters,
|
||||||
filterValue,
|
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
|
@ -86,10 +82,10 @@ class ArtistIndexTable extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualTable
|
<VirtualTable
|
||||||
ref={this.setTableRef}
|
|
||||||
className={styles.tableContainer}
|
className={styles.tableContainer}
|
||||||
items={items}
|
items={items}
|
||||||
scrollTop={scrollTop}
|
scrollTop={scrollTop}
|
||||||
|
scrollIndex={this.state.scrollIndex}
|
||||||
contentBody={contentBody}
|
contentBody={contentBody}
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
rowHeight={38}
|
rowHeight={38}
|
||||||
|
@ -104,8 +100,7 @@ class ArtistIndexTable extends Component {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
filterKey={filterKey}
|
filters={filters}
|
||||||
filterValue={filterValue}
|
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onRender={onRender}
|
onRender={onRender}
|
||||||
|
@ -118,11 +113,11 @@ class ArtistIndexTable extends Component {
|
||||||
ArtistIndexTable.propTypes = {
|
ArtistIndexTable.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterKey: PropTypes.string,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
scrollTop: PropTypes.number.isRequired,
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
jumpToCharacter: PropTypes.string,
|
||||||
contentBody: PropTypes.object.isRequired,
|
contentBody: PropTypes.object.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -29,6 +29,6 @@ export default connectSection(
|
||||||
createMapStateToProps,
|
createMapStateToProps,
|
||||||
createMapDispatchToProps,
|
createMapDispatchToProps,
|
||||||
undefined,
|
undefined,
|
||||||
{ withRef: true },
|
undefined,
|
||||||
{ section: 'artist', uiSection: 'artistIndex' }
|
{ section: 'artist', uiSection: 'artistIndex' }
|
||||||
)(ArtistIndexTable);
|
)(ArtistIndexTable);
|
||||||
|
|
|
@ -25,7 +25,7 @@ function MoveArtistModal(props) {
|
||||||
!destinationPath &&
|
!destinationPath &&
|
||||||
!destinationRootFolder
|
!destinationRootFolder
|
||||||
) {
|
) {
|
||||||
console.error('orginalPath and destinationPath OR destinationRootFolder must be provied');
|
console.error('orginalPath and destinationPath OR destinationRootFolder must be provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Measure from 'react-measure';
|
import Measure from 'react-measure';
|
||||||
import { align, icons } from 'Helpers/Props';
|
import { align, icons } from 'Helpers/Props';
|
||||||
|
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
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';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
|
||||||
import FilterMenuItem from 'Components/Menu/FilterMenuItem';
|
|
||||||
import NoArtist from 'Artist/NoArtist';
|
import NoArtist from 'Artist/NoArtist';
|
||||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||||
import Legend from './Legend/Legend';
|
import Legend from './Legend/Legend';
|
||||||
|
@ -42,10 +41,6 @@ class CalendarPage extends Component {
|
||||||
this.props.onDaysCountChange(days);
|
this.props.onDaysCountChange(days);
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterMenuItemPress = (filterKey, unmonitored) => {
|
|
||||||
this.props.onUnmonitoredChange(unmonitored);
|
|
||||||
}
|
|
||||||
|
|
||||||
onGetCalendarLinkPress = () => {
|
onGetCalendarLinkPress = () => {
|
||||||
this.setState({ isCalendarLinkModalOpen: true });
|
this.setState({ isCalendarLinkModalOpen: true });
|
||||||
}
|
}
|
||||||
|
@ -59,12 +54,15 @@ class CalendarPage extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
unmonitored,
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
hasArtist,
|
hasArtist,
|
||||||
colorImpairedMode
|
colorImpairedMode,
|
||||||
|
onFilterSelect
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isMeasured = this.state.width > 0;
|
const isMeasured = this.state.width > 0;
|
||||||
|
|
||||||
let PageComponent = 'div';
|
let PageComponent = 'div';
|
||||||
|
|
||||||
if (isMeasured) {
|
if (isMeasured) {
|
||||||
|
@ -85,30 +83,11 @@ class CalendarPage extends Component {
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
<FilterMenu
|
<FilterMenu
|
||||||
alignMenu={align.RIGHT}
|
alignMenu={align.RIGHT}
|
||||||
isDisabled={!hasArtist}
|
selectedFilterKey={selectedFilterKey}
|
||||||
>
|
filters={filters}
|
||||||
<MenuContent>
|
customFilters={[]}
|
||||||
<FilterMenuItem
|
onFilterSelect={onFilterSelect}
|
||||||
name="unmonitored"
|
/>
|
||||||
value={true}
|
|
||||||
filterKey="unmonitored"
|
|
||||||
filterValue={unmonitored}
|
|
||||||
onPress={this.onFilterMenuItemPress}
|
|
||||||
>
|
|
||||||
All
|
|
||||||
</FilterMenuItem>
|
|
||||||
|
|
||||||
<FilterMenuItem
|
|
||||||
name="unmonitored"
|
|
||||||
value={false}
|
|
||||||
filterKey="unmonitored"
|
|
||||||
filterValue={unmonitored}
|
|
||||||
onPress={this.onFilterMenuItemPress}
|
|
||||||
>
|
|
||||||
Monitored Only
|
|
||||||
</FilterMenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</FilterMenu>
|
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
|
@ -139,11 +118,12 @@ class CalendarPage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
CalendarPage.propTypes = {
|
CalendarPage.propTypes = {
|
||||||
unmonitored: PropTypes.bool.isRequired,
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
hasArtist: PropTypes.bool.isRequired,
|
hasArtist: PropTypes.bool.isRequired,
|
||||||
colorImpairedMode: PropTypes.bool.isRequired,
|
colorImpairedMode: PropTypes.bool.isRequired,
|
||||||
onDaysCountChange: PropTypes.func.isRequired,
|
onDaysCountChange: PropTypes.func.isRequired,
|
||||||
onUnmonitoredChange: PropTypes.func.isRequired
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CalendarPage;
|
export default CalendarPage;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { setCalendarDaysCount, setCalendarIncludeUnmonitored } from 'Store/Actions/calendarActions';
|
import { setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||||
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
|
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import CalendarPage from './CalendarPage';
|
import CalendarPage from './CalendarPage';
|
||||||
|
@ -12,7 +12,8 @@ function createMapStateToProps() {
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
(calendar, artistCount, uiSettings) => {
|
(calendar, artistCount, uiSettings) => {
|
||||||
return {
|
return {
|
||||||
unmonitored: calendar.unmonitored,
|
filters: calendar.filters,
|
||||||
|
selectedFilterKey: calendar.selectedFilterKey,
|
||||||
showUpcoming: calendar.showUpcoming,
|
showUpcoming: calendar.showUpcoming,
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||||
hasArtist: !!artistCount
|
hasArtist: !!artistCount
|
||||||
|
@ -27,8 +28,8 @@ function createMapDispatchToProps(dispatch, props) {
|
||||||
dispatch(setCalendarDaysCount({ dayCount }));
|
dispatch(setCalendarDaysCount({ dayCount }));
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnmonitoredChange(unmonitored) {
|
onFilterSelect(selectedFilterKey) {
|
||||||
dispatch(setCalendarIncludeUnmonitored({ unmonitored }));
|
dispatch(setCalendarFilter({ selectedFilterKey }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
.labelContainer {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelInputContainer {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rows {
|
||||||
|
margin-bottom: 100px;
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
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 FilterBuilderRow from './FilterBuilderRow';
|
||||||
|
import styles from './FilterBuilderModalContent.css';
|
||||||
|
|
||||||
|
class FilterBuilderModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const filters = [...props.filters];
|
||||||
|
|
||||||
|
// Push an empty filter if there aren't any filters. FilterBuilderRow
|
||||||
|
// will handle initializing the filter.
|
||||||
|
|
||||||
|
if (!filters.length) {
|
||||||
|
filters.push({});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
label: props.label,
|
||||||
|
filters,
|
||||||
|
labelErrors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onLabelChange = ({ value }) => {
|
||||||
|
this.setState({ label: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChange = (index, filter) => {
|
||||||
|
const filters = [...this.state.filters];
|
||||||
|
filters.splice(index, 1, filter);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
filters
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddFilterPress = () => {
|
||||||
|
const filters = [...this.state.filters];
|
||||||
|
filters.push({});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
filters
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveFilterPress = (index) => {
|
||||||
|
const filters = [...this.state.filters];
|
||||||
|
filters.splice(index, 1);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
filters
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSaveFilterPress = () => {
|
||||||
|
const {
|
||||||
|
customFilterKey: key,
|
||||||
|
onSaveCustomFilterPress,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
filters
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
this.setState({
|
||||||
|
labelErrors: [
|
||||||
|
{
|
||||||
|
message: 'Label is required'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSaveCustomFilterPress({ key, label, filters });
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
sectionItems,
|
||||||
|
filterBuilderProps,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
filters,
|
||||||
|
labelErrors
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Custom Filter
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.labelContainer}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
Label
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.labelInputContainer}>
|
||||||
|
<FormInputGroup
|
||||||
|
name="label"
|
||||||
|
value={label}
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
errors={labelErrors}
|
||||||
|
onChange={this.onLabelChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.label}>Filters</div>
|
||||||
|
|
||||||
|
<div className={styles.rows}>
|
||||||
|
{
|
||||||
|
filters.map((filter, index) => {
|
||||||
|
return (
|
||||||
|
<FilterBuilderRow
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
filterKey={filter.key}
|
||||||
|
filterValue={filter.value}
|
||||||
|
filterType={filter.type}
|
||||||
|
filterCount={filters.length}
|
||||||
|
onAddPress={this.onAddFilterPress}
|
||||||
|
onRemovePress={this.onRemoveFilterPress}
|
||||||
|
onFilterChange={this.onFilterChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={this.onSaveFilterPress}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterBuilderModalContent.propTypes = {
|
||||||
|
customFilterKey: PropTypes.string,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onRemoveCustomFilterPress: PropTypes.func.isRequired,
|
||||||
|
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterBuilderModalContent;
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import FilterBuilderModalContent from './FilterBuilderModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { customFilters }) => customFilters,
|
||||||
|
(state, { customFilterKey }) => customFilterKey,
|
||||||
|
(customFilters, customFilterKey) => {
|
||||||
|
if (customFilterKey) {
|
||||||
|
const customFilter = customFilters.find((c) => c.key === customFilterKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
customFilterKey: customFilter.key,
|
||||||
|
label: customFilter.label,
|
||||||
|
filters: customFilter.filters
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: '',
|
||||||
|
filters: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(FilterBuilderModalContent);
|
|
@ -0,0 +1,32 @@
|
||||||
|
.filterRow {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $tableRowHoverBackgroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
flex: 0 1 200px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueInputContainer {
|
||||||
|
flex: 0 1 300px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionsContainer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.filterRow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,248 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||||
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
|
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||||
|
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||||
|
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||||
|
import styles from './FilterBuilderRow.css';
|
||||||
|
|
||||||
|
function getselectedFilterBuilderProp(filterBuilderProps, name) {
|
||||||
|
return filterBuilderProps.find((a) => {
|
||||||
|
return a.name === name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterTypeOptions(filterBuilderProps, filterKey) {
|
||||||
|
const selectedFilterBuilderProp = getselectedFilterBuilderProp(filterBuilderProps, filterKey);
|
||||||
|
|
||||||
|
if (!selectedFilterBuilderProp) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterBuilderTypes.possibleFilterTypes[selectedFilterBuilderProp.type];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultFilterType(selectedFilterBuilderProp) {
|
||||||
|
return filterBuilderTypes.possibleFilterTypes[selectedFilterBuilderProp.type][0].key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
|
if (!selectedFilterBuilderProp) {
|
||||||
|
return FilterBuilderRowValueConnector;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueType = selectedFilterBuilderProp.valueType;
|
||||||
|
|
||||||
|
switch (valueType) {
|
||||||
|
case filterBuilderValueTypes.INDEXER:
|
||||||
|
return IndexerFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.PROTOCOL:
|
||||||
|
return ProtocolFilterBuilderRowValue;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.QUALITY:
|
||||||
|
return QualityFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return FilterBuilderRowValueConnector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilterBuilderRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
selectedFilterBuilderProp: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
filterKey,
|
||||||
|
filterBuilderProps,
|
||||||
|
onFilterChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (filterKey) {
|
||||||
|
const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey);
|
||||||
|
this.setState({ selectedFilterBuilderProp });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedFilterBuilderProp = filterBuilderProps[0];
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
key: selectedFilterBuilderProp.name,
|
||||||
|
value: [],
|
||||||
|
type: getDefaultFilterType(selectedFilterBuilderProp)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({ selectedFilterBuilderProp }, () => {
|
||||||
|
onFilterChange(index, filter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFilterKeyChange = ({ value: key }) => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
filterBuilderProps,
|
||||||
|
onFilterChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const selectedFilterBuilderProp = getselectedFilterBuilderProp(filterBuilderProps, key);
|
||||||
|
const type = getDefaultFilterType(selectedFilterBuilderProp);
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
key,
|
||||||
|
value: [],
|
||||||
|
type
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({ selectedFilterBuilderProp }, () => {
|
||||||
|
onFilterChange(index, filter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChange = ({ name, value }) => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
filterKey,
|
||||||
|
filterValue,
|
||||||
|
filterType,
|
||||||
|
onFilterChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
key: filterKey,
|
||||||
|
value: filterValue,
|
||||||
|
type: filterType
|
||||||
|
};
|
||||||
|
|
||||||
|
filter[name] = value;
|
||||||
|
|
||||||
|
onFilterChange(index, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddPress = () => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
onAddPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onAddPress(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemovePress = () => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
onRemovePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onRemovePress(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
filterKey,
|
||||||
|
filterType,
|
||||||
|
filterValue,
|
||||||
|
filterCount,
|
||||||
|
filterBuilderProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedFilterBuilderProp
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
|
||||||
|
return {
|
||||||
|
key: availablePropFilter.name,
|
||||||
|
value: availablePropFilter.label
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.filterRow}>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
{
|
||||||
|
filterKey &&
|
||||||
|
<SelectInput
|
||||||
|
name="key"
|
||||||
|
value={filterKey}
|
||||||
|
values={keyOptions}
|
||||||
|
onChange={this.onFilterKeyChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
{
|
||||||
|
filterType &&
|
||||||
|
<SelectInput
|
||||||
|
name="type"
|
||||||
|
value={filterType}
|
||||||
|
values={getFilterTypeOptions(filterBuilderProps, filterKey)}
|
||||||
|
onChange={this.onFilterChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.valueInputContainer}>
|
||||||
|
{
|
||||||
|
filterValue != null && !!selectedFilterBuilderProp &&
|
||||||
|
<ValueComponent
|
||||||
|
filterValue={filterValue}
|
||||||
|
selectedFilterBuilderProp={selectedFilterBuilderProp}
|
||||||
|
onChange={this.onFilterChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actionsContainer}>
|
||||||
|
<IconButton
|
||||||
|
name={icons.SUBTRACT}
|
||||||
|
isDisabled={filterCount === 1}
|
||||||
|
onPress={this.onRemovePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.ADD}
|
||||||
|
onPress={this.onAddPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterBuilderRow.propTypes = {
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
filterKey: PropTypes.string,
|
||||||
|
filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
|
||||||
|
filterType: PropTypes.string,
|
||||||
|
filterCount: PropTypes.number.isRequired,
|
||||||
|
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onFilterChange: PropTypes.func.isRequired,
|
||||||
|
onAddPress: PropTypes.func.isRequired,
|
||||||
|
onRemovePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterBuilderRow;
|
|
@ -0,0 +1,100 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { kinds, filterBuilderTypes } from 'Helpers/Props';
|
||||||
|
import TagInput, { tagShape } from 'Components/Form/TagInput';
|
||||||
|
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
|
||||||
|
|
||||||
|
const NAME = 'value';
|
||||||
|
|
||||||
|
class FilterBuilderRowValue extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onTagAdd = (tag) => {
|
||||||
|
const {
|
||||||
|
filterValue,
|
||||||
|
selectedFilterBuilderProp,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let id = tag.id;
|
||||||
|
|
||||||
|
if (id == null) {
|
||||||
|
id = selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ?
|
||||||
|
parseInt(tag.name) :
|
||||||
|
tag.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name: NAME,
|
||||||
|
value: [...filterValue, id]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onTagDelete = ({ index }) => {
|
||||||
|
const {
|
||||||
|
filterValue,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const value = filterValue.filter((v, i) => i !== index);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name: NAME,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
filterValue,
|
||||||
|
tagList
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const hasItems = !!tagList.length;
|
||||||
|
|
||||||
|
const tags = filterValue.map((id) => {
|
||||||
|
if (hasItems) {
|
||||||
|
const tag = tagList.find((t) => t.id === id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: tag && tag.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TagInput
|
||||||
|
name={NAME}
|
||||||
|
tags={tags}
|
||||||
|
tagList={tagList}
|
||||||
|
allowNew={!tagList.length}
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
delimiters={[9, 13]}
|
||||||
|
maxSuggestionsLength={100}
|
||||||
|
minQueryLength={0}
|
||||||
|
tagComponent={FilterBuilderRowValueTag}
|
||||||
|
onTagAdd={this.onTagAdd}
|
||||||
|
onTagDelete={this.onTagDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterBuilderRowValue.propTypes = {
|
||||||
|
filterValue: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired,
|
||||||
|
selectedFilterBuilderProp: PropTypes.object.isRequired,
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterBuilderRowValue;
|
|
@ -0,0 +1,50 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { filterBuilderTypes } from 'Helpers/Props';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createTagListSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { sectionItems }) => _.get(state, sectionItems),
|
||||||
|
(state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp,
|
||||||
|
(sectionItems, selectedFilterBuilderProp) => {
|
||||||
|
if (
|
||||||
|
selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
|
||||||
|
selectedFilterBuilderProp.type === filterBuilderTypes.STRING
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
|
||||||
|
if (selectedFilterBuilderProp.optionsSelector) {
|
||||||
|
items = sectionItems.map(selectedFilterBuilderProp.optionsSelector);
|
||||||
|
} else {
|
||||||
|
items = sectionItems.map((item) => {
|
||||||
|
const name = item[selectedFilterBuilderProp.name];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: name,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.uniqBy(items, 'id');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createTagListSelector(),
|
||||||
|
(tagList) => {
|
||||||
|
return {
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -0,0 +1,19 @@
|
||||||
|
.tag {
|
||||||
|
&.isLastTag {
|
||||||
|
.or {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
composes: label from 'Components/Label.css';
|
||||||
|
|
||||||
|
border-style: none;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.or {
|
||||||
|
margin: 0 3px;
|
||||||
|
color: $themeDarkColor;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import TagInputTag from 'Components/Form/TagInputTag';
|
||||||
|
import styles from './FilterBuilderRowValueTag.css';
|
||||||
|
|
||||||
|
function FilterBuilderRowValueTag(props) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={styles.tag}
|
||||||
|
>
|
||||||
|
<TagInputTag
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!props.isLastTag &&
|
||||||
|
<span className={styles.or}>
|
||||||
|
or
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterBuilderRowValueTag.propTypes = {
|
||||||
|
isLastTag: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterBuilderRowValueTag;
|
|
@ -0,0 +1,79 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||||
|
import { tagShape } from 'Components/Form/TagInput';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.indexers,
|
||||||
|
(qualityProfiles) => {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items
|
||||||
|
} = qualityProfiles;
|
||||||
|
|
||||||
|
const tagList = items.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchIndexers: fetchIndexers
|
||||||
|
};
|
||||||
|
|
||||||
|
class IndexerFilterBuilderRowValueConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
if (!this.props.isPopulated) {
|
||||||
|
this.props.dispatchFetchIndexers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterBuilderRowValue
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexerFilterBuilderRowValueConnector.propTypes = {
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerFilterBuilderRowValueConnector);
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
const protocols = [
|
||||||
|
{ id: 'torrent', name: 'Torrent' },
|
||||||
|
{ id: 'usenet', name: 'Usenet' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function ProtocolFilterBuilderRowValue(props) {
|
||||||
|
return (
|
||||||
|
<FilterBuilderRowValue
|
||||||
|
tagList={protocols}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProtocolFilterBuilderRowValue;
|
|
@ -0,0 +1,75 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import getQualities from 'Utilities/Quality/getQualities';
|
||||||
|
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||||
|
import { tagShape } from 'Components/Form/TagInput';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.qualityProfiles,
|
||||||
|
(qualityProfiles) => {
|
||||||
|
const {
|
||||||
|
isFetchingSchema: isFetching,
|
||||||
|
isSchemaPopulated: isPopulated,
|
||||||
|
schemaError: error,
|
||||||
|
schema
|
||||||
|
} = qualityProfiles;
|
||||||
|
|
||||||
|
const tagList = getQualities(schema.items);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema
|
||||||
|
};
|
||||||
|
|
||||||
|
class QualityFilterBuilderRowValueConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
if (!this.props.isPopulated) {
|
||||||
|
this.props.dispatchFetchQualityProfileSchema();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterBuilderRowValue
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityFilterBuilderRowValueConnector.propTypes = {
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(QualityFilterBuilderRowValueConnector);
|
|
@ -0,0 +1,17 @@
|
||||||
|
.customFilter {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $tableRowHoverBackgroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex: 0 1 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
flex: 0 0 60px;
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import styles from './CustomFilter.css';
|
||||||
|
|
||||||
|
class CustomFilter extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditPress = () => {
|
||||||
|
const {
|
||||||
|
customFilterKey,
|
||||||
|
onEditPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onEditPress(customFilterKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemovePress = () => {
|
||||||
|
const {
|
||||||
|
customFilterKey,
|
||||||
|
onRemovePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onRemovePress({ key: customFilterKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
label
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.customFilter}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<IconButton
|
||||||
|
name={icons.EDIT}
|
||||||
|
onPress={this.onEditPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.REMOVE}
|
||||||
|
onPress={this.onRemovePress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomFilter.propTypes = {
|
||||||
|
customFilterKey: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
onEditPress: PropTypes.func.isRequired,
|
||||||
|
onRemovePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomFilter;
|
|
@ -0,0 +1,3 @@
|
||||||
|
.addButtonContainer {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
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 CustomFilter from './CustomFilter';
|
||||||
|
import styles from './CustomFiltersModalContent.css';
|
||||||
|
|
||||||
|
function CustomFiltersModalContent(props) {
|
||||||
|
const {
|
||||||
|
customFilters,
|
||||||
|
onAddCustomFilter,
|
||||||
|
onRemoveCustomFilterPress,
|
||||||
|
onEditCustomFilter,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Custom Filters
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{
|
||||||
|
customFilters.map((customFilter, index) => {
|
||||||
|
return (
|
||||||
|
<CustomFilter
|
||||||
|
key={index}
|
||||||
|
customFilterKey={customFilter.key}
|
||||||
|
label={customFilter.label}
|
||||||
|
filters={customFilter.filters}
|
||||||
|
onRemovePress={onRemoveCustomFilterPress}
|
||||||
|
onEditPress={onEditCustomFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.addButtonContainer}>
|
||||||
|
<Button onPress={onAddCustomFilter}>
|
||||||
|
Add Custom Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomFiltersModalContent.propTypes = {
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onAddCustomFilter: PropTypes.func.isRequired,
|
||||||
|
onRemoveCustomFilterPress: PropTypes.func.isRequired,
|
||||||
|
onEditCustomFilter: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomFiltersModalContent;
|
|
@ -0,0 +1,90 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import FilterBuilderModalContentConnector from './Builder/FilterBuilderModalContentConnector';
|
||||||
|
import CustomFiltersModalContent from './CustomFilters/CustomFiltersModalContent';
|
||||||
|
|
||||||
|
class FilterModal extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
filterBuilder: !props.customFilters.length,
|
||||||
|
customFilterKey: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onAddCustomFilter = () => {
|
||||||
|
this.setState({
|
||||||
|
filterBuilder: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditCustomFilter = (customFilterKey) => {
|
||||||
|
this.setState({
|
||||||
|
filterBuilder: true,
|
||||||
|
customFilterKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.setState({
|
||||||
|
filterBuilder: false,
|
||||||
|
customFilterKey: null
|
||||||
|
}, () => {
|
||||||
|
this.props.onModalClose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
filterBuilder,
|
||||||
|
customFilterKey
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
filterBuilder ?
|
||||||
|
<FilterBuilderModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
customFilterKey={customFilterKey}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/> :
|
||||||
|
<CustomFiltersModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onAddCustomFilter={this.onAddCustomFilter}
|
||||||
|
onEditCustomFilter={this.onEditCustomFilter}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterModal;
|
|
@ -31,7 +31,6 @@
|
||||||
.isDisabled {
|
.isDisabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
pointer-events: all !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdownArrowContainer {
|
.dropdownArrowContainer {
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.pathHighlighted {
|
.pathHighlighted {
|
||||||
background-color: $menuItemHoverColor;
|
background-color: $menuItemHoverBackgroundColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileBrowserButton {
|
.fileBrowserButton {
|
||||||
|
|
|
@ -1,97 +1,77 @@
|
||||||
.container {
|
.inputContainer {
|
||||||
composes: input from 'Components/Form/Input.css';
|
composes: input from 'Components/Form/Input.css';
|
||||||
|
|
||||||
display: flex;
|
position: relative;
|
||||||
flex-wrap: wrap;
|
padding: 0;
|
||||||
min-height: 35px;
|
min-height: 35px;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
|
&.isFocused {
|
||||||
|
outline: 0;
|
||||||
|
border-color: $inputFocusBorderColor;
|
||||||
|
box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.containerFocused {
|
.hasError {
|
||||||
outline: 0;
|
composes: hasError from 'Components/Form/Input.css';
|
||||||
border-color: $inputFocusBorderColor;
|
|
||||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedTagContainer {
|
.hasWarning {
|
||||||
|
composes: hasWarning from 'Components/Form/Input.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
|
||||||
|
|
||||||
.selectedTag {
|
|
||||||
composes: label from 'Components/Label.css';
|
|
||||||
|
|
||||||
border-style: none;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Selected Tag Kinds */
|
|
||||||
|
|
||||||
.info {
|
|
||||||
composes: info from 'Components/Label.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
composes: success from 'Components/Label.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
composes: warning from 'Components/Label.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger {
|
|
||||||
composes: danger from 'Components/Label.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchInputContainer {
|
|
||||||
position: relative;
|
|
||||||
flex: 1 0 100px;
|
|
||||||
margin-top: 1px;
|
|
||||||
padding-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchInput {
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-size: 13px;
|
}
|
||||||
|
|
||||||
input {
|
.input {
|
||||||
margin: 0;
|
flex: 1 1 0%;
|
||||||
padding: 0;
|
margin-left: 3px;
|
||||||
max-width: 100%;
|
min-width: 20%;
|
||||||
outline: none;
|
max-width: 100%;
|
||||||
border: 0;
|
width: 0%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestionsContainer {
|
||||||
|
@add-mixin scrollbar;
|
||||||
|
@add-mixin scrollbarTrack;
|
||||||
|
@add-mixin scrollbarThumb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerOpen {
|
||||||
|
.suggestionsContainer {
|
||||||
|
position: absolute;
|
||||||
|
right: -1px;
|
||||||
|
left: -1px;
|
||||||
|
z-index: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 1px;
|
||||||
|
max-height: 110px;
|
||||||
|
border: 1px solid $inputBorderColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $white;
|
||||||
|
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions {
|
.suggestionsList {
|
||||||
position: absolute;
|
margin: 5px 0;
|
||||||
z-index: 1;
|
padding-left: 0;
|
||||||
overflow-y: auto;
|
list-style-type: none;
|
||||||
max-height: 200px;
|
}
|
||||||
width: 100%;
|
|
||||||
border: 1px solid $inputBorderColor;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: $white;
|
|
||||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
|
||||||
|
|
||||||
ul {
|
.suggestion {
|
||||||
margin: 5px 0;
|
padding: 0 16px;
|
||||||
padding-left: 0;
|
cursor: default;
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
&:hover {
|
||||||
padding: 0 16px;
|
background-color: $menuItemHoverBackgroundColor;
|
||||||
}
|
|
||||||
|
|
||||||
li mark {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
li:hover {
|
|
||||||
background-color: $menuItemHoverColor;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestionActive {
|
.suggestionHighlighted {
|
||||||
background-color: $menuItemHoverColor;
|
background-color: $menuItemHoverBackgroundColor;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
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 ReactTags from 'react-tag-autocomplete';
|
import Autosuggest from 'react-autosuggest';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import TagInputInput from './TagInputInput';
|
||||||
|
import TagInputTag from './TagInputTag';
|
||||||
import styles from './TagInput.css';
|
import styles from './TagInput.css';
|
||||||
|
|
||||||
|
function getTag(value, selectedIndex, suggestions, allowNew) {
|
||||||
|
if (selectedIndex == null && value) {
|
||||||
|
const existingTag = _.find(suggestions, { name: value });
|
||||||
|
|
||||||
|
if (existingTag) {
|
||||||
|
return existingTag;
|
||||||
|
} else if (allowNew) {
|
||||||
|
return { name: value };
|
||||||
|
}
|
||||||
|
} else if (selectedIndex != null) {
|
||||||
|
return suggestions[selectedIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class TagInput extends Component {
|
class TagInput extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -14,97 +30,240 @@ class TagInput extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this._tagsRef = null;
|
this.state = {
|
||||||
this._inputRef = null;
|
value: '',
|
||||||
|
suggestions: [],
|
||||||
|
isFocused: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this._autosuggestRef = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
_setTagsRef = (ref) => {
|
_setAutosuggestRef = (ref) => {
|
||||||
this._tagsRef = ref;
|
this._autosuggestRef = ref;
|
||||||
|
}
|
||||||
|
|
||||||
if (ref) {
|
getSuggestionValue({ name }) {
|
||||||
this._inputRef = this._tagsRef.input.input;
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
this._inputRef.addEventListener('blur', this.onInputBlur);
|
shouldRenderSuggestions = (value) => {
|
||||||
} else if (this._inputRef) {
|
return value.length >= this.props.minQueryLength;
|
||||||
this._inputRef.removeEventListener('blur', this.onInputBlur);
|
}
|
||||||
}
|
|
||||||
|
renderSuggestion({ name }, { query }) {
|
||||||
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
onInputContainerPress = () => {
|
||||||
|
this._autosuggestRef.input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
onTagAdd(tag) {
|
||||||
|
this.props.onTagAdd(tag);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
value: '',
|
||||||
|
suggestions: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputChange = (event, { newValue, method }) => {
|
||||||
|
const value = _.isObject(newValue) ? newValue.name : newValue;
|
||||||
|
|
||||||
|
if (method === 'type') {
|
||||||
|
this.setState({ value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputKeyDown = (event) => {
|
||||||
|
const {
|
||||||
|
tags,
|
||||||
|
allowNew,
|
||||||
|
delimiters,
|
||||||
|
onTagDelete
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
suggestions
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const keyCode = event.keyCode;
|
||||||
|
|
||||||
|
if (keyCode === 8 && !value.length) {
|
||||||
|
const index = tags.length - 1;
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
onTagDelete({ index, id: tags[index].id });
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.onSuggestionsFetchRequested({ value: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delimiters.includes(keyCode)) {
|
||||||
|
const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
|
||||||
|
const tag = getTag(value, selectedIndex, suggestions, allowNew);
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
this.onTagAdd(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputFocus = () => {
|
||||||
|
this.setState({ isFocused: true });
|
||||||
|
}
|
||||||
|
|
||||||
onInputBlur = () => {
|
onInputBlur = () => {
|
||||||
if (!this._tagsRef) {
|
this.setState({ isFocused: false });
|
||||||
|
|
||||||
|
if (!this._autosuggestRef) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
tagList,
|
|
||||||
allowNew
|
allowNew
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const query = this._tagsRef.state.query.trim();
|
const {
|
||||||
|
value,
|
||||||
|
suggestions
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
if (query) {
|
const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
|
||||||
const existingTag = _.find(tagList, { name: query });
|
const tag = getTag(value, selectedIndex, suggestions, allowNew);
|
||||||
|
|
||||||
if (existingTag) {
|
if (tag) {
|
||||||
this._tagsRef.addTag(existingTag);
|
this.onTagAdd(tag);
|
||||||
} else if (allowNew) {
|
|
||||||
this._tagsRef.addTag({ name: query });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSuggestionsFetchRequested = ({ value }) => {
|
||||||
|
const lowerCaseValue = value.toLowerCase();
|
||||||
|
|
||||||
|
const {
|
||||||
|
tags,
|
||||||
|
tagList
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const suggestions = tagList.filter((tag) => {
|
||||||
|
return (
|
||||||
|
tag.name.toLowerCase().includes(lowerCaseValue) &&
|
||||||
|
!tags.some((t) => t.id === tag.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({ suggestions });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuggestionsClearRequested = () => {
|
||||||
|
// Required because props aren't always rendered, but no-op
|
||||||
|
// because we don't want to reset the paths after a path is selected.
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuggestionSelected = (event, { suggestion }) => {
|
||||||
|
this.onTagAdd(suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
renderInputComponent = (inputProps) => {
|
||||||
const {
|
const {
|
||||||
tags,
|
tags,
|
||||||
tagList,
|
|
||||||
allowNew,
|
|
||||||
kind,
|
kind,
|
||||||
placeholder,
|
tagComponent,
|
||||||
onTagAdd,
|
|
||||||
onTagDelete
|
onTagDelete
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const tagInputClassNames = {
|
return (
|
||||||
root: styles.container,
|
<TagInputInput
|
||||||
rootFocused: styles.containerFocused,
|
tags={tags}
|
||||||
selected: styles.selectedTagContainer,
|
kind={kind}
|
||||||
selectedTag: classNames(styles.selectedTag, styles[kind]),
|
inputProps={inputProps}
|
||||||
search: styles.searchInputContainer,
|
isFocused={this.state.isFocused}
|
||||||
searchInput: styles.searchInput,
|
tagComponent={tagComponent}
|
||||||
suggestions: styles.suggestions,
|
onTagDelete={onTagDelete}
|
||||||
suggestionActive: styles.suggestionActive,
|
onInputContainerPress={this.onInputContainerPress}
|
||||||
suggestionDisabled: styles.suggestionDisabled
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
placeholder,
|
||||||
|
hasError,
|
||||||
|
hasWarning
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
suggestions,
|
||||||
|
isFocused
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const inputProps = {
|
||||||
|
className: styles.input,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
autoComplete: 'off',
|
||||||
|
spellCheck: false,
|
||||||
|
onChange: this.onInputChange,
|
||||||
|
onKeyDown: this.onInputKeyDown,
|
||||||
|
onFocus: this.onInputFocus,
|
||||||
|
onBlur: this.onInputBlur
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
container: classNames(
|
||||||
|
styles.inputContainer,
|
||||||
|
isFocused && styles.isFocused,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning,
|
||||||
|
),
|
||||||
|
containerOpen: styles.containerOpen,
|
||||||
|
suggestionsContainer: styles.suggestionsContainer,
|
||||||
|
suggestionsList: styles.suggestionsList,
|
||||||
|
suggestion: styles.suggestion,
|
||||||
|
suggestionHighlighted: styles.suggestionHighlighted
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactTags
|
<Autosuggest
|
||||||
ref={this._setTagsRef}
|
ref={this._setAutosuggestRef}
|
||||||
classNames={tagInputClassNames}
|
id={name}
|
||||||
tags={tags}
|
inputProps={inputProps}
|
||||||
suggestions={tagList}
|
theme={theme}
|
||||||
allowNew={allowNew}
|
suggestions={suggestions}
|
||||||
minQueryLength={1}
|
getSuggestionValue={this.getSuggestionValue}
|
||||||
placeholder={placeholder}
|
shouldRenderSuggestions={this.shouldRenderSuggestions}
|
||||||
delimiters={[9, 13, 32, 188]}
|
focusInputOnSuggestionClick={false}
|
||||||
handleAddition={onTagAdd}
|
renderSuggestion={this.renderSuggestion}
|
||||||
handleDelete={onTagDelete}
|
renderInputComponent={this.renderInputComponent}
|
||||||
|
onSuggestionSelected={this.onSuggestionSelected}
|
||||||
|
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagShape = {
|
export const tagShape = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||||
name: PropTypes.string.isRequired
|
name: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
TagInput.propTypes = {
|
TagInput.propTypes = {
|
||||||
|
@ -113,6 +272,11 @@ TagInput.propTypes = {
|
||||||
allowNew: PropTypes.bool.isRequired,
|
allowNew: PropTypes.bool.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
placeholder: PropTypes.string.isRequired,
|
placeholder: PropTypes.string.isRequired,
|
||||||
|
delimiters: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
minQueryLength: PropTypes.number.isRequired,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
hasWarning: PropTypes.bool,
|
||||||
|
tagComponent: PropTypes.func.isRequired,
|
||||||
onTagAdd: PropTypes.func.isRequired,
|
onTagAdd: PropTypes.func.isRequired,
|
||||||
onTagDelete: PropTypes.func.isRequired
|
onTagDelete: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
@ -120,7 +284,11 @@ TagInput.propTypes = {
|
||||||
TagInput.defaultProps = {
|
TagInput.defaultProps = {
|
||||||
allowNew: true,
|
allowNew: true,
|
||||||
kind: kinds.INFO,
|
kind: kinds.INFO,
|
||||||
placeholder: ''
|
placeholder: '',
|
||||||
|
// Tab, enter, space and comma
|
||||||
|
delimiters: [9, 13, 32, 188],
|
||||||
|
minQueryLength: 1,
|
||||||
|
tagComponent: TagInputTag
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagInput;
|
export default TagInput;
|
||||||
|
|
|
@ -103,7 +103,7 @@ class TagInputConnector extends Component {
|
||||||
this.props.onChange({ name, value: newValue });
|
this.props.onChange({ name, value: newValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
onTagDelete = (index) => {
|
onTagDelete = ({ index }) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
value
|
value
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
.inputContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 6px 16px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { tagShape } from './TagInput';
|
||||||
|
import styles from './TagInputInput.css';
|
||||||
|
|
||||||
|
class TagInputInput extends Component {
|
||||||
|
|
||||||
|
onMouseDown = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFocused,
|
||||||
|
onInputContainerPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (isFocused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputContainerPress();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
tags,
|
||||||
|
inputProps,
|
||||||
|
kind,
|
||||||
|
tagComponent: TagComponent,
|
||||||
|
onTagDelete
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
component="div"
|
||||||
|
onMouseDown={this.onMouseDown}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
tags.map((tag, index) => {
|
||||||
|
return (
|
||||||
|
<TagComponent
|
||||||
|
key={tag.id}
|
||||||
|
index={index}
|
||||||
|
tag={tag}
|
||||||
|
kind={kind}
|
||||||
|
isLastTag={index === tags.length - 1}
|
||||||
|
onDelete={onTagDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<input {...inputProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagInputInput.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
inputProps: PropTypes.object.isRequired,
|
||||||
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
|
isFocused: PropTypes.bool.isRequired,
|
||||||
|
tagComponent: PropTypes.func.isRequired,
|
||||||
|
onTagDelete: PropTypes.func.isRequired,
|
||||||
|
onInputContainerPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
TagInputInput.defaultProps = {
|
||||||
|
className: styles.inputContainer
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagInputInput;
|
|
@ -0,0 +1,52 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { tagShape } from './TagInput';
|
||||||
|
|
||||||
|
class TagInputTag extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onDelete = () => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
tag,
|
||||||
|
onDelete
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onDelete({
|
||||||
|
index,
|
||||||
|
id: tag.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
tag,
|
||||||
|
kind
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link onPress={this.onDelete}>
|
||||||
|
<Label kind={kind}>
|
||||||
|
{tag.name}
|
||||||
|
</Label>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagInputTag.propTypes = {
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
tag: PropTypes.shape(tagShape),
|
||||||
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
|
onDelete: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagInputTag;
|
|
@ -1,68 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import ReactTags from 'react-tag-autocomplete';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import styles from './TagInput.css';
|
|
||||||
|
|
||||||
class TextTagInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
tags,
|
|
||||||
allowNew,
|
|
||||||
kind,
|
|
||||||
placeholder,
|
|
||||||
onTagAdd,
|
|
||||||
onTagDelete
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const tagInputClassNames = {
|
|
||||||
root: styles.container,
|
|
||||||
rootFocused: styles.containerFocused,
|
|
||||||
selected: styles.selectedTagContainer,
|
|
||||||
selectedTag: classNames(styles.selectedTag, styles[kind]),
|
|
||||||
search: styles.searchInputContainer,
|
|
||||||
searchInput: styles.searchInput,
|
|
||||||
suggestions: styles.suggestions,
|
|
||||||
suggestionActive: styles.suggestionActive,
|
|
||||||
suggestionDisabled: styles.suggestionDisabled
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReactTags
|
|
||||||
classNames={tagInputClassNames}
|
|
||||||
tags={tags}
|
|
||||||
allowNew={allowNew}
|
|
||||||
minQueryLength={1}
|
|
||||||
placeholder={placeholder}
|
|
||||||
handleAddition={onTagAdd}
|
|
||||||
handleDelete={onTagDelete}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagShape = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
TextTagInput.propTypes = {
|
|
||||||
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
|
||||||
allowNew: PropTypes.bool.isRequired,
|
|
||||||
kind: PropTypes.string.isRequired,
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
onTagAdd: PropTypes.func.isRequired,
|
|
||||||
onTagDelete: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
TextTagInput.defaultProps = {
|
|
||||||
allowNew: true,
|
|
||||||
kind: kinds.INFO
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TextTagInput;
|
|
|
@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import split from 'Utilities/String/split';
|
import split from 'Utilities/String/split';
|
||||||
import TextTagInput from './TextTagInput';
|
import TagInput from './TagInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
@ -34,25 +34,27 @@ class TextTagInputConnector extends Component {
|
||||||
onTagAdd = (tag) => {
|
onTagAdd = (tag) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
value
|
value,
|
||||||
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const newValue = split(value);
|
const newValue = split(value);
|
||||||
newValue.push(tag.name);
|
newValue.push(tag.name);
|
||||||
|
|
||||||
this.props.onChange({ name, value: newValue.join(',') });
|
onChange({ name, value: newValue.join(',') });
|
||||||
}
|
}
|
||||||
|
|
||||||
onTagDelete = (index) => {
|
onTagDelete = ({ index }) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
value
|
value,
|
||||||
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const newValue = split(value);
|
const newValue = split(value);
|
||||||
newValue.splice(index, 1);
|
newValue.splice(index, 1);
|
||||||
|
|
||||||
this.props.onChange({
|
onChange({
|
||||||
name,
|
name,
|
||||||
value: newValue.join(',')
|
value: newValue.join(',')
|
||||||
});
|
});
|
||||||
|
@ -63,7 +65,8 @@ class TextTagInputConnector extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<TextTagInput
|
<TagInput
|
||||||
|
tagList={[]}
|
||||||
onTagAdd={this.onTagAdd}
|
onTagAdd={this.onTagAdd}
|
||||||
onTagDelete={this.onTagDelete}
|
onTagDelete={this.onTagDelete}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
|
|
|
@ -13,4 +13,8 @@
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
color: $iconButtonHoverColor;
|
color: $iconButtonHoverColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.isDisabled {
|
||||||
|
color: $iconButtonDisabledColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from './Link';
|
import Link from './Link';
|
||||||
import styles from './IconButton.css';
|
import styles from './IconButton.css';
|
||||||
|
@ -12,12 +13,18 @@ function IconButton(props) {
|
||||||
kind,
|
kind,
|
||||||
size,
|
size,
|
||||||
isSpinning,
|
isSpinning,
|
||||||
|
isDisabled,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className={className}
|
className={classNames(
|
||||||
|
className,
|
||||||
|
isDisabled && styles.isDisabled
|
||||||
|
)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -37,7 +44,8 @@ IconButton.propTypes = {
|
||||||
kind: PropTypes.string,
|
kind: PropTypes.string,
|
||||||
name: PropTypes.object.isRequired,
|
name: PropTypes.object.isRequired,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
isSpinning: PropTypes.bool
|
isSpinning: PropTypes.bool,
|
||||||
|
isDisabled: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
IconButton.defaultProps = {
|
IconButton.defaultProps = {
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:global(.isDisabled) {
|
&:global(.isDisabled) {
|
||||||
pointer-events: none;
|
cursor: default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,42 +1,107 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Menu from 'Components/Menu/Menu';
|
import FilterMenuContent from './FilterMenuContent';
|
||||||
import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton';
|
import Menu from './Menu';
|
||||||
|
import ToolbarMenuButton from './ToolbarMenuButton';
|
||||||
import styles from './FilterMenu.css';
|
import styles from './FilterMenu.css';
|
||||||
|
|
||||||
function FilterMenu(props) {
|
class FilterMenu extends Component {
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
isDisabled,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
//
|
||||||
<Menu
|
// Lifecycle
|
||||||
className={className}
|
|
||||||
{...otherProps}
|
constructor(props, context) {
|
||||||
>
|
super(props, context);
|
||||||
<ToolbarMenuButton
|
|
||||||
iconName={icons.FILTER}
|
this.state = {
|
||||||
text="Filter"
|
isFilterModalOpen: false
|
||||||
isDisabled={isDisabled}
|
};
|
||||||
/>
|
}
|
||||||
{children}
|
|
||||||
</Menu>
|
//
|
||||||
);
|
// Listeners
|
||||||
|
|
||||||
|
onCustomFiltersPress = () => {
|
||||||
|
this.setState({ isFilterModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onFiltersModalClose = () => {
|
||||||
|
this.setState({ isFilterModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render(props) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
isDisabled,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
buttonComponent: ButtonComponent,
|
||||||
|
filterModalConnectorComponent: FilterModalConnectorComponent,
|
||||||
|
onFilterSelect,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const showCustomFilters = !!FilterModalConnectorComponent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Menu
|
||||||
|
className={className}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<ButtonComponent
|
||||||
|
iconName={icons.FILTER}
|
||||||
|
text="Filter"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FilterMenuContent
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
showCustomFilters={showCustomFilters}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
onCustomFiltersPress={this.onCustomFiltersPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
{
|
||||||
|
showCustomFilters &&
|
||||||
|
<FilterModalConnectorComponent
|
||||||
|
isOpen={this.state.isFilterModalOpen}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
onModalClose={this.onFiltersModalClose}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterMenu.propTypes = {
|
FilterMenu.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
children: PropTypes.node.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
buttonComponent: PropTypes.func.isRequired,
|
||||||
|
filterModalConnectorComponent: PropTypes.func,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
FilterMenu.defaultProps = {
|
FilterMenu.defaultProps = {
|
||||||
className: styles.filterMenu,
|
className: styles.filterMenu,
|
||||||
isDisabled: false
|
isDisabled: false,
|
||||||
|
buttonComponent: ToolbarMenuButton
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FilterMenu;
|
export default FilterMenu;
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import MenuContent from './MenuContent';
|
||||||
|
import FilterMenuItem from './FilterMenuItem';
|
||||||
|
import MenuItem from './MenuItem';
|
||||||
|
import MenuItemSeparator from './MenuItemSeparator';
|
||||||
|
|
||||||
|
class FilterMenuContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
showCustomFilters,
|
||||||
|
onFilterSelect,
|
||||||
|
onCustomFiltersPress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuContent {...otherProps}>
|
||||||
|
{
|
||||||
|
filters.map((filter) => {
|
||||||
|
return (
|
||||||
|
<FilterMenuItem
|
||||||
|
key={filter.key}
|
||||||
|
filterKey={filter.key}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
onPress={onFilterSelect}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</FilterMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
customFilters.map((filter) => {
|
||||||
|
return (
|
||||||
|
<FilterMenuItem
|
||||||
|
key={filter.key}
|
||||||
|
filterKey={filter.key}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
onPress={onFilterSelect}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</FilterMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showCustomFilters &&
|
||||||
|
<MenuItemSeparator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showCustomFilters &&
|
||||||
|
<MenuItem onPress={onCustomFiltersPress}>
|
||||||
|
Custom Filters
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
|
</MenuContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterMenuContent.propTypes = {
|
||||||
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
showCustomFilters: PropTypes.bool.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
onCustomFiltersPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
FilterMenuContent.defaultProps = {
|
||||||
|
showCustomFilters: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterMenuContent;
|
|
@ -9,12 +9,11 @@ class FilterMenuItem extends Component {
|
||||||
|
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
const {
|
const {
|
||||||
name,
|
filterKey,
|
||||||
value,
|
|
||||||
onPress
|
onPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
onPress(name, value);
|
onPress(filterKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -22,18 +21,14 @@ class FilterMenuItem extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
name,
|
|
||||||
value,
|
|
||||||
filterKey,
|
filterKey,
|
||||||
filterValue,
|
selectedFilterKey,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isSelected = name === filterKey && value === filterValue;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectedMenuItem
|
<SelectedMenuItem
|
||||||
isSelected={isSelected}
|
isSelected={filterKey === selectedFilterKey}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
/>
|
/>
|
||||||
|
@ -42,16 +37,9 @@ class FilterMenuItem extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterMenuItem.propTypes = {
|
FilterMenuItem.propTypes = {
|
||||||
name: PropTypes.string,
|
filterKey: PropTypes.string.isRequired,
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
filterKey: PropTypes.string,
|
|
||||||
filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
|
||||||
onPress: PropTypes.func.isRequired
|
onPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
FilterMenuItem.defaultProps = {
|
|
||||||
name: null,
|
|
||||||
value: null
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FilterMenuItem;
|
export default FilterMenuItem;
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
.separator {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 1px;
|
||||||
|
background-color: $themeDarkColor;
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './MenuItemSeparator.css';
|
||||||
|
|
||||||
|
function MenuItemSeparator() {
|
||||||
|
return (
|
||||||
|
<div className={styles.separator} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MenuItemSeparator;
|
|
@ -0,0 +1,11 @@
|
||||||
|
.menuButton {
|
||||||
|
composes: menuButton from './MenuButton.css';
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import MenuButton from 'Components/Menu/MenuButton';
|
||||||
|
import styles from './PageMenuButton.css';
|
||||||
|
|
||||||
|
function PageMenuButton(props) {
|
||||||
|
const {
|
||||||
|
iconName,
|
||||||
|
text,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuButton
|
||||||
|
className={styles.menuButton}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={iconName}
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.label}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</MenuButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PageMenuButton.propTypes = {
|
||||||
|
iconName: PropTypes.object.isRequired,
|
||||||
|
text: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageMenuButton;
|
|
@ -5,9 +5,7 @@
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabledButton {
|
.isDisabled {
|
||||||
composes: button from 'Components/Link/IconButton.css';
|
|
||||||
|
|
||||||
color: $disabledColor;
|
color: $disabledColor;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,22 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
import styles from './MonitorToggleButton.css';
|
import styles from './MonitorToggleButton.css';
|
||||||
|
|
||||||
|
function getTooltip(monitored, isDisabled) {
|
||||||
|
if (isDisabled) {
|
||||||
|
return 'Cannot toogle monitored state when artist is unmonitored';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitored) {
|
||||||
|
return 'Monitored, click to unmonitor';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unmonitored, click to monitor';
|
||||||
|
}
|
||||||
|
|
||||||
class MonitorToggleButton extends Component {
|
class MonitorToggleButton extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -29,27 +41,18 @@ class MonitorToggleButton extends Component {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const monitoredMessage = 'Monitored, click to unmonitor';
|
|
||||||
const unmonitoredMessage = 'Unmonitored, click to monitor';
|
|
||||||
const iconName = monitored ? icons.MONITORED : icons.UNMONITORED;
|
const iconName = monitored ? icons.MONITORED : icons.UNMONITORED;
|
||||||
|
|
||||||
if (isDisabled) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
className={styles.disabledButton}
|
|
||||||
size={size}
|
|
||||||
name={iconName}
|
|
||||||
title="Cannot toogle monitored state when artist is unmonitored"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SpinnerIconButton
|
<SpinnerIconButton
|
||||||
className={className}
|
className={classNames(
|
||||||
|
className,
|
||||||
|
isDisabled && styles.isDisabled
|
||||||
|
)}
|
||||||
name={iconName}
|
name={iconName}
|
||||||
size={size}
|
size={size}
|
||||||
title={monitored ? monitoredMessage : unmonitoredMessage}
|
title={getTooltip(monitored, isDisabled)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
isSpinning={isSaving}
|
isSpinning={isSaving}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
|
|
||||||
.icon {
|
|
||||||
line-height: 24px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
|
|
|
@ -155,7 +155,7 @@ class ArtistSearchInput extends Component {
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuggestionSelected = (event, { suggestion, sectionIndex }) => {
|
onSuggestionSelected = (event, { suggestion }) => {
|
||||||
if (suggestion.type === ADD_NEW_TYPE) {
|
if (suggestion.type === ADD_NEW_TYPE) {
|
||||||
this.props.onGoToAddNewArtist(this.state.value);
|
this.props.onGoToAddNewArtist(this.state.value);
|
||||||
} else {
|
} else {
|
||||||
|
@ -181,7 +181,7 @@ class ArtistSearchInput extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestions.length <= 3) {
|
if (value.length >= 3) {
|
||||||
suggestionGroups.push({
|
suggestionGroups.push({
|
||||||
title: 'Add New Artist',
|
title: 'Add New Artist',
|
||||||
suggestions: [
|
suggestions: [
|
||||||
|
@ -218,10 +218,7 @@ class ArtistSearchInput extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<Icon
|
<Icon name={icons.SEARCH} />
|
||||||
className={styles.icon}
|
|
||||||
name={icons.SEARCH}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Autosuggest
|
<Autosuggest
|
||||||
ref={this.setAutosuggestRef}
|
ref={this.setAutosuggestRef}
|
||||||
|
|
|
@ -13,12 +13,6 @@
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
|
||||||
overflow: hidden;
|
|
||||||
height: 1px;
|
|
||||||
background-color: $themeDarkColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.menuButton {
|
.menuButton {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Menu from 'Components/Menu/Menu';
|
||||||
import MenuButton from 'Components/Menu/MenuButton';
|
import MenuButton from 'Components/Menu/MenuButton';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
import MenuItem from 'Components/Menu/MenuItem';
|
import MenuItem from 'Components/Menu/MenuItem';
|
||||||
|
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||||
import styles from './PageHeaderActionsMenu.css';
|
import styles from './PageHeaderActionsMenu.css';
|
||||||
|
|
||||||
function PageHeaderActionsMenu(props) {
|
function PageHeaderActionsMenu(props) {
|
||||||
|
@ -34,7 +35,7 @@ function PageHeaderActionsMenu(props) {
|
||||||
Keyboard Shortcuts
|
Keyboard Shortcuts
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<div className={styles.separator} />
|
<MenuItemSeparator />
|
||||||
|
|
||||||
<MenuItem onPress={onRestartPress}>
|
<MenuItem onPress={onRestartPress}>
|
||||||
<Icon
|
<Icon
|
||||||
|
|
|
@ -336,7 +336,7 @@ class PageSidebar extends Component {
|
||||||
|
|
||||||
if (isSidebarVisible && (touchStartX > 210 || touchStartX < 180)) {
|
if (isSidebarVisible && (touchStartX > 210 || touchStartX < 180)) {
|
||||||
return;
|
return;
|
||||||
} else if (!isSidebarVisible && touchStartX > 30) {
|
} else if (!isSidebarVisible && touchStartX > 40) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,22 +347,29 @@ class PageSidebar extends Component {
|
||||||
onTouchMove = (event) => {
|
onTouchMove = (event) => {
|
||||||
const touches = event.touches;
|
const touches = event.touches;
|
||||||
const currentTouchX = touches[0].pageX;
|
const currentTouchX = touches[0].pageX;
|
||||||
const currentTouchY = touches[0].pageY;
|
// const currentTouchY = touches[0].pageY;
|
||||||
|
// const isSidebarVisible = this.props.isSidebarVisible;
|
||||||
|
|
||||||
if (!this._touchStartX) {
|
if (!this._touchStartX) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.abs(this._touchStartY - currentTouchY) > 20) {
|
// This is a bit funky when trying to close and you scroll
|
||||||
this.setState({
|
// vertical too much by mistake, commenting out for now.
|
||||||
transition: 'none',
|
// TODO: Evaluate if this should be nuked
|
||||||
transform: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
// if (Math.abs(this._touchStartY - currentTouchY) > 40) {
|
||||||
}
|
// const transform = isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1;
|
||||||
|
|
||||||
if (Math.abs(this._touchStartX - currentTouchX) < 20) {
|
// this.setState({
|
||||||
|
// transition: 'none',
|
||||||
|
// transform
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (Math.abs(this._touchStartX - currentTouchX) < 40) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $toobarButtonHoverColor;
|
color: $toobarButtonHoverColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.isDisabled {
|
||||||
|
color: $disabledColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.isDisabled {
|
.isDisabled {
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
transition: background-color 500ms;
|
transition: background-color 500ms;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #fafbfc;
|
background-color: $tableRowHoverBackgroundColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,16 @@ class VirtualTable extends Component {
|
||||||
this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody);
|
this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, preState) {
|
||||||
|
const scrollIndex = this.props.scrollIndex;
|
||||||
|
|
||||||
|
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
||||||
|
const scrollTop = (scrollIndex + 1) * ROW_HEIGHT + 20;
|
||||||
|
|
||||||
|
this.props.onScroll({ scrollTop });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
|
@ -57,12 +67,6 @@ class VirtualTable extends Component {
|
||||||
return this.props.items[index];
|
return this.props.items[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToRow = (rowIndex) => {
|
|
||||||
const scrollTop = (rowIndex + 1) * ROW_HEIGHT + 20;
|
|
||||||
|
|
||||||
this.props.onScroll({ scrollTop });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
@ -144,6 +148,7 @@ VirtualTable.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
scrollTop: PropTypes.number.isRequired,
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
scrollIndex: PropTypes.number,
|
||||||
contentBody: PropTypes.object.isRequired,
|
contentBody: PropTypes.object.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
header: PropTypes.node.isRequired,
|
header: PropTypes.node.isRequired,
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import * as filterTypes from './filterTypes';
|
||||||
|
|
||||||
|
export const EXACT = 'exact';
|
||||||
|
export const NUMBER = 'number';
|
||||||
|
export const STRING = 'string';
|
||||||
|
|
||||||
|
export const all = [
|
||||||
|
EXACT,
|
||||||
|
NUMBER,
|
||||||
|
STRING
|
||||||
|
];
|
||||||
|
|
||||||
|
export const possibleFilterTypes = {
|
||||||
|
[EXACT]: [
|
||||||
|
{ key: filterTypes.EQUAL, value: 'Is' },
|
||||||
|
{ key: filterTypes.NOT_EQUAL, value: 'Is Not' }
|
||||||
|
],
|
||||||
|
|
||||||
|
[NUMBER]: [
|
||||||
|
{ key: filterTypes.EQUAL, value: 'Equal' },
|
||||||
|
{ key: filterTypes.GREATER_THAN, value: 'Greater Than' },
|
||||||
|
{ key: filterTypes.GREATER_THAN_OR_EQUAL, value: 'Greater Than or Equal' },
|
||||||
|
{ key: filterTypes.LESS_THAN, value: 'Less Than' },
|
||||||
|
{ key: filterTypes.LESS_THAN_OR_EQUAL, value: 'Less Than or Equal' },
|
||||||
|
{ key: filterTypes.NOT_EQUAL, value: 'Not Equal' }
|
||||||
|
],
|
||||||
|
|
||||||
|
[STRING]: [
|
||||||
|
{ key: filterTypes.CONTAINS, value: 'Contains' },
|
||||||
|
{ key: filterTypes.EQUAL, value: 'Equal' },
|
||||||
|
{ key: filterTypes.NOT_EQUAL, value: 'Not Equal' }
|
||||||
|
]
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const DEFAULT = 'default';
|
||||||
|
export const INDEXER = 'indexer';
|
||||||
|
export const PROTOCOL = 'protocol';
|
||||||
|
export const QUALITY = 'quality';
|
|
@ -1,5 +1,7 @@
|
||||||
import * as align from './align';
|
import * as align from './align';
|
||||||
import * as inputTypes from './inputTypes';
|
import * as inputTypes from './inputTypes';
|
||||||
|
import * as filterBuilderTypes from './filterBuilderTypes';
|
||||||
|
import * as filterBuilderValueTypes from './filterBuilderValueTypes';
|
||||||
import * as filterTypes from './filterTypes';
|
import * as filterTypes from './filterTypes';
|
||||||
import * as icons from './icons';
|
import * as icons from './icons';
|
||||||
import * as kinds from './kinds';
|
import * as kinds from './kinds';
|
||||||
|
@ -12,6 +14,8 @@ import * as tooltipPositions from './tooltipPositions';
|
||||||
export {
|
export {
|
||||||
align,
|
align,
|
||||||
inputTypes,
|
inputTypes,
|
||||||
|
filterBuilderTypes,
|
||||||
|
filterBuilderValueTypes,
|
||||||
filterTypes,
|
filterTypes,
|
||||||
icons,
|
icons,
|
||||||
kinds,
|
kinds,
|
||||||
|
|
|
@ -83,6 +83,22 @@ class MediaManagement extends Component {
|
||||||
{...settings.createEmptyArtistFolders}
|
{...settings.createEmptyArtistFolders}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<FormLabel>Delete empty folders</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="deleteEmptyFolders"
|
||||||
|
helpText="Delete empty artist and album folders during disk scan and when track files are deleted"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...settings.deleteEmptyFolders}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,14 +1,11 @@
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
import { filterTypes } from 'Helpers/Props';
|
|
||||||
|
|
||||||
function createSetClientSideCollectionFilterReducer(section) {
|
function createSetClientSideCollectionFilterReducer(section) {
|
||||||
return (state, { payload }) => {
|
return (state, { payload }) => {
|
||||||
const newState = getSectionState(state, section);
|
const newState = getSectionState(state, section);
|
||||||
|
|
||||||
newState.filterKey = payload.filterKey;
|
newState.selectedFilterKey = payload.selectedFilterKey;
|
||||||
newState.filterValue = payload.filterValue;
|
|
||||||
newState.filterType = payload.filterType || filterTypes.EQUAL;
|
|
||||||
|
|
||||||
return updateSectionState(state, section, newState);
|
return updateSectionState(state, section, newState);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,40 +2,40 @@ import $ from 'jquery';
|
||||||
import updateAlbums from 'Utilities/Album/updateAlbums';
|
import updateAlbums from 'Utilities/Album/updateAlbums';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
|
||||||
function createBatchToggleAlbumMonitoredHandler(section) {
|
function createBatchToggleAlbumMonitoredHandler(section, fetchHandler) {
|
||||||
return function(payload) {
|
return function(getState, payload, dispatch) {
|
||||||
return function(dispatch, getState) {
|
const {
|
||||||
const {
|
albumIds,
|
||||||
albumIds,
|
monitored
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const state = getSectionState(getState(), section, true);
|
||||||
|
|
||||||
|
dispatch(updateAlbums(section, state.items, albumIds, {
|
||||||
|
isSaving: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promise = $.ajax({
|
||||||
|
url: '/album/monitor',
|
||||||
|
method: 'PUT',
|
||||||
|
data: JSON.stringify({ albumIds, monitored }),
|
||||||
|
dataType: 'json'
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.done(() => {
|
||||||
|
dispatch(updateAlbums(section, state.items, albumIds, {
|
||||||
|
isSaving: false,
|
||||||
monitored
|
monitored
|
||||||
} = payload;
|
}));
|
||||||
|
|
||||||
const state = getSectionState(getState(), section, true);
|
dispatch(fetchHandler());
|
||||||
|
});
|
||||||
|
|
||||||
updateAlbums(dispatch, section, state.items, albumIds, {
|
promise.fail(() => {
|
||||||
isSaving: true
|
dispatch(updateAlbums(section, state.items, albumIds, {
|
||||||
});
|
isSaving: false
|
||||||
|
}));
|
||||||
const promise = $.ajax({
|
});
|
||||||
url: '/album/monitor',
|
|
||||||
method: 'PUT',
|
|
||||||
data: JSON.stringify({ albumIds, monitored }),
|
|
||||||
dataType: 'json'
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.done(() => {
|
|
||||||
updateAlbums(dispatch, section, state.items, albumIds, {
|
|
||||||
isSaving: false,
|
|
||||||
monitored
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail(() => {
|
|
||||||
updateAlbums(dispatch, section, state.items, albumIds, {
|
|
||||||
isSaving: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
import { set, updateServerSideCollection } from '../baseActions';
|
import { set, updateServerSideCollection } from '../baseActions';
|
||||||
|
|
||||||
|
@ -15,11 +16,21 @@ function createFetchServerSideCollectionHandler(section, url) {
|
||||||
_.pick(sectionState, [
|
_.pick(sectionState, [
|
||||||
'pageSize',
|
'pageSize',
|
||||||
'sortDirection',
|
'sortDirection',
|
||||||
'sortKey',
|
'sortKey'
|
||||||
'filterKey',
|
|
||||||
'filterValue'
|
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters
|
||||||
|
} = sectionState;
|
||||||
|
|
||||||
|
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
||||||
|
|
||||||
|
selectedFilters.forEach((filter) => {
|
||||||
|
data[filter.key] = filter.value;
|
||||||
|
});
|
||||||
|
|
||||||
const promise = $.ajax({
|
const promise = $.ajax({
|
||||||
url,
|
url,
|
||||||
data
|
data
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import updateAlbums from 'Utilities/Album/updateAlbums';
|
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
|
||||||
|
|
||||||
function createToggleAlbumMonitoredHandler(section) {
|
|
||||||
return function(payload) {
|
|
||||||
return function(dispatch, getState) {
|
|
||||||
const {
|
|
||||||
albumId,
|
|
||||||
monitored
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
const state = getSectionState(getState(), section, true);
|
|
||||||
|
|
||||||
updateAlbums(dispatch, section, state.items, [albumId], {
|
|
||||||
isSaving: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const promise = $.ajax({
|
|
||||||
url: `/album/${albumId}`,
|
|
||||||
method: 'PUT',
|
|
||||||
data: JSON.stringify({ monitored }),
|
|
||||||
dataType: 'json'
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.done(() => {
|
|
||||||
updateAlbums(dispatch, section, state.items, [albumId], {
|
|
||||||
isSaving: false,
|
|
||||||
monitored
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail(() => {
|
|
||||||
updateAlbums(dispatch, section, state.items, [albumId], {
|
|
||||||
isSaving: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createToggleAlbumMonitoredHandler;
|
|
|
@ -46,10 +46,9 @@ export const actionHandlers = handleThunks({
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
page: 1,
|
page: 1,
|
||||||
filterKey: 'albumId',
|
|
||||||
filterValue: payload.albumId,
|
|
||||||
sortKey: 'date',
|
sortKey: 'date',
|
||||||
sortDirection: sortDirections.DESCENDING
|
sortDirection: sortDirections.DESCENDING,
|
||||||
|
albumId: payload.albumId
|
||||||
};
|
};
|
||||||
|
|
||||||
const promise = $.ajax({
|
const promise = $.ajax({
|
||||||
|
|
|
@ -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 { filterTypes, sortDirections } from 'Helpers/Props';
|
import { 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';
|
||||||
|
@ -25,17 +25,17 @@ export const defaultState = {
|
||||||
sortDirection: sortDirections.ASCENDING,
|
sortDirection: sortDirections.ASCENDING,
|
||||||
secondarySortKey: 'sortName',
|
secondarySortKey: 'sortName',
|
||||||
secondarySortDirection: sortDirections.ASCENDING,
|
secondarySortDirection: sortDirections.ASCENDING,
|
||||||
filterKey: null,
|
selectedFilterKey: 'all',
|
||||||
filterValue: null,
|
// filters come from artistActions
|
||||||
filterType: filterTypes.EQUAL
|
customFilters: []
|
||||||
|
// filterPredicates come from artistActions
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
'albumStudio.sortKey',
|
'albumStudio.sortKey',
|
||||||
'albumStudio.sortDirection',
|
'albumStudio.sortDirection',
|
||||||
'albumStudio.filterKey',
|
'albumStudio.selectedFilterKey',
|
||||||
'albumStudio.filterValue',
|
'albumStudio.customFilters'
|
||||||
'albumStudio.filterType'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue