New: Bulk Grab Releases and Parameter Search

This commit is contained in:
Qstick
2021-11-20 02:52:26 -06:00
parent f69f96695b
commit 5d32bcf8b9
20 changed files with 961 additions and 50 deletions

View File

@@ -0,0 +1,35 @@
.groups {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 20px;
}
.namingSelectContainer {
display: flex;
justify-content: flex-end;
}
.namingSelect {
composes: select from '~Components/Form/SelectInput.css';
margin-left: 10px;
width: 200px;
}
.footNote {
display: flex;
color: $helpTextColor;
.icon {
margin-top: 3px;
margin-right: 5px;
padding: 2px;
}
code {
padding: 0 1px;
border: 1px solid $borderColor;
background-color: #f7f7f7;
}
}

View File

@@ -0,0 +1,270 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FieldSet from 'Components/FieldSet';
import SelectInput from 'Components/Form/SelectInput';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
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 translate from 'Utilities/String/translate';
import QueryParameterOption from './QueryParameterOption';
import styles from './QueryParameterModal.css';
const searchOptions = [
{ key: 'search', value: 'Basic Search' },
{ key: 'tvsearch', value: 'TV Search' },
{ key: 'movie', value: 'Movie Search' },
{ key: 'music', value: 'Audio Search' },
{ key: 'book', value: 'Book Search' }
];
const seriesTokens = [
{ token: '{ImdbId:tt1234567}', example: 'tt12345' },
{ token: '{TvdbId:12345}', example: '12345' },
{ token: '{TvMazeId:12345}', example: '54321' },
{ token: '{Season:00}', example: '01' },
{ token: '{Episode:00}', example: '01' }
];
const movieTokens = [
{ token: '{ImdbId:tt1234567}', example: 'tt12345' },
{ token: '{TmdbId:12345}', example: '12345' },
{ token: '{Year:2000}', example: '2005' }
];
const audioTokens = [
{ token: '{Artist:Some Body}', example: 'Nirvana' },
{ token: '{Album:Some Album}', example: 'Nevermind' },
{ token: '{Label:Some Label}', example: 'Geffen' }
];
const bookTokens = [
{ token: '{Author:Some Author}', example: 'J. R. R. Tolkien' },
{ token: '{Title:Some Book}', example: 'Lord of the Rings' }
];
class QueryParameterModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._selectionStart = null;
this._selectionEnd = null;
this.state = {
separator: ' '
};
}
//
// Listeners
onInputSelectionChange = (selectionStart, selectionEnd) => {
this._selectionStart = selectionStart;
this._selectionEnd = selectionEnd;
}
onOptionPress = ({ isFullFilename, tokenValue }) => {
const {
name,
value,
onSearchInputChange
} = this.props;
const selectionStart = this._selectionStart;
const selectionEnd = this._selectionEnd;
if (isFullFilename) {
onSearchInputChange({ name, value: tokenValue });
} else if (selectionStart == null) {
onSearchInputChange({
name,
value: `${value}${tokenValue}`
});
} else {
const start = value.substring(0, selectionStart);
const end = value.substring(selectionEnd);
const newValue = `${start}${tokenValue}${end}`;
onSearchInputChange({ name, value: newValue });
this._selectionStart = newValue.length - 1;
this._selectionEnd = newValue.length - 1;
}
}
onInputChange = ({ name, value }) => {
this.props.onSearchInputChange({ value: '' });
this.props.onInputChange({ name, value });
}
//
// Render
render() {
const {
name,
value,
searchType,
isOpen,
onSearchInputChange,
onModalClose
} = this.props;
const {
separator: tokenSeparator
} = this.state;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('QueryOptions')}
</ModalHeader>
<ModalBody>
<FieldSet legend={translate('SearchType')}>
<div className={styles.groups}>
<SelectInput
className={styles.namingSelect}
name="searchType"
value={searchType}
values={searchOptions}
onChange={this.onInputChange}
/>
</div>
</FieldSet>
{
searchType === 'tvsearch' &&
<FieldSet legend={translate('TvSearch')}>
<div className={styles.groups}>
{
seriesTokens.map(({ token, example }) => {
return (
<QueryParameterOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
}
{
searchType === 'movie' &&
<FieldSet legend={translate('MovieSearch')}>
<div className={styles.groups}>
{
movieTokens.map(({ token, example }) => {
return (
<QueryParameterOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
}
{
searchType === 'music' &&
<FieldSet legend={translate('AudioSearch')}>
<div className={styles.groups}>
{
audioTokens.map(({ token, example }) => {
return (
<QueryParameterOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
}
{
searchType === 'book' &&
<FieldSet legend={translate('BookSearch')}>
<div className={styles.groups}>
{
bookTokens.map(({ token, example }) => {
return (
<QueryParameterOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
}
</ModalBody>
<ModalFooter>
<TextInput
name={name}
value={value}
onChange={onSearchInputChange}
onSelectionChange={this.onInputSelectionChange}
/>
<Button onPress={onModalClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
QueryParameterModal.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
searchType: PropTypes.string.isRequired,
isOpen: PropTypes.bool.isRequired,
onSearchInputChange: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default QueryParameterModal;

View File

@@ -0,0 +1,66 @@
.option {
display: flex;
align-items: stretch;
flex-wrap: wrap;
margin: 3px;
border: 1px solid $borderColor;
&:hover {
.token {
background-color: #ddd;
}
.example {
background-color: #ccc;
}
}
}
.small {
width: 480px;
}
.large {
width: 100%;
}
.token {
flex: 0 0 50%;
padding: 6px 16px;
background-color: #eee;
font-family: $monoSpaceFontFamily;
}
.example {
display: flex;
align-items: center;
justify-content: space-between;
flex: 0 0 50%;
padding: 6px 16px;
background-color: #ddd;
.footNote {
padding: 2px;
color: #aaa;
}
}
.isFullFilename {
.token,
.example {
flex: 1 0 auto;
}
}
@media only screen and (max-width: $breakpointSmall) {
.option.small {
width: 100%;
}
}
@media only screen and (max-width: $breakpointExtraSmall) {
.token,
.example {
flex: 1 0 auto;
}
}

View File

@@ -0,0 +1,83 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons, sizes } from 'Helpers/Props';
import styles from './QueryParameterOption.css';
class QueryParameterOption extends Component {
//
// Listeners
onPress = () => {
const {
token,
tokenSeparator,
isFullFilename,
onPress
} = this.props;
let tokenValue = token;
tokenValue = tokenValue.replace(/ /g, tokenSeparator);
onPress({ isFullFilename, tokenValue });
}
//
// Render
render() {
const {
token,
tokenSeparator,
example,
footNote,
isFullFilename,
size
} = this.props;
return (
<Link
className={classNames(
styles.option,
styles[size],
isFullFilename && styles.isFullFilename
)}
onPress={this.onPress}
>
<div className={styles.token}>
{token.replace(/ /g, tokenSeparator)}
</div>
<div className={styles.example}>
{example.replace(/ /g, tokenSeparator)}
{
footNote !== 0 &&
<Icon className={styles.footNote} name={icons.FOOTNOTE} />
}
</div>
</Link>
);
}
}
QueryParameterOption.propTypes = {
token: PropTypes.string.isRequired,
example: PropTypes.string.isRequired,
footNote: PropTypes.number.isRequired,
tokenSeparator: PropTypes.string.isRequired,
isFullFilename: PropTypes.bool.isRequired,
size: PropTypes.oneOf([sizes.SMALL, sizes.LARGE]),
onPress: PropTypes.func.isRequired
};
QueryParameterOption.defaultProps = {
footNote: 0,
size: sizes.SMALL,
isFullFilename: false
};
export default QueryParameterOption;

View File

@@ -31,6 +31,12 @@
height: 35px;
}
.selectedReleasesLabel {
margin-bottom: 3px;
text-align: right;
font-weight: bold;
}
@media only screen and (max-width: $breakpointSmall) {
.inputContainer {
margin-right: 0;
@@ -47,8 +53,4 @@
.buttons {
justify-content: space-between;
}
.selectedMovieLabel {
text-align: left;
}
}

View File

@@ -1,13 +1,17 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import IndexersSelectInputConnector from 'Components/Form/IndexersSelectInputConnector';
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import keyboardShortcuts from 'Components/keyboardShortcuts';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { icons, inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import QueryParameterModal from './QueryParameterModal';
import SearchFooterLabel from './SearchFooterLabel';
import styles from './SearchFooter.css';
@@ -22,10 +26,14 @@ class SearchFooter extends Component {
const {
defaultIndexerIds,
defaultCategories,
defaultSearchQuery
defaultSearchQuery,
defaultSearchType
} = props;
this.state = {
isQueryParameterModalOpen: false,
queryModalOptions: null,
searchType: defaultSearchType,
searchingReleases: false,
searchQuery: defaultSearchQuery || '',
searchIndexerIds: defaultIndexerIds,
@@ -53,12 +61,14 @@ class SearchFooter extends Component {
defaultIndexerIds,
defaultCategories,
defaultSearchQuery,
defaultSearchType,
searchError
} = this.props;
const {
searchIndexerIds,
searchCategories
searchCategories,
searchType
} = this.state;
const newState = {};
@@ -67,6 +77,10 @@ class SearchFooter extends Component {
newState.searchQuery = defaultSearchQuery;
}
if (searchType !== defaultSearchType) {
newState.searchType = defaultSearchType;
}
if (searchIndexerIds !== defaultIndexerIds) {
newState.searchIndexerIds = defaultIndexerIds;
}
@@ -87,8 +101,21 @@ class SearchFooter extends Component {
//
// Listeners
onQueryParameterModalOpenClick = () => {
this.setState({
queryModalOptions: {
name: 'queryParameters'
},
isQueryParameterModalOpen: true
});
}
onQueryParameterModalClose = () => {
this.setState({ isQueryParameterModalOpen: false });
}
onSearchPress = () => {
this.props.onSearchPress(this.state.searchQuery, this.state.searchIndexerIds, this.state.searchCategories);
this.props.onSearchPress(this.state.searchQuery, this.state.searchIndexerIds, this.state.searchCategories, this.state.searchType);
}
onSearchInputChange = ({ value }) => {
@@ -101,36 +128,77 @@ class SearchFooter extends Component {
render() {
const {
isFetching,
isPopulated,
isGrabbing,
hasIndexers,
onInputChange
onInputChange,
onBulkGrabPress,
itemCount,
selectedCount
} = this.props;
const {
searchQuery,
searchIndexerIds,
searchCategories
searchCategories,
isQueryParameterModalOpen,
queryModalOptions,
searchType
} = this.state;
let icon = icons.SEARCH;
switch (searchType) {
case 'book':
icon = icons.BOOK;
break;
case 'tvsearch':
icon = icons.TV;
break;
case 'movie':
icon = icons.FILM;
break;
case 'music':
icon = icons.AUDIO;
break;
default:
icon = icons.SEARCH;
}
let footerLabel = `Search ${searchIndexerIds.length === 0 ? 'all' : searchIndexerIds.length} Indexers`;
if (isPopulated) {
footerLabel = selectedCount === 0 ? `Found ${itemCount} releases` : `Selected ${selectedCount} of ${itemCount} releases`;
}
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<SearchFooterLabel
label={'Query'}
label={translate('Query')}
isSaving={false}
/>
<TextInput
name='searchQuery'
autoFocus={true}
<FormInputGroup
type={inputTypes.TEXT}
name="searchQuery"
value={searchQuery}
isDisabled={isFetching}
buttons={
<FormInputButton onPress={this.onQueryParameterModalOpenClick}>
<Icon
name={icon}
/>
</FormInputButton>}
onChange={this.onSearchInputChange}
onFocus={this.onApikeyFocus}
isDisabled={isFetching}
{...searchQuery}
/>
</div>
<div className={styles.indexerContainer}>
<SearchFooterLabel
label={'Indexers'}
label={translate('Indexers')}
isSaving={false}
/>
@@ -144,7 +212,7 @@ class SearchFooter extends Component {
<div className={styles.indexerContainer}>
<SearchFooterLabel
label={'Categories'}
label={translate('Categories')}
isSaving={false}
/>
@@ -159,12 +227,26 @@ class SearchFooter extends Component {
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<SearchFooterLabel
label={`Search ${searchIndexerIds.length === 0 ? 'all' : searchIndexerIds.length} Indexers`}
className={styles.selectedReleasesLabel}
label={footerLabel}
isSaving={false}
/>
<div className={styles.buttons}>
{
isPopulated &&
<SpinnerButton
className={styles.searchButton}
kind={kinds.SUCCESS}
isSpinning={isGrabbing}
isDisabled={isFetching || !hasIndexers || selectedCount === 0}
onPress={onBulkGrabPress}
>
{translate('Grab Releases')}
</SpinnerButton>
}
<SpinnerButton
className={styles.searchButton}
isSpinning={isFetching}
@@ -176,6 +258,17 @@ class SearchFooter extends Component {
</div>
</div>
</div>
<QueryParameterModal
isOpen={isQueryParameterModalOpen}
{...queryModalOptions}
name='queryParameters'
value={searchQuery}
searchType={searchType}
onSearchInputChange={this.onSearchInputChange}
onInputChange={onInputChange}
onModalClose={this.onQueryParameterModalClose}
/>
</PageContentFooter>
);
}
@@ -185,8 +278,14 @@ SearchFooter.propTypes = {
defaultIndexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
defaultCategories: PropTypes.arrayOf(PropTypes.number).isRequired,
defaultSearchQuery: PropTypes.string.isRequired,
defaultSearchType: PropTypes.string.isRequired,
selectedCount: PropTypes.number.isRequired,
itemCount: PropTypes.number.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired,
onSearchPress: PropTypes.func.isRequired,
onBulkGrabPress: PropTypes.func.isRequired,
hasIndexers: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
searchError: PropTypes.object,

View File

@@ -12,13 +12,15 @@ function createMapStateToProps() {
const {
searchQuery: defaultSearchQuery,
searchIndexerIds: defaultIndexerIds,
searchCategories: defaultCategories
searchCategories: defaultCategories,
searchType: defaultSearchType
} = releases.defaults;
return {
defaultSearchQuery,
defaultIndexerIds,
defaultCategories
defaultCategories,
defaultSearchType
};
}
);

View File

@@ -18,6 +18,9 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
import NoSearchResults from './NoSearchResults';
@@ -44,12 +47,16 @@ class SearchIndex extends Component {
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: false,
searchType: null,
lastToggled: null
lastToggled: null,
allSelected: false,
allUnselected: false,
selectedState: {}
};
}
componentDidMount() {
this.setJumpBarItems();
this.setSelectedState();
window.addEventListener('keyup', this.onKeyUp);
}
@@ -66,6 +73,7 @@ class SearchIndex extends Component {
hasDifferentItemsOrOrder(prevProps.items, items)
) {
this.setJumpBarItems();
this.setSelectedState();
}
if (this.state.jumpToCharacter != null) {
@@ -80,6 +88,48 @@ class SearchIndex extends Component {
this.setState({ scroller: ref });
}
getSelectedIds = () => {
if (this.state.allUnselected) {
return [];
}
return getSelectedIds(this.state.selectedState, { parseIds: false });
}
setSelectedState() {
const {
items
} = this.props;
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((release) => {
const isItemSelected = selectedState[release.guid];
if (isItemSelected) {
newSelectedState[release.guid] = isItemSelected;
} else {
newSelectedState[release.guid] = false;
}
});
const selectedCount = getSelectedIds(newSelectedState).length;
const newStateCount = Object.keys(newSelectedState).length;
let isAllSelected = false;
let isAllUnselected = false;
if (selectedCount === 0) {
isAllUnselected = true;
} else if (selectedCount === newStateCount) {
isAllSelected = true;
}
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
}
setJumpBarItems() {
const {
items,
@@ -146,8 +196,14 @@ class SearchIndex extends Component {
this.setState({ jumpToCharacter });
}
onSearchPress = (query, indexerIds, categories) => {
this.props.onSearchPress({ query, indexerIds, categories });
onSearchPress = (query, indexerIds, categories, type) => {
this.props.onSearchPress({ query, indexerIds, categories, type });
}
onBulkGrabPress = () => {
const selectedIds = this.getSelectedIds();
const result = _.filter(this.props.items, (release) => _.indexOf(selectedIds, release.guid) !== -1);
this.props.onBulkGrabPress(result);
}
onKeyUp = (event) => {
@@ -162,6 +218,20 @@ class SearchIndex extends Component {
}
}
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
//
// Render
@@ -169,7 +239,9 @@ class SearchIndex extends Component {
const {
isFetching,
isPopulated,
isGrabbing,
error,
grabError,
totalItems,
items,
columns,
@@ -190,9 +262,14 @@ class SearchIndex extends Component {
jumpBarItems,
isAddIndexerModalOpen,
isEditIndexerModalOpen,
jumpToCharacter
jumpToCharacter,
selectedState,
allSelected,
allUnselected
} = this.state;
const selectedIndexerIds = this.getSelectedIds();
const ViewComponent = getViewComponent();
const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoIndexer = !totalItems;
@@ -263,6 +340,11 @@ class SearchIndex extends Component {
sortDirection={sortDirection}
columns={columns}
jumpToCharacter={jumpToCharacter}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
{...otherProps}
/>
</div>
@@ -293,8 +375,14 @@ class SearchIndex extends Component {
<SearchFooterConnector
isFetching={isFetching}
isPopulated={isPopulated}
isGrabbing={isGrabbing}
grabError={grabError}
selectedCount={selectedIndexerIds.length}
itemCount={items.length}
hasIndexers={hasIndexers}
onSearchPress={this.onSearchPress}
onBulkGrabPress={this.onBulkGrabPress}
/>
<AddIndexerModal
@@ -314,7 +402,9 @@ class SearchIndex extends Component {
SearchIndex.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired,
error: PropTypes.object,
grabError: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -327,6 +417,7 @@ SearchIndex.propTypes = {
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired,
onBulkGrabPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
hasIndexers: PropTypes.bool.isRequired
};

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import withScrollPosition from 'Components/withScrollPosition';
import { cancelFetchReleases, clearReleases, fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions';
import { bulkGrabReleases, cancelFetchReleases, clearReleases, fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions';
import scrollPositions from 'Store/scrollPositions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector';
@@ -46,6 +46,10 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(fetchReleases(payload));
},
onBulkGrabPress(payload) {
dispatch(bulkGrabReleases(payload));
},
dispatchCancelFetchReleases() {
dispatch(cancelFetchReleases());
},
@@ -83,6 +87,7 @@ class SearchIndexConnector extends Component {
SearchIndexConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
onSearchPress: PropTypes.func.isRequired,
onBulkGrabPress: PropTypes.func.isRequired,
dispatchCancelFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired,
items: PropTypes.arrayOf(PropTypes.object)

View File

@@ -10,18 +10,13 @@
flex: 4 0 110px;
}
.indexer,
.category {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 110px;
}
.indexer {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 85px;
}
.age,
.size,
.files,

View File

@@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton';
import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
import styles from './SearchIndexHeader.css';
@@ -38,6 +39,9 @@ class SearchIndexHeader extends Component {
const {
columns,
onTableOptionChange,
allSelected,
allUnselected,
onSelectAllChange,
...otherProps
} = this.props;
@@ -56,6 +60,17 @@ class SearchIndexHeader extends Component {
return null;
}
if (name === 'select') {
return (
<VirtualTableSelectAllHeaderCell
key={name}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
);
}
if (name === 'actions') {
return (
<VirtualTableHeaderCell
@@ -100,6 +115,9 @@ class SearchIndexHeader extends Component {
SearchIndexHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
onTableOptionChange: PropTypes.func.isRequired
};

View File

@@ -17,18 +17,13 @@
flex: 4 0 110px;
}
.indexer,
.category {
composes: cell;
flex: 0 0 110px;
}
.indexer {
composes: cell;
flex: 0 0 85px;
}
.age,
.size,
.files,

View File

@@ -5,6 +5,7 @@ import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime';
@@ -75,6 +76,7 @@ class SearchIndexRow extends Component {
render() {
const {
guid,
protocol,
categories,
age,
@@ -96,7 +98,9 @@ class SearchIndexRow extends Component {
isGrabbed,
grabError,
longDateFormat,
timeFormat
timeFormat,
isSelected,
onSelectedChange
} = this.props;
return (
@@ -111,6 +115,19 @@ class SearchIndexRow extends Component {
return null;
}
if (column.name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={guid}
key={column.name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (column.name === 'protocol') {
return (
<VirtualTableRowCell
@@ -322,7 +339,9 @@ SearchIndexRow.propTypes = {
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
timeFormat: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
SearchIndexRow.defaultProps = {

View File

@@ -49,6 +49,8 @@ class SearchIndexTable extends Component {
columns,
longDateFormat,
timeFormat,
selectedState,
onSelectedChange,
onGrabPress
} = this.props;
@@ -64,6 +66,8 @@ class SearchIndexTable extends Component {
component={SearchIndexRow}
columns={columns}
guid={release.guid}
isSelected={selectedState[release.guid]}
onSelectedChange={onSelectedChange}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
onGrabPress={onGrabPress}
@@ -83,7 +87,11 @@ class SearchIndexTable extends Component {
sortDirection,
isSmallScreen,
onSortPress,
scroller
scroller,
allSelected,
allUnselected,
onSelectAllChange,
selectedState
} = this.props;
return (
@@ -102,8 +110,12 @@ class SearchIndexTable extends Component {
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
}
selectedState={selectedState}
columns={columns}
/>
);
@@ -121,7 +133,12 @@ SearchIndexTable.propTypes = {
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired
onGrabPress: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onSelectAllChange: PropTypes.func.isRequired
};
export default SearchIndexTable;