diff --git a/frontend/src/InteractiveSearch/InteractiveSearchContent.css b/frontend/src/InteractiveSearch/InteractiveSearch.css similarity index 100% rename from frontend/src/InteractiveSearch/InteractiveSearchContent.css rename to frontend/src/InteractiveSearch/InteractiveSearch.css diff --git a/frontend/src/InteractiveSearch/InteractiveSearchContent.css.d.ts b/frontend/src/InteractiveSearch/InteractiveSearch.css.d.ts similarity index 100% rename from frontend/src/InteractiveSearch/InteractiveSearchContent.css.d.ts rename to frontend/src/InteractiveSearch/InteractiveSearch.css.d.ts diff --git a/frontend/src/InteractiveSearch/InteractiveSearchContent.js b/frontend/src/InteractiveSearch/InteractiveSearch.js similarity index 80% rename from frontend/src/InteractiveSearch/InteractiveSearchContent.js rename to frontend/src/InteractiveSearch/InteractiveSearch.js index a4544514d..d3fa3ca14 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchContent.js +++ b/frontend/src/InteractiveSearch/InteractiveSearch.js @@ -3,13 +3,16 @@ import React, { Fragment } from 'react'; import Alert from 'Components/Alert'; import Icon from 'Components/Icon'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import PageMenuButton from 'Components/Menu/PageMenuButton'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import { icons, kinds, sortDirections } from 'Helpers/Props'; +import { align, icons, kinds, sortDirections } from 'Helpers/Props'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; +import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; import InteractiveSearchRowConnector from './InteractiveSearchRowConnector'; -import styles from './InteractiveSearchContent.css'; +import styles from './InteractiveSearch.css'; const columns = [ { @@ -24,23 +27,6 @@ const columns = [ isSortable: true, isVisible: true }, - { - name: 'releaseWeight', - label: React.createElement(Icon, { name: icons.DOWNLOAD }), - isSortable: true, - fixedSortDirection: sortDirections.ASCENDING, - isVisible: true - }, - { - name: 'rejections', - label: React.createElement(Icon, { - name: icons.DANGER, - title: () => translate('Rejections') - }), - isSortable: true, - fixedSortDirection: sortDirections.ASCENDING, - isVisible: true - }, { name: 'title', label: () => translate('Title'), @@ -84,12 +70,6 @@ const columns = [ isSortable: true, isVisible: true }, - { - name: 'customFormat', - label: () => translate('Formats'), - isSortable: true, - isVisible: true - }, { name: 'customFormatScore', label: React.createElement(Icon, { @@ -107,10 +87,27 @@ const columns = [ }), isSortable: true, isVisible: true + }, + { + name: 'releaseWeight', + label: React.createElement(Icon, { name: icons.DOWNLOAD }), + isSortable: true, + fixedSortDirection: sortDirections.ASCENDING, + isVisible: true + }, + { + name: 'rejections', + label: React.createElement(Icon, { + name: icons.DANGER, + title: () => translate('Rejections') + }), + isSortable: true, + fixedSortDirection: sortDirections.ASCENDING, + isVisible: true } ]; -function InteractiveSearchContent(props) { +function InteractiveSearch(props) { const { searchPayload, isFetching, @@ -118,18 +115,36 @@ function InteractiveSearchContent(props) { error, totalReleasesCount, items, + selectedFilterKey, + filters, + customFilters, sortKey, sortDirection, longDateFormat, timeFormat, onSortPress, + onFilterSelect, onGrabPress } = props; const errorMessage = getErrorMessage(error); + const type = 'movies'; return (
+
+ +
+ { isFetching ? : null } @@ -203,19 +218,23 @@ function InteractiveSearchContent(props) { ); } -InteractiveSearchContent.propTypes = { +InteractiveSearch.propTypes = { searchPayload: PropTypes.object.isRequired, isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, error: PropTypes.object, totalReleasesCount: PropTypes.number.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, sortDirection: PropTypes.string, longDateFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired, onSortPress: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, onGrabPress: PropTypes.func.isRequired }; -export default InteractiveSearchContent; +export default InteractiveSearch; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchContentConnector.js b/frontend/src/InteractiveSearch/InteractiveSearchConnector.js similarity index 76% rename from frontend/src/InteractiveSearch/InteractiveSearchContentConnector.js rename to frontend/src/InteractiveSearch/InteractiveSearchConnector.js index f4432d9e3..17b4d8743 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchContentConnector.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchConnector.js @@ -5,7 +5,7 @@ import { createSelector } from 'reselect'; import * as releaseActions from 'Store/Actions/releaseActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import InteractiveSearchContent from './InteractiveSearchContent'; +import InteractiveSearch from './InteractiveSearch'; function createMapStateToProps(appState) { return createSelector( @@ -29,17 +29,12 @@ function createMapDispatchToProps(dispatch, props) { dispatch(releaseActions.fetchReleases(payload)); }, - dispatchClearReleases(payload) { - dispatch(releaseActions.clearReleases(payload)); - }, - onSortPress(sortKey, sortDirection) { dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection })); }, onFilterSelect(selectedFilterKey) { - const action = releaseActions.setReleasesFilter; - dispatch(action({ selectedFilterKey })); + dispatch(releaseActions.setReleasesFilter({ selectedFilterKey })); }, onGrabPress(payload) { @@ -48,7 +43,7 @@ function createMapDispatchToProps(dispatch, props) { }; } -class InteractiveSearchContentConnector extends Component { +class InteractiveSearchConnector extends Component { // // Lifecycle @@ -73,24 +68,22 @@ class InteractiveSearchContentConnector extends Component { render() { const { dispatchFetchReleases, - dispatchClearReleases, ...otherProps } = this.props; return ( - ); } } -InteractiveSearchContentConnector.propTypes = { +InteractiveSearchConnector.propTypes = { searchPayload: PropTypes.object.isRequired, isPopulated: PropTypes.bool.isRequired, - dispatchFetchReleases: PropTypes.func.isRequired, - dispatchClearReleases: PropTypes.func.isRequired + dispatchFetchReleases: PropTypes.func.isRequired }; -export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector); +export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector); diff --git a/frontend/src/InteractiveSearch/InteractiveSearchFilterMenu.js b/frontend/src/InteractiveSearch/InteractiveSearchFilterMenu.js index 10fc32c93..1a3081ad1 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchFilterMenu.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchFilterMenu.js @@ -4,7 +4,7 @@ import FilterMenu from 'Components/Menu/FilterMenu'; import PageMenuButton from 'Components/Menu/PageMenuButton'; import { align } from 'Helpers/Props'; import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; -import styles from './InteractiveSearchContent.css'; +import styles from './InteractiveSearch.css'; function InteractiveSearchFilterMenu(props) { const { diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css b/frontend/src/InteractiveSearch/InteractiveSearchRow.css index 74a84defb..929f22ab3 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css @@ -1,23 +1,25 @@ -.cell { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; -} - .protocol { - composes: cell; + composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 80px; } +.titleContent { + display: flex; + align-items: center; + justify-content: space-between; + word-break: break-all; +} + .indexer { - composes: cell; + composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 85px; } .quality, -.customFormat, .languages { - composes: cell; + composes: cell from '~Components/Table/Cells/TableRowCell.css'; } .languages { @@ -25,7 +27,7 @@ } .customFormatScore { - composes: cell; + composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 55px; font-weight: bold; @@ -33,31 +35,28 @@ } .rejected, -.indexerFlags { - composes: cell; +.indexerFlags, +.download { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 50px; } .age, .size { - composes: cell; + composes: cell from '~Components/Table/Cells/TableRowCell.css'; white-space: nowrap; } .peers { - composes: cell; + composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 75px; } -.titleContent { - overflow-wrap: break-word; -} - .history { - composes: cell; + composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 75px; } @@ -67,7 +66,7 @@ } .download { - composes: cell; + composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 80px; } diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts b/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts index 6b8ab7ae6..723fbbc15 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts @@ -3,8 +3,6 @@ interface CssExports { 'age': string; 'blocklist': string; - 'cell': string; - 'customFormat': string; 'customFormatScore': string; 'download': string; 'downloadIcon': string; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx index aca3e910f..35be786dd 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx @@ -199,53 +199,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) { {formatAge(age, ageHours, ageMinutes)} - - - - -
- - - -
- -
- - - {rejections.length ? ( - } - title={translate('ReleaseRejected')} - body={ -
    - {rejections.map((rejection, index) => { - return
  • {rejection}
  • ; - })} -
- } - position={tooltipPositions.RIGHT} - /> - ) : null} -
-
@@ -316,10 +269,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) { - - - - + + {rejections.length ? ( + } + title={translate('ReleaseRejected')} + body={ +
    + {rejections.map((rejection, index) => { + return
  • {rejection}
  • ; + })} +
+ } + position={tooltipPositions.LEFT} + /> + ) : null} +
+ + + + + +
+ + + +
+ +
+ - ); -} - -InteractiveSearchTable.propTypes = { -}; - -export default InteractiveSearchTable; diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js index fdde4d345..730e581f7 100644 --- a/frontend/src/Movie/Details/MovieDetails.js +++ b/frontend/src/Movie/Details/MovieDetails.js @@ -23,12 +23,11 @@ import Popover from 'Components/Tooltip/Popover'; import Tooltip from 'Components/Tooltip/Tooltip'; import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; -import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector'; -import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import MovieHistoryTable from 'Movie/History/MovieHistoryTable'; import MoviePoster from 'Movie/MoviePoster'; +import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector'; import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable'; import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; @@ -78,6 +77,7 @@ class MovieDetails extends Component { isEditMovieModalOpen: false, isDeleteMovieModalOpen: false, isInteractiveImportModalOpen: false, + isInteractiveSearchModalOpen: false, allExpanded: false, allCollapsed: false, expandedState: {}, @@ -134,6 +134,14 @@ class MovieDetails extends Component { this.setState({ isEditMovieModalOpen: false }); }; + onInteractiveSearchPress = () => { + this.setState({ isInteractiveSearchModalOpen: true }); + }; + + onInteractiveSearchModalClose = () => { + this.setState({ isInteractiveSearchModalOpen: false }); + }; + onDeleteMoviePress = () => { this.setState({ isEditMovieModalOpen: false, @@ -295,6 +303,7 @@ class MovieDetails extends Component { isEditMovieModalOpen, isDeleteMovieModalOpen, isInteractiveImportModalOpen, + isInteractiveSearchModalOpen, overviewHeight, titleWidth, selectedTabIndex @@ -324,6 +333,14 @@ class MovieDetails extends Component { onPress={onSearchPress} /> + + - - {translate('Search')} - - - { - selectedTabIndex === 1 && -
- -
- } - @@ -715,12 +718,6 @@ class MovieDetails extends Component { /> - - - - + + ); diff --git a/frontend/src/Movie/Search/MovieInteractiveSearchModal.js b/frontend/src/Movie/Search/MovieInteractiveSearchModal.js new file mode 100644 index 000000000..ec8987dfa --- /dev/null +++ b/frontend/src/Movie/Search/MovieInteractiveSearchModal.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import MovieInteractiveSearchModalContent from './MovieInteractiveSearchModalContent'; + +function MovieInteractiveSearchModal(props) { + const { + isOpen, + movieId, + onModalClose + } = props; + + return ( + + + + ); +} + +MovieInteractiveSearchModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + movieId: PropTypes.number.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default MovieInteractiveSearchModal; diff --git a/frontend/src/Movie/Search/MovieInteractiveSearchModalConnector.js b/frontend/src/Movie/Search/MovieInteractiveSearchModalConnector.js new file mode 100644 index 000000000..f00b1cb4d --- /dev/null +++ b/frontend/src/Movie/Search/MovieInteractiveSearchModalConnector.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; +import MovieInteractiveSearchModal from './MovieInteractiveSearchModal'; + +function createMapDispatchToProps(dispatch, props) { + return { + onModalClose() { + dispatch(cancelFetchReleases()); + dispatch(clearReleases()); + props.onModalClose(); + } + }; +} + +export default connect(null, createMapDispatchToProps)(MovieInteractiveSearchModal); diff --git a/frontend/src/Movie/Search/MovieInteractiveSearchModalContent.js b/frontend/src/Movie/Search/MovieInteractiveSearchModalContent.js new file mode 100644 index 000000000..dfcabc73e --- /dev/null +++ b/frontend/src/Movie/Search/MovieInteractiveSearchModalContent.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { scrollDirections } from 'Helpers/Props'; +import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector'; +import translate from 'Utilities/String/translate'; + +function MovieInteractiveSearchModalContent(props) { + const { + movieId, + onModalClose + } = props; + + return ( + + + {translate('InteractiveSearchModalHeader')} + + + + + + + + + + + ); +} + +MovieInteractiveSearchModalContent.propTypes = { + movieId: PropTypes.number.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default MovieInteractiveSearchModalContent; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index afe21df42..3e6a2bd9b 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -556,6 +556,7 @@ "InteractiveImportNoMovie": "Movie must be chosen for each selected file", "InteractiveImportNoQuality": "Quality must be chosen for each selected file", "InteractiveSearch": "Interactive Search", + "InteractiveSearchModalHeader": "Interactive Search", "InteractiveSearchResultsFailedErrorMessage": "Search failed because its {message}. Try refreshing the movie info and verify the necessary information is present before searching again.", "Interval": "Interval", "InvalidFormat": "Invalid Format",