diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorTableContent.js b/frontend/src/MovieFile/Editor/MovieFileEditorTableContent.js index 259f8c935..40133755c 100644 --- a/frontend/src/MovieFile/Editor/MovieFileEditorTableContent.js +++ b/frontend/src/MovieFile/Editor/MovieFileEditorTableContent.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; +import { sortDirections } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import MovieFileEditorRow from './MovieFileEditorRow'; import styles from './MovieFileEditorTableContent.css'; @@ -15,6 +16,9 @@ class MovieFileEditorTableContent extends Component { const { items, columns, + sortKey, + sortDirection, + onSortPress, onTableOptionChange } = this.props; @@ -31,6 +35,9 @@ class MovieFileEditorTableContent extends Component { !!items.length && @@ -60,7 +67,10 @@ MovieFileEditorTableContent.propTypes = { isDeleting: PropTypes.bool.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string.isRequired, + sortDirection: PropTypes.oneOf(sortDirections.all), onTableOptionChange: PropTypes.func.isRequired, + onSortPress: PropTypes.func.isRequired, onDeletePress: PropTypes.func.isRequired }; diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorTableContentConnector.js b/frontend/src/MovieFile/Editor/MovieFileEditorTableContentConnector.js index b3e190228..3d19a4bee 100644 --- a/frontend/src/MovieFile/Editor/MovieFileEditorTableContentConnector.js +++ b/frontend/src/MovieFile/Editor/MovieFileEditorTableContentConnector.js @@ -2,8 +2,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { deleteMovieFile, setMovieFilesTableOption, updateMovieFiles } from 'Store/Actions/movieFileActions'; +import { deleteMovieFile, setMovieFilesSort, setMovieFilesTableOption } from 'Store/Actions/movieFileActions'; import { fetchLanguages, fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createMovieSelector from 'Store/Selectors/createMovieSelector'; import getQualities from 'Utilities/Quality/getQualities'; import MovieFileEditorTableContent from './MovieFileEditorTableContent'; @@ -11,7 +12,7 @@ import MovieFileEditorTableContent from './MovieFileEditorTableContent'; function createMapStateToProps() { return createSelector( (state, { movieId }) => movieId, - (state) => state.movieFiles, + createClientSideCollectionSelector('movieFiles'), (state) => state.settings.languages, (state) => state.settings.qualityProfiles, createMovieSelector(), @@ -28,6 +29,8 @@ function createMapStateToProps() { return { items: filesForMovie, columns: movieFiles.columns, + sortKey: movieFiles.sortKey, + sortDirection: movieFiles.sortDirection, isDeleting: movieFiles.isDeleting, isSaving: movieFiles.isSaving, error: null, @@ -38,31 +41,13 @@ function createMapStateToProps() { ); } -function createMapDispatchToProps(dispatch, props) { - return { - dispatchFetchQualityProfileSchema() { - dispatch(fetchQualityProfileSchema()); - }, - - dispatchFetchLanguages() { - dispatch(fetchLanguages()); - }, - - dispatchUpdateMovieFiles(updateProps) { - dispatch(updateMovieFiles(updateProps)); - }, - - onTableOptionChange(payload) { - dispatch(setMovieFilesTableOption(payload)); - }, - - onDeletePress(movieFileId) { - dispatch(deleteMovieFile({ - id: movieFileId - })); - } - }; -} +const mapDispatchToProps = { + fetchQualityProfileSchema, + fetchLanguages, + deleteMovieFile, + setMovieFilesTableOption, + setMovieFilesSort +}; class MovieFileEditorTableContentConnector extends Component { @@ -70,24 +55,40 @@ class MovieFileEditorTableContentConnector extends Component { // Lifecycle componentDidMount() { - this.props.dispatchFetchLanguages(); - this.props.dispatchFetchQualityProfileSchema(); + this.props.fetchLanguages(); + this.props.fetchQualityProfileSchema(); } + // + // Listeners + + onDeletePress = (movieFileId) => { + this.props.deleteMovieFile({ + id: movieFileId + }); + }; + + onTableOptionChange = (payload) => { + this.props.setMovieFilesTableOption(payload); + }; + + onSortPress = (sortKey, sortDirection) => { + this.props.setMovieFilesSort({ + sortKey, + sortDirection + }); + }; + // // Render render() { - const { - dispatchFetchLanguages, - dispatchFetchQualityProfileSchema, - dispatchUpdateMovieFiles, - ...otherProps - } = this.props; - return ( ); } @@ -97,9 +98,11 @@ MovieFileEditorTableContentConnector.propTypes = { movieId: PropTypes.number.isRequired, languages: PropTypes.arrayOf(PropTypes.object).isRequired, qualities: PropTypes.arrayOf(PropTypes.object).isRequired, - dispatchFetchLanguages: PropTypes.func.isRequired, - dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, - dispatchUpdateMovieFiles: PropTypes.func.isRequired + fetchLanguages: PropTypes.func.isRequired, + fetchQualityProfileSchema: PropTypes.func.isRequired, + deleteMovieFile: PropTypes.func.isRequired, + setMovieFilesTableOption: PropTypes.func.isRequired, + setMovieFilesSort: PropTypes.func.isRequired }; -export default connect(createMapStateToProps, createMapDispatchToProps)(MovieFileEditorTableContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(MovieFileEditorTableContentConnector); diff --git a/frontend/src/Store/Actions/movieFileActions.js b/frontend/src/Store/Actions/movieFileActions.js index 609cb67f2..238a85d5d 100644 --- a/frontend/src/Store/Actions/movieFileActions.js +++ b/frontend/src/Store/Actions/movieFileActions.js @@ -4,8 +4,9 @@ import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import Icon from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; -import { icons } from 'Helpers/Props'; +import { icons, sortDirections } from 'Helpers/Props'; import movieEntities from 'Movie/movieEntities'; +import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; @@ -31,13 +32,16 @@ export const defaultState = { deleteError: null, isSaving: false, saveError: null, + sortKey: 'relativePath', + sortDirection: sortDirections.ASCENDING, items: [], columns: [ { name: 'relativePath', label: () => translate('RelativePath'), - isVisible: true + isVisible: true, + isSortable: true }, { name: 'videoCodec', @@ -67,7 +71,8 @@ export const defaultState = { { name: 'size', label: () => translate('Size'), - isVisible: true + isVisible: true, + isSortable: true }, { name: 'languages', @@ -96,12 +101,14 @@ export const defaultState = { name: icons.SCORE, title: () => translate('CustomFormatScore') }), - isVisible: true + isVisible: true, + isSortable: true }, { name: 'dateAdded', label: () => translate('Added'), - isVisible: false + isVisible: false, + isSortable: true }, { name: 'actions', @@ -114,7 +121,9 @@ export const defaultState = { }; export const persistState = [ - 'movieFiles.columns' + 'movieFiles.columns', + 'movieFiles.sortDirection', + 'movieFiles.sortKey' ]; // @@ -125,6 +134,7 @@ export const DELETE_MOVIE_FILE = 'movieFiles/deleteMovieFile'; export const DELETE_MOVIE_FILES = 'movieFiles/deleteMovieFiles'; export const UPDATE_MOVIE_FILES = 'movieFiles/updateMovieFiles'; export const CLEAR_MOVIE_FILES = 'movieFiles/clearMovieFiles'; +export const SET_MOVIE_FILES_SORT = 'movieFiles/setMovieFilesSort'; export const SET_MOVIE_FILES_TABLE_OPTION = 'movieFiles/setMovieFilesTableOption'; // @@ -135,6 +145,7 @@ export const deleteMovieFile = createThunk(DELETE_MOVIE_FILE); export const deleteMovieFiles = createThunk(DELETE_MOVIE_FILES); export const updateMovieFiles = createThunk(UPDATE_MOVIE_FILES); export const clearMovieFiles = createAction(CLEAR_MOVIE_FILES); +export const setMovieFilesSort = createAction(SET_MOVIE_FILES_SORT); export const setMovieFilesTableOption = createAction(SET_MOVIE_FILES_TABLE_OPTION); // @@ -327,6 +338,7 @@ export const actionHandlers = handleThunks({ // Reducers export const reducers = createHandleActions({ + [SET_MOVIE_FILES_TABLE_OPTION]: createSetTableOptionReducer(section), [CLEAR_MOVIE_FILES]: (state) => { @@ -340,6 +352,8 @@ export const reducers = createHandleActions({ saveError: null, items: [] }); - } + }, + + [SET_MOVIE_FILES_SORT]: createSetClientSideCollectionSortReducer(section) }, defaultState, section); diff --git a/src/Radarr.Api.V3/ExtraFiles/ExtraFileController.cs b/src/Radarr.Api.V3/ExtraFiles/ExtraFileController.cs index 4d8ec87d3..fb3d649e1 100644 --- a/src/Radarr.Api.V3/ExtraFiles/ExtraFileController.cs +++ b/src/Radarr.Api.V3/ExtraFiles/ExtraFileController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Metadata.Files; @@ -27,9 +28,9 @@ namespace Radarr.Api.V3.ExtraFiles { var extraFiles = new List(); - var subtitleFiles = _subtitleFileService.GetFilesByMovie(movieId); - var metadataFiles = _metadataFileService.GetFilesByMovie(movieId); - var otherExtraFiles = _otherFileService.GetFilesByMovie(movieId); + var subtitleFiles = _subtitleFileService.GetFilesByMovie(movieId).OrderBy(f => f.RelativePath).ToList(); + var metadataFiles = _metadataFileService.GetFilesByMovie(movieId).OrderBy(f => f.RelativePath).ToList(); + var otherExtraFiles = _otherFileService.GetFilesByMovie(movieId).OrderBy(f => f.RelativePath).ToList(); extraFiles.AddRange(subtitleFiles.ToResource()); extraFiles.AddRange(metadataFiles.ToResource());