From 069fc5cd33de8129960bfb5fb889e6783a01f74b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 30 May 2020 13:56:28 -0700 Subject: [PATCH] Mass Editor size and options New: Option to show size in Mass Editor New: Size on Disk in Mass Editor custom filters Closes #3273 --- frontend/src/Series/Editor/SeriesEditor.js | 84 +++----- .../Series/Editor/SeriesEditorConnector.js | 13 +- .../src/Series/Editor/SeriesEditorFooter.js | 198 +++++++++++------- frontend/src/Series/Editor/SeriesEditorRow.js | 148 +++++++++---- frontend/src/Store/Actions/seriesActions.js | 6 + .../src/Store/Actions/seriesEditorActions.js | 71 +++++++ .../src/Store/Actions/seriesIndexActions.js | 6 - 7 files changed, 336 insertions(+), 190 deletions(-) diff --git a/frontend/src/Series/Editor/SeriesEditor.js b/frontend/src/Series/Editor/SeriesEditor.js index dc780ac2f..88065b06e 100644 --- a/frontend/src/Series/Editor/SeriesEditor.js +++ b/frontend/src/Series/Editor/SeriesEditor.js @@ -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 { + + + + + + 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 diff --git a/frontend/src/Series/Editor/SeriesEditorConnector.js b/frontend/src/Series/Editor/SeriesEditorConnector.js index fb9cb478f..6f68c4ed8 100644 --- a/frontend/src/Series/Editor/SeriesEditorConnector.js +++ b/frontend/src/Series/Editor/SeriesEditorConnector.js @@ -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 diff --git a/frontend/src/Series/Editor/SeriesEditorFooter.js b/frontend/src/Series/Editor/SeriesEditorFooter.js index b4edc9a83..bec0f7c5c 100644 --- a/frontend/src/Series/Editor/SeriesEditorFooter.js +++ b/frontend/src/Series/Editor/SeriesEditorFooter.js @@ -145,7 +145,7 @@ class SeriesEditorFooter extends Component { isSaving, isDeleting, isOrganizingSeries, - showLanguageProfile, + columns, onOrganizeSeriesPress } = this.props; @@ -192,85 +192,130 @@ class SeriesEditorFooter extends Component { /> -
- - - -
- { - showLanguageProfile && -
- + columns.map((column) => { + const { + name, + isVisible + } = column; - -
+ if (!isVisible) { + return null; + } + + if (name === 'qualityProfileId') { + return ( +
+ + + +
+ ); + } + + if (name === 'languageProfileId') { + return ( +
+ + + +
+ ); + } + + if (name === 'seriesType') { + return ( +
+ + + +
+ ); + } + + if (name === 'seasonFolder') { + return ( +
+ + + +
+ ); + } + + if (name === 'path') { + return ( +
+ + + +
+ ); + } + }) } -
- - - -
- -
- - - -
- -
- - - -
-
- - - - - - - - {qualityProfile.name} - - { - _.find(columns, { name: 'languageProfileId' }).isVisible && - - {languageProfile.name} - + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'status') { + return ( + + ); + } + + if (name === 'sortTitle') { + return ( + + + + ); + } + + if (name === 'qualityProfileId') { + return ( + + {qualityProfile.name} + + ); + } + + if (name === 'languageProfileId') { + return ( + + {languageProfile.name} + + ); + } + + if (name === 'seriesType') { + return ( + + {titleCase(seriesType)} + + ); + } + + if (name === 'seasonFolder') { + return ( + + + + ); + } + + if (name === 'path') { + return ( + + {path} + + ); + } + + if (name === 'sizeOnDisk') { + return ( + + {formatBytes(statistics.sizeOnDisk)} + + ); + } + + if (name === 'tags') { + return ( + + + + ); + } + }) } - - - {titleCase(seriesType)} - - - - - - - - {path} - - - - - ); } @@ -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, diff --git a/frontend/src/Store/Actions/seriesActions.js b/frontend/src/Store/Actions/seriesActions.js index 30fca8c36..3e576c22d 100644 --- a/frontend/src/Store/Actions/seriesActions.js +++ b/frontend/src/Store/Actions/seriesActions.js @@ -140,6 +140,12 @@ export const sortPredicates = { } return result; + }, + + sizeOnDisk: function(item) { + const { statistics = {} } = item; + + return statistics.sizeOnDisk; } }; diff --git a/frontend/src/Store/Actions/seriesEditorActions.js b/frontend/src/Store/Actions/seriesEditorActions.js index 85f3db981..60aa9d1c9 100644 --- a/frontend/src/Store/Actions/seriesEditorActions.js +++ b/frontend/src/Store/Actions/seriesEditorActions.js @@ -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) diff --git a/frontend/src/Store/Actions/seriesIndexActions.js b/frontend/src/Store/Actions/seriesIndexActions.js index 6822625c1..fc8b811aa 100644 --- a/frontend/src/Store/Actions/seriesIndexActions.js +++ b/frontend/src/Store/Actions/seriesIndexActions.js @@ -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;