1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-10-29 23:12:39 +01:00

Mass Editor size and options

New: Option to show size in Mass Editor
New: Size on Disk in Mass Editor custom filters

Closes #3273
This commit is contained in:
Mark McDowall 2020-05-30 13:56:28 -07:00
parent 3586d7042b
commit 069fc5cd33
7 changed files with 336 additions and 190 deletions

View File

@ -3,13 +3,16 @@ import React, { Component } from 'react';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, sortDirections } from 'Helpers/Props';
import { align, icons, sortDirections } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import FilterMenu from 'Components/Menu/FilterMenu';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import NoSeries from 'Series/NoSeries';
@ -18,58 +21,6 @@ import SeriesEditorRowConnector from './SeriesEditorRowConnector';
import SeriesEditorFooter from './SeriesEditorFooter';
import SeriesEditorFilterModalConnector from './SeriesEditorFilterModalConnector';
function getColumns(showLanguageProfile) {
return [
{
name: 'status',
isSortable: true,
isVisible: true
},
{
name: 'sortTitle',
label: 'Title',
isSortable: true,
isVisible: true
},
{
name: 'qualityProfileId',
label: 'Quality Profile',
isSortable: true,
isVisible: true
},
{
name: 'languageProfileId',
label: 'Language Profile',
isSortable: true,
isVisible: showLanguageProfile
},
{
name: 'seriesType',
label: 'Series Type',
isSortable: false,
isVisible: true
},
{
name: 'seasonFolder',
label: 'Season Folder',
isSortable: true,
isVisible: true
},
{
name: 'path',
label: 'Path',
isSortable: true,
isVisible: true
},
{
name: 'tags',
label: 'Tags',
isSortable: false,
isVisible: true
}
];
}
class SeriesEditor extends Component {
//
@ -83,8 +34,7 @@ class SeriesEditor extends Component {
allUnselected: false,
lastToggled: null,
selectedState: {},
isOrganizingSeriesModalOpen: false,
columns: getColumns(props.showLanguageProfile)
isOrganizingSeriesModalOpen: false
};
}
@ -152,6 +102,7 @@ class SeriesEditor extends Component {
error,
totalItems,
items,
columns,
selectedFilterKey,
filters,
customFilters,
@ -162,7 +113,7 @@ class SeriesEditor extends Component {
isDeleting,
deleteError,
isOrganizingSeries,
showLanguageProfile,
onTableOptionChange,
onSortPress,
onFilterSelect
} = this.props;
@ -170,8 +121,7 @@ class SeriesEditor extends Component {
const {
allSelected,
allUnselected,
selectedState,
columns
selectedState
} = this.state;
const selectedSeriesIds = this.getSelectedIds();
@ -181,6 +131,18 @@ class SeriesEditor extends Component {
<PageToolbar>
<PageToolbarSection />
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<PageToolbarSeparator />
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
@ -249,7 +211,8 @@ class SeriesEditor extends Component {
isDeleting={isDeleting}
deleteError={deleteError}
isOrganizingSeries={isOrganizingSeries}
showLanguageProfile={showLanguageProfile}
columns={columns}
showLanguageProfile={columns.find((column) => column.name === 'languageProfileId').isVisible}
onSaveSelected={this.onSaveSelected}
onOrganizeSeriesPress={this.onOrganizeSeriesPress}
/>
@ -270,6 +233,7 @@ SeriesEditor.propTypes = {
error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
@ -280,7 +244,7 @@ SeriesEditor.propTypes = {
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
isOrganizingSeries: PropTypes.bool.isRequired,
showLanguageProfile: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired

View File

@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { setSeriesEditorSort, setSeriesEditorFilter, saveSeriesEditor } from 'Store/Actions/seriesEditorActions';
import { setSeriesEditorSort, setSeriesEditorFilter, setSeriesEditorTableOption, saveSeriesEditor } from 'Store/Actions/seriesEditorActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
@ -12,13 +12,11 @@ import SeriesEditor from './SeriesEditor';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.languageProfiles,
createClientSideCollectionSelector('series', 'seriesEditor'),
createCommandExecutingSelector(commandNames.RENAME_SERIES),
(languageProfiles, series, isOrganizingSeries) => {
(series, isOrganizingSeries) => {
return {
isOrganizingSeries,
showLanguageProfile: languageProfiles.items.length > 1,
...series
};
}
@ -28,6 +26,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
dispatchSetSeriesEditorSort: setSeriesEditorSort,
dispatchSetSeriesEditorFilter: setSeriesEditorFilter,
dispatchSetSeriesEditorTableOption: setSeriesEditorTableOption,
dispatchSaveSeriesEditor: saveSeriesEditor,
dispatchFetchRootFolders: fetchRootFolders,
dispatchExecuteCommand: executeCommand
@ -53,6 +52,10 @@ class SeriesEditorConnector extends Component {
this.props.dispatchSetSeriesEditorFilter({ selectedFilterKey });
}
onTableOptionChange = (payload) => {
this.props.dispatchSetSeriesEditorTableOption(payload);
}
onSaveSelected = (payload) => {
this.props.dispatchSaveSeriesEditor(payload);
}
@ -74,6 +77,7 @@ class SeriesEditorConnector extends Component {
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onSaveSelected={this.onSaveSelected}
onTableOptionChange={this.onTableOptionChange}
/>
);
}
@ -82,6 +86,7 @@ class SeriesEditorConnector extends Component {
SeriesEditorConnector.propTypes = {
dispatchSetSeriesEditorSort: PropTypes.func.isRequired,
dispatchSetSeriesEditorFilter: PropTypes.func.isRequired,
dispatchSetSeriesEditorTableOption: PropTypes.func.isRequired,
dispatchSaveSeriesEditor: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired

View File

@ -145,7 +145,7 @@ class SeriesEditorFooter extends Component {
isSaving,
isDeleting,
isOrganizingSeries,
showLanguageProfile,
columns,
onOrganizeSeriesPress
} = this.props;
@ -192,85 +192,130 @@ class SeriesEditorFooter extends Component {
/>
</div>
<div className={styles.inputContainer}>
<SeriesEditorFooterLabel
label="Quality Profile"
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
{
showLanguageProfile &&
<div className={styles.inputContainer}>
<SeriesEditorFooterLabel
label="Language Profile"
isSaving={isSaving && languageProfileId !== NO_CHANGE}
/>
columns.map((column) => {
const {
name,
isVisible
} = column;
<LanguageProfileSelectInputConnector
name="languageProfileId"
value={languageProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
if (!isVisible) {
return null;
}
if (name === 'qualityProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<SeriesEditorFooterLabel
label="Quality Profile"
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'languageProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<SeriesEditorFooterLabel
label="Language Profile"
isSaving={isSaving && languageProfileId !== NO_CHANGE}
/>
<LanguageProfileSelectInputConnector
name="languageProfileId"
value={languageProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'seriesType') {
return (
<div
key={name}
className={styles.inputContainer}
>
<SeriesEditorFooterLabel
label="Series Type"
isSaving={isSaving && seriesType !== NO_CHANGE}
/>
<SeriesTypeSelectInput
name="seriesType"
value={seriesType}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'seasonFolder') {
return (
<div
key={name}
className={styles.inputContainer}
>
<SeriesEditorFooterLabel
label="Season Folder"
isSaving={isSaving && seasonFolder !== NO_CHANGE}
/>
<SelectInput
name="seasonFolder"
value={seasonFolder}
values={seasonFolderOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'path') {
return (
<div
key={name}
className={styles.inputContainer}
>
<SeriesEditorFooterLabel
label="Root Folder"
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
);
}
})
}
<div className={styles.inputContainer}>
<SeriesEditorFooterLabel
label="Series Type"
isSaving={isSaving && seriesType !== NO_CHANGE}
/>
<SeriesTypeSelectInput
name="seriesType"
value={seriesType}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<SeriesEditorFooterLabel
label="Season Folder"
isSaving={isSaving && seasonFolder !== NO_CHANGE}
/>
<SelectInput
name="seasonFolder"
value={seasonFolder}
values={seasonFolderOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<SeriesEditorFooterLabel
label="Root Folder"
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<SeriesEditorFooterLabel
@ -346,6 +391,7 @@ SeriesEditorFooter.propTypes = {
deleteError: PropTypes.object,
isOrganizingSeries: PropTypes.bool.isRequired,
showLanguageProfile: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSaveSelected: PropTypes.func.isRequired,
onOrganizeSeriesPress: PropTypes.func.isRequired
};

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import titleCase from 'Utilities/String/titleCase';
import formatBytes from 'Utilities/Number/formatBytes';
import TagListConnector from 'Components/TagListConnector';
import CheckInput from 'Components/Form/CheckInput';
import TableRow from 'Components/Table/TableRow';
@ -36,6 +36,7 @@ class SeriesEditorRow extends Component {
seriesType,
seasonFolder,
path,
statistics,
tags,
columns,
isSelected,
@ -50,51 +51,109 @@ class SeriesEditorRow extends Component {
onSelectedChange={onSelectedChange}
/>
<SeriesStatusCell
monitored={monitored}
status={status}
/>
<TableRowCell className={styles.title}>
<SeriesTitleLink
titleSlug={titleSlug}
title={title}
/>
</TableRowCell>
<TableRowCell>
{qualityProfile.name}
</TableRowCell>
{
_.find(columns, { name: 'languageProfileId' }).isVisible &&
<TableRowCell>
{languageProfile.name}
</TableRowCell>
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<SeriesStatusCell
key={name}
monitored={monitored}
status={status}
/>
);
}
if (name === 'sortTitle') {
return (
<TableRowCell
key={name}
className={styles.title}
>
<SeriesTitleLink
titleSlug={titleSlug}
title={title}
/>
</TableRowCell>
);
}
if (name === 'qualityProfileId') {
return (
<TableRowCell key={name}>
{qualityProfile.name}
</TableRowCell>
);
}
if (name === 'languageProfileId') {
return (
<TableRowCell key={name}>
{languageProfile.name}
</TableRowCell>
);
}
if (name === 'seriesType') {
return (
<TableRowCell key={name}>
{titleCase(seriesType)}
</TableRowCell>
);
}
if (name === 'seasonFolder') {
return (
<TableRowCell
key={name}
className={styles.seasonFolder}
>
<CheckInput
name="seasonFolder"
value={seasonFolder}
isDisabled={true}
onChange={this.onSeasonFolderChange}
/>
</TableRowCell>
);
}
if (name === 'path') {
return (
<TableRowCell key={name}>
{path}
</TableRowCell>
);
}
if (name === 'sizeOnDisk') {
return (
<TableRowCell key={name}>
{formatBytes(statistics.sizeOnDisk)}
</TableRowCell>
);
}
if (name === 'tags') {
return (
<TableRowCell key={name}>
<TagListConnector
tags={tags}
/>
</TableRowCell>
);
}
})
}
<TableRowCell>
{titleCase(seriesType)}
</TableRowCell>
<TableRowCell className={styles.seasonFolder}>
<CheckInput
name="seasonFolder"
value={seasonFolder}
isDisabled={true}
onChange={this.onSeasonFolderChange}
/>
</TableRowCell>
<TableRowCell>
{path}
</TableRowCell>
<TableRowCell>
<TagListConnector
tags={tags}
/>
</TableRowCell>
</TableRow>
);
}
@ -111,6 +170,7 @@ SeriesEditorRow.propTypes = {
seriesType: PropTypes.string.isRequired,
seasonFolder: PropTypes.bool.isRequired,
path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSelected: PropTypes.bool,

View File

@ -140,6 +140,12 @@ export const sortPredicates = {
}
return result;
},
sizeOnDisk: function(item) {
const { statistics = {} } = item;
return statistics.sizeOnDisk;
}
};

View File

@ -3,6 +3,7 @@ import { batchActions } from 'redux-batched-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createHandleActions from './Creators/createHandleActions';
@ -30,6 +31,65 @@ export const defaultState = {
filters,
filterPredicates,
columns: [
{
name: 'status',
columnLabel: 'Status',
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'sortTitle',
label: 'Series Title',
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'qualityProfileId',
label: 'Quality Profile',
isSortable: true,
isVisible: true
},
{
name: 'languageProfileId',
label: 'Language Profile',
isSortable: false,
isVisible: true
},
{
name: 'seriesType',
label: 'Type',
isSortable: true,
isVisible: true
},
{
name: 'seasonFolder',
label: 'Season Folder',
isSortable: true,
isVisible: true
},
{
name: 'path',
label: 'Path',
isSortable: true,
isVisible: true
},
{
name: 'sizeOnDisk',
label: 'Size on Disk',
isSortable: true,
isVisible: false
},
{
name: 'tags',
label: 'Tags',
isSortable: false,
isVisible: true
}
],
filterBuilderProps: [
{
name: 'monitored',
@ -70,6 +130,12 @@ export const defaultState = {
label: 'Root Folder Path',
type: filterBuilderTypes.EXACT
},
{
name: 'sizeOnDisk',
label: 'Size on Disk',
type: filterBuilderTypes.NUMBER,
valueType: filterBuilderValueTypes.BYTES
},
{
name: 'tags',
label: 'Tags',
@ -95,6 +161,8 @@ export const SET_SERIES_EDITOR_SORT = 'seriesEditor/setSeriesEditorSort';
export const SET_SERIES_EDITOR_FILTER = 'seriesEditor/setSeriesEditorFilter';
export const SAVE_SERIES_EDITOR = 'seriesEditor/saveSeriesEditor';
export const BULK_DELETE_SERIES = 'seriesEditor/bulkDeleteSeries';
export const SET_SERIES_EDITOR_TABLE_OPTION = 'seriesIndex/setSeriesEditorTableOption';
//
// Action Creators
@ -102,6 +170,8 @@ export const setSeriesEditorSort = createAction(SET_SERIES_EDITOR_SORT);
export const setSeriesEditorFilter = createAction(SET_SERIES_EDITOR_FILTER);
export const saveSeriesEditor = createThunk(SAVE_SERIES_EDITOR);
export const bulkDeleteSeries = createThunk(BULK_DELETE_SERIES);
export const setSeriesEditorTableOption = createAction(SET_SERIES_EDITOR_TABLE_OPTION);
//
// Action Handlers
@ -184,6 +254,7 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({
[SET_SERIES_EDITOR_TABLE_OPTION]: createSetTableOptionReducer(section),
[SET_SERIES_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
[SET_SERIES_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)

View File

@ -236,12 +236,6 @@ export const defaultState = {
return statistics.seasonCount;
},
sizeOnDisk: function(item) {
const { statistics = {} } = item;
return statistics.sizeOnDisk;
},
ratings: function(item) {
const { ratings = {} } = item;