New: Move Interactive search to toolbar

pull/9376/head
Qstick 9 months ago
parent 3e534cf8bf
commit 40eeb31a21

@ -3,13 +3,16 @@ import React, { Fragment } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; 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 Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; 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 getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector'; import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearchContent.css'; import styles from './InteractiveSearch.css';
const columns = [ const columns = [
{ {
@ -24,23 +27,6 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: 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', name: 'title',
label: () => translate('Title'), label: () => translate('Title'),
@ -84,12 +70,6 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'customFormat',
label: () => translate('Formats'),
isSortable: true,
isVisible: true
},
{ {
name: 'customFormatScore', name: 'customFormatScore',
label: React.createElement(Icon, { label: React.createElement(Icon, {
@ -107,10 +87,27 @@ const columns = [
}), }),
isSortable: true, isSortable: true,
isVisible: 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 { const {
searchPayload, searchPayload,
isFetching, isFetching,
@ -118,18 +115,36 @@ function InteractiveSearchContent(props) {
error, error,
totalReleasesCount, totalReleasesCount,
items, items,
selectedFilterKey,
filters,
customFilters,
sortKey, sortKey,
sortDirection, sortDirection,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
onSortPress, onSortPress,
onFilterSelect,
onGrabPress onGrabPress
} = props; } = props;
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
const type = 'movies';
return ( return (
<div> <div>
<div className={styles.filterMenuContainer}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
filterModalConnectorComponentProps={{ type }}
onFilterSelect={onFilterSelect}
/>
</div>
{ {
isFetching ? <LoadingIndicator /> : null isFetching ? <LoadingIndicator /> : null
} }
@ -203,19 +218,23 @@ function InteractiveSearchContent(props) {
); );
} }
InteractiveSearchContent.propTypes = { InteractiveSearch.propTypes = {
searchPayload: PropTypes.object.isRequired, searchPayload: PropTypes.object.isRequired,
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, totalReleasesCount: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).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, sortKey: PropTypes.string,
sortDirection: PropTypes.string, sortDirection: PropTypes.string,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired onGrabPress: PropTypes.func.isRequired
}; };
export default InteractiveSearchContent; export default InteractiveSearch;

@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
import * as releaseActions from 'Store/Actions/releaseActions'; import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import InteractiveSearchContent from './InteractiveSearchContent'; import InteractiveSearch from './InteractiveSearch';
function createMapStateToProps(appState) { function createMapStateToProps(appState) {
return createSelector( return createSelector(
@ -29,17 +29,12 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(releaseActions.fetchReleases(payload)); dispatch(releaseActions.fetchReleases(payload));
}, },
dispatchClearReleases(payload) {
dispatch(releaseActions.clearReleases(payload));
},
onSortPress(sortKey, sortDirection) { onSortPress(sortKey, sortDirection) {
dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection })); dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection }));
}, },
onFilterSelect(selectedFilterKey) { onFilterSelect(selectedFilterKey) {
const action = releaseActions.setReleasesFilter; dispatch(releaseActions.setReleasesFilter({ selectedFilterKey }));
dispatch(action({ selectedFilterKey }));
}, },
onGrabPress(payload) { onGrabPress(payload) {
@ -48,7 +43,7 @@ function createMapDispatchToProps(dispatch, props) {
}; };
} }
class InteractiveSearchContentConnector extends Component { class InteractiveSearchConnector extends Component {
// //
// Lifecycle // Lifecycle
@ -73,24 +68,22 @@ class InteractiveSearchContentConnector extends Component {
render() { render() {
const { const {
dispatchFetchReleases, dispatchFetchReleases,
dispatchClearReleases,
...otherProps ...otherProps
} = this.props; } = this.props;
return ( return (
<InteractiveSearchContent <InteractiveSearch
{...otherProps} {...otherProps}
/> />
); );
} }
} }
InteractiveSearchContentConnector.propTypes = { InteractiveSearchConnector.propTypes = {
searchPayload: PropTypes.object.isRequired, searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
dispatchFetchReleases: PropTypes.func.isRequired, dispatchFetchReleases: PropTypes.func.isRequired
dispatchClearReleases: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector); export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);

@ -4,7 +4,7 @@ import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton'; import PageMenuButton from 'Components/Menu/PageMenuButton';
import { align } from 'Helpers/Props'; import { align } from 'Helpers/Props';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import styles from './InteractiveSearchContent.css'; import styles from './InteractiveSearch.css';
function InteractiveSearchFilterMenu(props) { function InteractiveSearchFilterMenu(props) {
const { const {

@ -1,23 +1,25 @@
.cell {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.protocol { .protocol {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px; width: 80px;
} }
.titleContent {
display: flex;
align-items: center;
justify-content: space-between;
word-break: break-all;
}
.indexer { .indexer {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 85px; width: 85px;
} }
.quality, .quality,
.customFormat,
.languages { .languages {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
} }
.languages { .languages {
@ -25,7 +27,7 @@
} }
.customFormatScore { .customFormatScore {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px; width: 55px;
font-weight: bold; font-weight: bold;
@ -33,31 +35,28 @@
} }
.rejected, .rejected,
.indexerFlags { .indexerFlags,
composes: cell; .download {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px; width: 50px;
} }
.age, .age,
.size { .size {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
white-space: nowrap; white-space: nowrap;
} }
.peers { .peers {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px; width: 75px;
} }
.titleContent {
overflow-wrap: break-word;
}
.history { .history {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px; width: 75px;
} }
@ -67,7 +66,7 @@
} }
.download { .download {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px; width: 80px;
} }

@ -3,8 +3,6 @@
interface CssExports { interface CssExports {
'age': string; 'age': string;
'blocklist': string; 'blocklist': string;
'cell': string;
'customFormat': string;
'customFormatScore': string; 'customFormatScore': string;
'download': string; 'download': string;
'downloadIcon': string; 'downloadIcon': string;

@ -199,53 +199,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
{formatAge(age, ageHours, ageMinutes)} {formatAge(age, ageHours, ageMinutes)}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.RIGHT}
/>
) : null}
</TableRowCell>
<TableRowCell> <TableRowCell>
<div className={styles.titleContent}> <div className={styles.titleContent}>
<Link to={infoUrl} title={title}> <Link to={infoUrl} title={title}>
@ -316,10 +269,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
<MovieQuality quality={quality} /> <MovieQuality quality={quality} />
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.customFormat}>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
<Tooltip <Tooltip
anchor={formatCustomFormatScore( anchor={formatCustomFormatScore(
@ -348,6 +297,53 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
) : null} ) : null}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<ConfirmModal <ConfirmModal
isOpen={isConfirmGrabModalOpen} isOpen={isConfirmGrabModalOpen}
kind={kinds.WARNING} kind={kinds.WARNING}

@ -1,16 +0,0 @@
import React from 'react';
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
function InteractiveSearchTable(props) {
return (
<InteractiveSearchContentConnector
searchPayload={props}
/>
);
}
InteractiveSearchTable.propTypes = {
};
export default InteractiveSearchTable;

@ -23,12 +23,11 @@ import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieHistoryTable from 'Movie/History/MovieHistoryTable'; import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable'; import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable'; import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@ -78,6 +77,7 @@ class MovieDetails extends Component {
isEditMovieModalOpen: false, isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false, isDeleteMovieModalOpen: false,
isInteractiveImportModalOpen: false, isInteractiveImportModalOpen: false,
isInteractiveSearchModalOpen: false,
allExpanded: false, allExpanded: false,
allCollapsed: false, allCollapsed: false,
expandedState: {}, expandedState: {},
@ -134,6 +134,14 @@ class MovieDetails extends Component {
this.setState({ isEditMovieModalOpen: false }); this.setState({ isEditMovieModalOpen: false });
}; };
onInteractiveSearchPress = () => {
this.setState({ isInteractiveSearchModalOpen: true });
};
onInteractiveSearchModalClose = () => {
this.setState({ isInteractiveSearchModalOpen: false });
};
onDeleteMoviePress = () => { onDeleteMoviePress = () => {
this.setState({ this.setState({
isEditMovieModalOpen: false, isEditMovieModalOpen: false,
@ -295,6 +303,7 @@ class MovieDetails extends Component {
isEditMovieModalOpen, isEditMovieModalOpen,
isDeleteMovieModalOpen, isDeleteMovieModalOpen,
isInteractiveImportModalOpen, isInteractiveImportModalOpen,
isInteractiveSearchModalOpen,
overviewHeight, overviewHeight,
titleWidth, titleWidth,
selectedTabIndex selectedTabIndex
@ -324,6 +333,14 @@ class MovieDetails extends Component {
onPress={onSearchPress} onPress={onSearchPress}
/> />
<PageToolbarButton
label={translate('InteractiveSearch')}
iconName={icons.INTERACTIVE}
isSpinning={isSearching}
title={undefined}
onPress={this.onInteractiveSearchPress}
/>
<PageToolbarSeparator /> <PageToolbarSeparator />
<PageToolbarButton <PageToolbarButton
@ -665,13 +682,6 @@ class MovieDetails extends Component {
{translate('History')} {translate('History')}
</Tab> </Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Search')}
</Tab>
<Tab <Tab
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
@ -700,13 +710,6 @@ class MovieDetails extends Component {
{translate('Crew')} {translate('Crew')}
</Tab> </Tab>
{
selectedTabIndex === 1 &&
<div className={styles.filterIcon}>
<InteractiveSearchFilterMenuConnector />
</div>
}
</TabList> </TabList>
<TabPanel> <TabPanel>
@ -715,12 +718,6 @@ class MovieDetails extends Component {
/> />
</TabPanel> </TabPanel>
<TabPanel>
<InteractiveSearchTable
movieId={id}
/>
</TabPanel>
<TabPanel> <TabPanel>
<MovieFileEditorTable <MovieFileEditorTable
movieId={id} movieId={id}
@ -780,6 +777,12 @@ class MovieDetails extends Component {
showImportMode={false} showImportMode={false}
onModalClose={this.onInteractiveImportModalClose} onModalClose={this.onInteractiveImportModalClose}
/> />
<MovieInteractiveSearchModalConnector
isOpen={isInteractiveSearchModalOpen}
movieId={id}
onModalClose={this.onInteractiveSearchModalClose}
/>
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
); );

@ -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 (
<Modal
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
size={sizes.EXTRA_LARGE}
>
<MovieInteractiveSearchModalContent
movieId={movieId}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieInteractiveSearchModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
movieId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieInteractiveSearchModal;

@ -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);

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('InteractiveSearchModalHeader')}
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
<InteractiveSearchConnector
searchPayload={{ movieId }}
/>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
MovieInteractiveSearchModalContent.propTypes = {
movieId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieInteractiveSearchModalContent;

@ -556,6 +556,7 @@
"InteractiveImportNoMovie": "Movie must be chosen for each selected file", "InteractiveImportNoMovie": "Movie must be chosen for each selected file",
"InteractiveImportNoQuality": "Quality must be chosen for each selected file", "InteractiveImportNoQuality": "Quality must be chosen for each selected file",
"InteractiveSearch": "Interactive Search", "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.", "InteractiveSearchResultsFailedErrorMessage": "Search failed because its {message}. Try refreshing the movie info and verify the necessary information is present before searching again.",
"Interval": "Interval", "Interval": "Interval",
"InvalidFormat": "Invalid Format", "InvalidFormat": "Invalid Format",

Loading…
Cancel
Save