Refactor Indexer index to use react-window

(cherry picked from commit d022679b7dcbce3cec98e6a1fd0879e3c0d92523)
This commit is contained in:
Mark McDowall
2023-01-05 18:20:49 -08:00
committed by Qstick
parent c2599ef2e7
commit c0383ad5f5
314 changed files with 4928 additions and 3017 deletions
@@ -1,57 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
function CapabilitiesLabel(props) {
const {
categoryFilter
} = props;
const {
categories
} = props.capabilities;
let filteredList = categories.filter((item) => item.id < 100000);
if (categoryFilter.length > 0) {
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id) || (item.subCategories && item.subCategories.some((r) => categoryFilter.includes(r.id))));
}
const nameList = filteredList.map((item) => item.name).sort();
return (
<span>
{
nameList.map((category) => {
return (
<Label key={category}>
{category}
</Label>
);
})
}
{
filteredList.length === 0 ?
<Label>
{'None'}
</Label> :
null
}
</span>
);
}
CapabilitiesLabel.propTypes = {
capabilities: PropTypes.object.isRequired,
categoryFilter: PropTypes.arrayOf(PropTypes.number).isRequired
};
CapabilitiesLabel.defaultProps = {
capabilities: {
categories: []
},
categoryFilter: []
};
export default CapabilitiesLabel;
@@ -0,0 +1,39 @@
import React from 'react';
import Label from 'Components/Label';
import { IndexerCapabilities } from 'Indexer/Indexer';
interface CapabilitiesLabelProps {
capabilities: IndexerCapabilities;
categoryFilter?: number[];
}
function CapabilitiesLabel(props: CapabilitiesLabelProps) {
const { categoryFilter = [] } = props;
const { categories = [] } = props.capabilities;
let filteredList = categories.filter((item) => item.id < 100000);
if (categoryFilter.length > 0) {
filteredList = filteredList.filter(
(item) =>
categoryFilter.includes(item.id) ||
(item.subCategories &&
item.subCategories.some((r) => categoryFilter.includes(r.id)))
);
}
const nameList = filteredList.map((item) => item.name).sort();
return (
<span>
{nameList.map((category) => {
return <Label key={category}>{category}</Label>;
})}
{filteredList.length === 0 ? <Label>{'None'}</Label> : null}
</span>
);
}
export default CapabilitiesLabel;
@@ -1,103 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import { icons } from 'Helpers/Props';
import DeleteMovieModal from 'Indexer/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Indexer/Edit/EditMovieModalConnector';
import translate from 'Utilities/String/translate';
class IndexerIndexActionsCell extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false
};
}
//
// Listeners
onEditMoviePress = () => {
this.setState({ isEditMovieModalOpen: true });
};
onEditMovieModalClose = () => {
this.setState({ isEditMovieModalOpen: false });
};
onDeleteMoviePress = () => {
this.setState({
isEditMovieModalOpen: false,
isDeleteMovieModalOpen: true
});
};
onDeleteMovieModalClose = () => {
this.setState({ isDeleteMovieModalOpen: false });
};
//
// Render
render() {
const {
id,
isRefreshingMovie,
onRefreshMoviePress,
...otherProps
} = this.props;
const {
isEditMovieModalOpen,
isDeleteMovieModalOpen
} = this.state;
return (
<VirtualTableRowCell
{...otherProps}
>
<SpinnerIconButton
name={icons.REFRESH}
title={translate('RefreshMovie')}
isSpinning={isRefreshingMovie}
onPress={onRefreshMoviePress}
/>
<IconButton
name={icons.EDIT}
title={translate('EditIndexer')}
onPress={this.onEditMoviePress}
/>
<EditMovieModalConnector
isOpen={isEditMovieModalOpen}
indexerId={id}
onModalClose={this.onEditMovieModalClose}
onDeleteMoviePress={this.onDeleteMoviePress}
/>
<DeleteMovieModal
isOpen={isDeleteMovieModalOpen}
indexerId={id}
onModalClose={this.onDeleteMovieModalClose}
/>
</VirtualTableRowCell>
);
}
}
IndexerIndexActionsCell.propTypes = {
id: PropTypes.number.isRequired,
isRefreshingMovie: PropTypes.bool.isRequired,
onRefreshMoviePress: PropTypes.func.isRequired
};
export default IndexerIndexActionsCell;
@@ -1,132 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
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 IndexerIndexTableOptionsConnector from './IndexerIndexTableOptionsConnector';
import styles from './IndexerIndexHeader.css';
class IndexerIndexHeader extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isTableOptionsModalOpen: false
};
}
//
// Listeners
onTableOptionsPress = () => {
this.setState({ isTableOptionsModalOpen: true });
};
onTableOptionsModalClose = () => {
this.setState({ isTableOptionsModalOpen: false });
};
//
// Render
render() {
const {
columns,
onTableOptionChange,
allSelected,
allUnselected,
onSelectAllChange,
isMovieEditorActive,
...otherProps
} = this.props;
return (
<VirtualTableHeader>
{
columns.map((column) => {
const {
name,
label,
isSortable,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'select') {
if (isMovieEditorActive) {
return (
<VirtualTableSelectAllHeaderCell
key={name}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
);
}
return null;
}
if (name === 'actions') {
return (
<VirtualTableHeaderCell
key={name}
className={styles[name]}
name={name}
isSortable={false}
{...otherProps}
>
<IconButton
name={icons.ADVANCED_SETTINGS}
onPress={this.onTableOptionsPress}
/>
</VirtualTableHeaderCell>
);
}
return (
<VirtualTableHeaderCell
key={name}
className={styles[name]}
name={name}
isSortable={isSortable}
{...otherProps}
>
{label}
</VirtualTableHeaderCell>
);
})
}
<TableOptionsModal
isOpen={this.state.isTableOptionsModalOpen}
columns={columns}
optionsComponent={IndexerIndexTableOptionsConnector}
onTableOptionChange={onTableOptionChange}
onModalClose={this.onTableOptionsModalClose}
/>
</VirtualTableHeader>
);
}
}
IndexerIndexHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isMovieEditorActive: PropTypes.bool.isRequired
};
export default IndexerIndexHeader;
@@ -1,13 +0,0 @@
import { connect } from 'react-redux';
import { setMovieTableOption } from 'Store/Actions/indexerIndexActions';
import IndexerIndexHeader from './IndexerIndexHeader';
function createMapDispatchToProps(dispatch, props) {
return {
onTableOptionChange(payload) {
dispatch(setMovieTableOption(payload));
}
};
}
export default connect(undefined, createMapDispatchToProps)(IndexerIndexHeader);
@@ -0,0 +1,19 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actions': string;
'added': string;
'appProfileId': string;
'capabilities': string;
'cell': string;
'checkInput': string;
'externalLink': string;
'priority': string;
'privacy': string;
'protocol': string;
'sortName': string;
'status': string;
'tags': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,323 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import IndexerInfoModal from 'Indexer/Info/IndexerInfoModal';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import CapabilitiesLabel from './CapabilitiesLabel';
import IndexerStatusCell from './IndexerStatusCell';
import ProtocolLabel from './ProtocolLabel';
import styles from './IndexerIndexRow.css';
class IndexerIndexRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditIndexerModalOpen: false,
isDeleteMovieModalOpen: false,
isIndexerInfoModalOpen: false
};
}
onEditIndexerPress = () => {
this.setState({ isEditIndexerModalOpen: true });
};
onIndexerInfoPress = () => {
this.setState({ isIndexerInfoModalOpen: true });
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
onIndexerInfoModalClose = () => {
this.setState({ isIndexerInfoModalOpen: false });
};
onDeleteMoviePress = () => {
this.setState({
isEditIndexerModalOpen: false,
isDeleteMovieModalOpen: true
});
};
onDeleteMovieModalClose = () => {
this.setState({ isDeleteMovieModalOpen: false });
};
onUseSceneNumberingChange = () => {
// Mock handler to satisfy `onChange` being required for `CheckInput`.
//
};
//
// Render
render() {
const {
id,
name,
indexerUrls,
enable,
redirect,
tags,
protocol,
privacy,
priority,
status,
fields,
appProfile,
added,
capabilities,
columns,
longDateFormat,
timeFormat,
isMovieEditorActive,
isSelected,
onSelectedChange
} = this.props;
const {
isEditIndexerModalOpen,
isDeleteMovieModalOpen,
isIndexerInfoModalOpen
} = this.state;
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? (Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
return (
<>
{
columns.map((column) => {
const {
isVisible
} = column;
if (!isVisible) {
return null;
}
if (isMovieEditorActive && column.name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={id}
key={column.name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (column.name === 'status') {
return (
<IndexerStatusCell
key={column.name}
className={styles[column.name]}
enabled={enable}
redirect={redirect}
status={status}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
component={VirtualTableRowCell}
/>
);
}
if (column.name === 'sortName') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{name}
</VirtualTableRowCell>
);
}
if (column.name === 'privacy') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<Label>
{titleCase(privacy)}
</Label>
</VirtualTableRowCell>
);
}
if (column.name === 'priority') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{priority}
</VirtualTableRowCell>
);
}
if (column.name === 'protocol') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<ProtocolLabel
protocol={protocol}
/>
</VirtualTableRowCell>
);
}
if (column.name === 'appProfileId') {
return (
<VirtualTableRowCell
key={name}
className={styles[column.name]}
>
{appProfile?.name || ''}
</VirtualTableRowCell>
);
}
if (column.name === 'capabilities') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<CapabilitiesLabel
capabilities={capabilities}
/>
</VirtualTableRowCell>
);
}
if (column.name === 'added') {
return (
<RelativeDateCellConnector
key={column.name}
className={styles[column.name]}
date={added}
component={VirtualTableRowCell}
/>
);
}
if (column.name === 'tags') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<TagListConnector
tags={tags}
/>
</VirtualTableRowCell>
);
}
if (column.name === 'actions') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<IconButton
name={icons.INFO}
title={translate('IndexerInfo')}
onPress={this.onIndexerInfoPress}
/>
{
baseUrl ?
<IconButton
className={styles.externalLink}
name={icons.EXTERNAL_LINK}
title={translate('Website')}
to={baseUrl.replace(/(:\/\/)api\./, '$1')}
/> : null
}
<IconButton
name={icons.EDIT}
title={translate('EditIndexer')}
onPress={this.onEditIndexerPress}
/>
</VirtualTableRowCell>
);
}
return null;
})
}
<EditIndexerModalConnector
id={id}
isOpen={isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
onDeleteIndexerPress={this.onDeleteMoviePress}
/>
<IndexerInfoModal
indexerId={id}
isOpen={isIndexerInfoModalOpen}
onModalClose={this.onIndexerInfoModalClose}
/>
<DeleteIndexerModal
isOpen={isDeleteMovieModalOpen}
indexerId={id}
onModalClose={this.onDeleteMovieModalClose}
/>
</>
);
}
}
IndexerIndexRow.propTypes = {
id: PropTypes.number.isRequired,
indexerUrls: PropTypes.arrayOf(PropTypes.string),
protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enable: PropTypes.bool.isRequired,
redirect: PropTypes.bool.isRequired,
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
appProfile: PropTypes.object.isRequired,
status: PropTypes.object,
capabilities: PropTypes.object,
added: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isMovieEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
};
IndexerIndexRow.defaultProps = {
tags: []
};
export default IndexerIndexRow;
@@ -0,0 +1,256 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { SelectActionType, useSelect } from 'App/SelectContext';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItemSelector';
import IndexerInfoModal from 'Indexer/Info/IndexerInfoModal';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import CapabilitiesLabel from './CapabilitiesLabel';
import IndexerStatusCell from './IndexerStatusCell';
import ProtocolLabel from './ProtocolLabel';
import styles from './IndexerIndexRow.css';
interface IndexerIndexRowProps {
indexerId: number;
sortKey: string;
columns: Column[];
isSelectMode: boolean;
}
function IndexerIndexRow(props: IndexerIndexRowProps) {
const { indexerId, columns, isSelectMode } = props;
const { indexer, appProfile } = useSelector(
createIndexerIndexItemSelector(props.indexerId)
);
const {
name: indexerName,
indexerUrls,
enable,
redirect,
tags,
protocol,
privacy,
priority,
status,
fields,
added,
capabilities,
} = indexer;
const baseUrl =
fields.find((field) => field.name === 'baseUrl')?.value ??
(Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
const [isIndexerInfoModalOpen, setIsIndexerInfoModalOpen] = useState(false);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
useState(false);
const [selectState, selectDispatch] = useSelect();
const onEditIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(true);
}, [setIsEditIndexerModalOpen]);
const onEditIndexerModalClose = useCallback(() => {
setIsEditIndexerModalOpen(false);
}, [setIsEditIndexerModalOpen]);
const onIndexerInfoPress = useCallback(() => {
setIsIndexerInfoModalOpen(true);
}, [setIsIndexerInfoModalOpen]);
const onIndexerInfoModalClose = useCallback(() => {
setIsIndexerInfoModalOpen(false);
}, [setIsIndexerInfoModalOpen]);
const onDeleteIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(false);
setIsDeleteIndexerModalOpen(true);
}, [setIsDeleteIndexerModalOpen]);
const onDeleteIndexerModalClose = useCallback(() => {
setIsDeleteIndexerModalOpen(false);
}, [setIsDeleteIndexerModalOpen]);
const checkInputCallback = useCallback(() => {
// Mock handler to satisfy `onChange` being required for `CheckInput`.
}, []);
const onSelectedChange = useCallback(
({ id, value, shiftKey }) => {
selectDispatch({
type: SelectActionType.ToggleSelected,
id,
isSelected: value,
shiftKey,
});
},
[selectDispatch]
);
return (
<>
{isSelectMode ? (
<VirtualTableSelectCell
id={indexerId}
isSelected={selectState.selectedState[indexerId]}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
) : null}
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<IndexerStatusCell
key={name}
className={styles[name]}
enabled={enable}
redirect={redirect}
status={status}
component={VirtualTableRowCell}
/>
);
}
if (name === 'sortName') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{indexerName}
</VirtualTableRowCell>
);
}
if (name === 'privacy') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<Label>{titleCase(privacy)}</Label>
</VirtualTableRowCell>
);
}
if (name === 'priority') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{priority}
</VirtualTableRowCell>
);
}
if (name === 'protocol') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<ProtocolLabel protocol={protocol} />
</VirtualTableRowCell>
);
}
if (name === 'appProfileId') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{appProfile?.name || ''}
</VirtualTableRowCell>
);
}
if (name === 'capabilities') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<CapabilitiesLabel capabilities={capabilities} />
</VirtualTableRowCell>
);
}
if (name === 'added') {
return (
<RelativeDateCellConnector
key={name}
className={styles[name]}
date={added.toString()}
component={VirtualTableRowCell}
/>
);
}
if (name === 'tags') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<TagListConnector tags={tags} />
</VirtualTableRowCell>
);
}
if (name === 'actions') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<IconButton
name={icons.INFO}
title={translate('IndexerInfo')}
onPress={onIndexerInfoPress}
/>
{baseUrl ? (
<IconButton
className={styles.externalLink}
name={icons.EXTERNAL_LINK}
title={translate('Website')}
to={baseUrl.replace('api.', '')}
/>
) : null}
<IconButton
name={icons.EDIT}
title={translate('EditIndexer')}
onPress={onEditIndexerPress}
/>
</VirtualTableRowCell>
);
}
return null;
})}
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
id={indexerId}
onModalClose={onEditIndexerModalClose}
onDeleteIndexerPress={onDeleteIndexerPress}
/>
<IndexerInfoModal
indexerId={indexerId}
isOpen={isIndexerInfoModalOpen}
onModalClose={onIndexerInfoModalClose}
/>
<DeleteIndexerModal
isOpen={isDeleteIndexerModalOpen}
indexerId={indexerId}
onModalClose={onDeleteIndexerModalClose}
/>
</>
);
}
export default IndexerIndexRow;
@@ -1,5 +1,3 @@
.tableContainer {
composes: tableContainer from '~Components/Table/VirtualTable.css';
flex: 1 0 auto;
.tableScroller {
position: relative;
}
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'tableScroller': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,140 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import VirtualTable from 'Components/Table/VirtualTable';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import { sortDirections } from 'Helpers/Props';
import IndexerIndexItemConnector from 'Indexer/Index/IndexerIndexItemConnector';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import IndexerIndexHeaderConnector from './IndexerIndexHeaderConnector';
import IndexerIndexRow from './IndexerIndexRow';
import styles from './IndexerIndexTable.css';
class IndexerIndexTable extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
scrollIndex: null
};
}
componentDidUpdate(prevProps) {
const {
items,
jumpToCharacter
} = this.props;
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter);
if (scrollIndex != null) {
this.setState({ scrollIndex });
}
} else if (jumpToCharacter == null && prevProps.jumpToCharacter != null) {
this.setState({ scrollIndex: null });
}
}
//
// Control
rowRenderer = ({ key, rowIndex, style }) => {
const {
items,
columns,
selectedState,
onSelectedChange,
isMovieEditorActive
} = this.props;
const movie = items[rowIndex];
return (
<VirtualTableRow
key={key}
style={style}
>
<IndexerIndexItemConnector
key={movie.id}
component={IndexerIndexRow}
columns={columns}
indexerId={movie.id}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/>
</VirtualTableRow>
);
};
//
// Render
render() {
const {
items,
columns,
sortKey,
sortDirection,
isSmallScreen,
onSortPress,
scroller,
allSelected,
allUnselected,
onSelectAllChange,
isMovieEditorActive,
selectedState
} = this.props;
return (
<VirtualTable
className={styles.tableContainer}
items={items}
scrollIndex={this.state.scrollIndex}
isSmallScreen={isSmallScreen}
scroller={scroller}
rowHeight={38}
overscanRowCount={2}
rowRenderer={this.rowRenderer}
header={
<IndexerIndexHeaderConnector
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
isMovieEditorActive={isMovieEditorActive}
/>
}
selectedState={selectedState}
columns={columns}
/>
);
}
}
IndexerIndexTable.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
jumpToCharacter: PropTypes.string,
isSmallScreen: PropTypes.bool.isRequired,
scroller: PropTypes.instanceOf(Element).isRequired,
onSortPress: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isMovieEditorActive: PropTypes.bool.isRequired
};
export default IndexerIndexTable;
@@ -0,0 +1,213 @@
import { throttle } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import Scroller from 'Components/Scroller/Scroller';
import Column from 'Components/Table/Column';
import useMeasure from 'Helpers/Hooks/useMeasure';
import ScrollDirection from 'Helpers/Props/ScrollDirection';
import SortDirection from 'Helpers/Props/SortDirection';
import Indexer from 'Indexer/Indexer';
import dimensions from 'Styles/Variables/dimensions';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import IndexerIndexRow from './IndexerIndexRow';
import IndexerIndexTableHeader from './IndexerIndexTableHeader';
import selectTableOptions from './selectTableOptions';
import styles from './IndexerIndexTable.css';
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
const bodyPaddingSmallScreen = parseInt(
dimensions.pageContentBodyPaddingSmallScreen
);
interface RowItemData {
items: Indexer[];
sortKey: string;
columns: Column[];
isSelectMode: boolean;
}
interface IndexerIndexTableProps {
items: Indexer[];
sortKey?: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const columnsSelector = createSelector(
(state) => state.indexerIndex.columns,
(columns) => columns
);
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
index,
style,
data,
}) => {
const { items, sortKey, columns, isSelectMode } = data;
if (index >= items.length) {
return null;
}
const indexer = items[index];
return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
...style,
}}
>
<IndexerIndexRow
indexerId={indexer.id}
sortKey={sortKey}
columns={columns}
isSelectMode={isSelectMode}
/>
</div>
);
};
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;
}
function IndexerIndexTable(props: IndexerIndexTableProps) {
const {
items,
sortKey,
sortDirection,
jumpToCharacter,
isSelectMode,
isSmallScreen,
scrollerRef,
} = props;
const columns = useSelector(columnsSelector);
const { showBanners } = useSelector(selectTableOptions);
const listRef: React.MutableRefObject<List> = useRef();
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const rowHeight = useMemo(() => {
return showBanners ? 70 : 38;
}, [showBanners]);
useEffect(() => {
const current = scrollerRef.current as HTMLElement;
if (isSmallScreen) {
setSize({
width: windowWidth,
height: windowHeight,
});
return;
}
if (current) {
const width = current.clientWidth;
const padding =
(isSmallScreen ? bodyPaddingSmallScreen : bodyPadding) - 5;
setSize({
width: width - padding * 2,
height: windowHeight,
});
}
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
const scrollTop =
(isSmallScreen
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
listRef.current.scrollTo(scrollTop);
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
return () => {
handleScroll.cancel();
if (currentScrollListener) {
currentScrollListener.removeEventListener('scroll', handleScroll);
}
};
}, [isSmallScreen, listRef, scrollerRef]);
useEffect(() => {
if (jumpToCharacter) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (index != null) {
let scrollTop = index * rowHeight;
// If the offset is zero go to the top, otherwise offset
// by the approximate size of the header + padding (37 + 20).
if (scrollTop > 0) {
const offset = 57;
scrollTop += offset;
}
listRef.current.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop);
}
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
return (
<div ref={measureRef}>
<Scroller
className={styles.tableScroller}
scrollDirection={ScrollDirection.Horizontal}
>
<IndexerIndexTableHeader
showBanners={showBanners}
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
isSelectMode={isSelectMode}
/>
<List<RowItemData>
ref={listRef}
style={{
width: '100%',
height: '100%',
overflow: 'none',
}}
width={size.width}
height={size.height}
itemCount={items.length}
itemSize={rowHeight}
itemData={{
items,
sortKey,
columns,
isSelectMode,
}}
>
{Row}
</List>
</Scroller>
</div>
);
}
export default IndexerIndexTable;
@@ -1,29 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setMovieSort } from 'Store/Actions/indexerIndexActions';
import IndexerIndexTable from './IndexerIndexTable';
function createMapStateToProps() {
return createSelector(
(state) => state.app.dimensions,
(state) => state.indexerIndex.tableOptions,
(state) => state.indexerIndex.columns,
(dimensions, tableOptions, columns) => {
return {
isSmallScreen: dimensions.isSmallScreen,
showBanners: tableOptions.showBanners,
columns
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onSortPress(sortKey) {
dispatch(setMovieSort({ sortKey }));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(IndexerIndexTable);
@@ -0,0 +1,16 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actions': string;
'added': string;
'appProfileId': string;
'capabilities': string;
'priority': string;
'privacy': string;
'protocol': string;
'sortName': string;
'status': string;
'tags': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,110 @@
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { SelectActionType, useSelect } from 'App/SelectContext';
import IconButton from 'Components/Link/IconButton';
import Column from 'Components/Table/Column';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
setIndexerSort,
setIndexerTableOption,
} from 'Store/Actions/indexerIndexActions';
import IndexerIndexTableOptions from './IndexerIndexTableOptions';
import styles from './IndexerIndexTableHeader.css';
interface IndexerIndexTableHeaderProps {
showBanners: boolean;
columns: Column[];
sortKey?: string;
sortDirection?: SortDirection;
isSelectMode: boolean;
}
function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) {
const { columns, sortKey, sortDirection, isSelectMode } = props;
const dispatch = useDispatch();
const [selectState, selectDispatch] = useSelect();
const onSortPress = useCallback(
(value) => {
dispatch(setIndexerSort({ sortKey: value }));
},
[dispatch]
);
const onTableOptionChange = useCallback(
(payload) => {
dispatch(setIndexerTableOption(payload));
},
[dispatch]
);
const onSelectAllChange = useCallback(
({ value }) => {
selectDispatch({
type: value ? SelectActionType.SelectAll : SelectActionType.UnselectAll,
});
},
[selectDispatch]
);
return (
<VirtualTableHeader>
{isSelectMode ? (
<VirtualTableSelectAllHeaderCell
allSelected={selectState.allSelected}
allUnselected={selectState.allUnselected}
onSelectAllChange={onSelectAllChange}
/>
) : null}
{columns.map((column) => {
const { name, label, isSortable, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'actions') {
return (
<VirtualTableHeaderCell
key={name}
className={styles[name]}
name={name}
isSortable={false}
>
<TableOptionsModalWrapper
columns={columns}
optionsComponent={IndexerIndexTableOptions}
onTableOptionChange={onTableOptionChange}
>
<IconButton name={icons.ADVANCED_SETTINGS} />
</TableOptionsModalWrapper>
</VirtualTableHeaderCell>
);
}
return (
<VirtualTableHeaderCell
key={name}
className={classNames(styles[name])}
name={name}
sortKey={sortKey}
sortDirection={sortDirection}
isSortable={isSortable}
onSortPress={onSortPress}
>
{label}
</VirtualTableHeaderCell>
);
})}
</VirtualTableHeader>
);
}
export default IndexerIndexTableHeader;
@@ -1,77 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class IndexerIndexTableOptions extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
showSearchAction: props.showSearchAction
};
}
componentDidUpdate(prevProps) {
const { showSearchAction } = this.props;
if (showSearchAction !== prevProps.showSearchAction) {
this.setState({
showSearchAction
});
}
}
//
// Listeners
onTableOptionChange = ({ name, value }) => {
this.setState({
[name]: value
}, () => {
this.props.onTableOptionChange({
tableOptions: {
...this.state,
[name]: value
}
});
});
};
//
// Render
render() {
const {
showSearchAction
} = this.state;
return (
<FormGroup>
<FormLabel>{translate('ShowSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showSearchAction"
value={showSearchAction}
helpText={translate('ShowSearchHelpText')}
onChange={this.onTableOptionChange}
/>
</FormGroup>
);
}
}
IndexerIndexTableOptions.propTypes = {
showSearchAction: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired
};
export default IndexerIndexTableOptions;
@@ -0,0 +1,49 @@
import React, { Fragment, useCallback } from 'react';
import { useSelector } from 'react-redux';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import selectTableOptions from './selectTableOptions';
interface IndexerIndexTableOptionsProps {
onTableOptionChange(...args: unknown[]): unknown;
}
function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) {
const { onTableOptionChange } = props;
const tableOptions = useSelector(selectTableOptions);
const { showSearchAction } = tableOptions;
const onTableOptionChangeWrapper = useCallback(
({ name, value }) => {
onTableOptionChange({
tableOptions: {
...tableOptions,
[name]: value,
},
});
},
[tableOptions, onTableOptionChange]
);
return (
<Fragment>
<FormGroup>
<FormLabel>Show Search</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showSearchAction"
value={showSearchAction}
helpText="Show search button on hover"
onChange={onTableOptionChangeWrapper}
/>
</FormGroup>
</Fragment>
);
}
export default IndexerIndexTableOptions;
@@ -1,14 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import IndexerIndexTableOptions from './IndexerIndexTableOptions';
function createMapStateToProps() {
return createSelector(
(state) => state.indexerIndex.tableOptions,
(tableOptions) => {
return tableOptions;
}
);
}
export default connect(createMapStateToProps)(IndexerIndexTableOptions);
@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'status': string;
'statusIcon': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,66 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons, kinds } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime';
import styles from './IndexerStatusCell.css';
function IndexerStatusCell(props) {
const {
className,
enabled,
redirect,
status,
longDateFormat,
timeFormat,
component: Component,
...otherProps
} = props;
const enableKind = redirect ? kinds.INFO : kinds.SUCCESS;
const enableIcon = redirect ? icons.REDIRECT : icons.CHECK;
const enableTitle = redirect ? 'Indexer is Enabled, Redirect is Enabled' : 'Indexer is Enabled';
return (
<Component
className={className}
{...otherProps}
>
{
<Icon
className={styles.statusIcon}
kind={enabled ? enableKind : kinds.DEFAULT}
name={enabled ? enableIcon: icons.BLOCKLIST}
title={enabled ? enableTitle : 'Indexer is Disabled'}
/>
}
{
status &&
<Icon
className={styles.statusIcon}
kind={kinds.DANGER}
name={icons.WARNING}
title={`Indexer is Disabled due to failures until ${formatDateTime(status.disabledTill, longDateFormat, timeFormat)}`}
/>
}
</Component>
);
}
IndexerStatusCell.propTypes = {
className: PropTypes.string.isRequired,
enabled: PropTypes.bool.isRequired,
redirect: PropTypes.bool.isRequired,
status: PropTypes.object,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
component: PropTypes.elementType
};
IndexerStatusCell.defaultProps = {
className: styles.status,
component: VirtualTableRowCell
};
export default IndexerStatusCell;
@@ -0,0 +1,57 @@
import React from 'react';
import Icon from 'Components/Icon';
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons, kinds } from 'Helpers/Props';
import { IndexerStatus } from 'Indexer/Indexer';
import formatDateTime from 'Utilities/Date/formatDateTime';
import styles from './IndexerStatusCell.css';
interface IndexerStatusCellProps {
className: string;
enabled: boolean;
redirect: boolean;
status: IndexerStatus;
component?: React.ElementType;
}
function IndexerStatusCell(props: IndexerStatusCellProps) {
const {
className,
enabled,
redirect,
status,
component: Component = VirtualTableRowCell,
...otherProps
} = props;
const enableKind = redirect ? kinds.INFO : kinds.SUCCESS;
const enableIcon = redirect ? icons.REDIRECT : icons.CHECK;
const enableTitle = redirect
? 'Indexer is Enabled, Redirect is Enabled'
: 'Indexer is Enabled';
return (
<Component className={className} {...otherProps}>
{
<Icon
className={styles.statusIcon}
kind={enabled ? enableKind : kinds.DEFAULT}
name={enabled ? enableIcon : icons.BLOCKLIST}
title={enabled ? enableTitle : 'Indexer is Disabled'}
/>
}
{status ? (
<Icon
className={styles.statusIcon}
kind={kinds.DANGER}
name={icons.WARNING}
title={`Indexer is Disabled due to failures until ${formatDateTime(
status.disabledTill
)}`}
/>
) : null}
</Component>
);
}
export default IndexerStatusCell;
@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'torrent': string;
'usenet': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,20 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import styles from './ProtocolLabel.css';
function ProtocolLabel({ protocol }) {
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
return (
<Label className={styles[protocol]}>
{protocolName}
</Label>
);
}
ProtocolLabel.propTypes = {
protocol: PropTypes.string.isRequired
};
export default ProtocolLabel;
@@ -0,0 +1,17 @@
import React from 'react';
import Label from 'Components/Label';
import styles from './ProtocolLabel.css';
interface ProtocolLabelProps {
protocol: string;
}
function ProtocolLabel(props: ProtocolLabelProps) {
const { protocol } = props;
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
return <Label className={styles[protocol]}>{protocolName}</Label>;
}
export default ProtocolLabel;
@@ -0,0 +1,8 @@
import { createSelector } from 'reselect';
const selectTableOptions = createSelector(
(state) => state.indexerIndex.tableOptions,
(tableOptions) => tableOptions
);
export default selectTableOptions;