mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-05 13:21:25 -05:00
Compare commits
214 Commits
v5.8.2.891
...
v5.11.0.92
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b43c2e345 | ||
|
|
dc599b6531 | ||
|
|
1421179654 | ||
|
|
41dcf32e24 | ||
|
|
7a813a44b6 | ||
|
|
54a5059080 | ||
|
|
adaf7444d3 | ||
|
|
49d11e59b3 | ||
|
|
a7eb4a4a04 | ||
|
|
66a6a663ba | ||
|
|
f735e31835 | ||
|
|
b8f1286abb | ||
|
|
9df45199d0 | ||
|
|
a692c35b03 | ||
|
|
ddcad270c3 | ||
|
|
b06f1d7c12 | ||
|
|
480bb50b85 | ||
|
|
dbc94dbe4e | ||
|
|
b89271fc01 | ||
|
|
66fcde7325 | ||
|
|
463741da1f | ||
|
|
3388fae1a5 | ||
|
|
72b2cfe8be | ||
|
|
d5dd5e08ca | ||
|
|
fabd40cbae | ||
|
|
3ca327f611 | ||
|
|
c804140896 | ||
|
|
bb43d0c796 | ||
|
|
5757fa797f | ||
|
|
2fc32189d8 | ||
|
|
5975be3690 | ||
|
|
6095819005 | ||
|
|
7528882adf | ||
|
|
c1f1307345 | ||
|
|
348060351a | ||
|
|
ca31cdd33a | ||
|
|
36e278aa82 | ||
|
|
927e84654f | ||
|
|
96e60906c5 | ||
|
|
7a55b563c0 | ||
|
|
b4bbb71a9b | ||
|
|
0361299a73 | ||
|
|
e11339fb83 | ||
|
|
fbdd3129f5 | ||
|
|
2843647e23 | ||
|
|
b6b7d30fc1 | ||
|
|
e688dfadf7 | ||
|
|
f3ce0ac620 | ||
|
|
08c250dfe3 | ||
|
|
a5c1025efd | ||
|
|
f0019d622a | ||
|
|
032e8aa920 | ||
|
|
51cd7c70ba | ||
|
|
12c814ed78 | ||
|
|
d7bdc2c46c | ||
|
|
6b4c0bd24c | ||
|
|
c332c38890 | ||
|
|
d910611630 | ||
|
|
c6feb27962 | ||
|
|
0f7721ef11 | ||
|
|
89731bdc41 | ||
|
|
126b6eba00 | ||
|
|
56fece293c | ||
|
|
4503c3d36e | ||
|
|
0d6ce5ea49 | ||
|
|
b4b5ad9567 | ||
|
|
3ae6347532 | ||
|
|
a826ffdbc9 | ||
|
|
dc3bf9acb0 | ||
|
|
eca95826c2 | ||
|
|
593b943cb0 | ||
|
|
a3faa9ed5f | ||
|
|
229e87f398 | ||
|
|
fcb758bf67 | ||
|
|
0a9ae45ed1 | ||
|
|
b062a46cbd | ||
|
|
ac4669dfc1 | ||
|
|
36d80387c6 | ||
|
|
9fe4793606 | ||
|
|
fa80608394 | ||
|
|
6e81d5917e | ||
|
|
8d189523c4 | ||
|
|
4d589422e6 | ||
|
|
675612e7c6 | ||
|
|
be3916f67d | ||
|
|
453f216e0d | ||
|
|
2d4846e5be | ||
|
|
2700a6cf8a | ||
|
|
674e414111 | ||
|
|
21bd21b70c | ||
|
|
fde87a38f9 | ||
|
|
0d6ba200d3 | ||
|
|
93298645e3 | ||
|
|
58f544e9e0 | ||
|
|
cf952d5c0b | ||
|
|
f6d630bdd3 | ||
|
|
657ced4772 | ||
|
|
d3a0c83f98 | ||
|
|
5833d5d4c4 | ||
|
|
2ba4562f49 | ||
|
|
d79db69644 | ||
|
|
7532dfb03c | ||
|
|
a47528aa81 | ||
|
|
a812d9f39f | ||
|
|
fc97f05850 | ||
|
|
644876123d | ||
|
|
540659a799 | ||
|
|
288668f7e6 | ||
|
|
fcf3be42d5 | ||
|
|
16e218501e | ||
|
|
caf2d33c11 | ||
|
|
bc918ed3b5 | ||
|
|
df77474314 | ||
|
|
bf84471509 | ||
|
|
d346d969de | ||
|
|
14b125ccd9 | ||
|
|
da5323a08f | ||
|
|
672b351497 | ||
|
|
fc4f4ab211 | ||
|
|
333e8281ea | ||
|
|
c278ffd8a0 | ||
|
|
5898eea3d0 | ||
|
|
5b78a1297a | ||
|
|
14e3e1fa35 | ||
|
|
c0e76544ef | ||
|
|
8c16677875 | ||
|
|
401e19547c | ||
|
|
c9f28fdc4f | ||
|
|
0ad4d7ea9a | ||
|
|
e8bb3df68e | ||
|
|
9442f1fb04 | ||
|
|
9ad6b3a611 | ||
|
|
fa1d6ad109 | ||
|
|
ccbc8f591b | ||
|
|
a4301f8db0 | ||
|
|
fe00825f2b | ||
|
|
17a9b0f7b0 | ||
|
|
62bdb66d0f | ||
|
|
7c1fedb8ce | ||
|
|
333351da45 | ||
|
|
fbbe7f7b5d | ||
|
|
edec201a6c | ||
|
|
1e783bfe07 | ||
|
|
7d5236de21 | ||
|
|
1efe7db5f3 | ||
|
|
b37cc42805 | ||
|
|
fa19f45171 | ||
|
|
4ae382cea7 | ||
|
|
37c09ba1f8 | ||
|
|
322df78f5a | ||
|
|
3a4446cc8e | ||
|
|
6c456e57d8 | ||
|
|
abc7efabea | ||
|
|
ace692aca6 | ||
|
|
882bde713f | ||
|
|
2575e3647f | ||
|
|
5cac5b6068 | ||
|
|
4628868dfa | ||
|
|
25685314bc | ||
|
|
41b1ea553e | ||
|
|
5d17f8e84d | ||
|
|
7490fc7040 | ||
|
|
f4e1f51a9c | ||
|
|
8e1016572b | ||
|
|
caabb032f3 | ||
|
|
ce9c5d4d97 | ||
|
|
967bed3161 | ||
|
|
8d9f1697ee | ||
|
|
3be2c6b0be | ||
|
|
b6d9c73a17 | ||
|
|
b1a7652753 | ||
|
|
f76c97c3ce | ||
|
|
1f5a84d202 | ||
|
|
d25bcdb043 | ||
|
|
f75497f57d | ||
|
|
2f413c68d9 | ||
|
|
68c20713e5 | ||
|
|
6eeed96d12 | ||
|
|
6f306a22e5 | ||
|
|
29ef75960d | ||
|
|
364a42424a | ||
|
|
a5b315ba83 | ||
|
|
e80e96de0e | ||
|
|
44c7c71226 | ||
|
|
04c5e6c2a6 | ||
|
|
5533528b56 | ||
|
|
74246df881 | ||
|
|
88127298ae | ||
|
|
5559fa5fa5 | ||
|
|
d503e01747 | ||
|
|
ae89ae175f | ||
|
|
df35e78e1f | ||
|
|
a3b3fee06b | ||
|
|
ae377d97a5 | ||
|
|
270df9d1dd | ||
|
|
6ed3045433 | ||
|
|
ddb7d5690b | ||
|
|
a1104b8263 | ||
|
|
358ff0c130 | ||
|
|
ff0a04c331 | ||
|
|
c12f01f919 | ||
|
|
93d661242a | ||
|
|
324dac8db3 | ||
|
|
bba69d8b22 | ||
|
|
1366f6e8b4 | ||
|
|
f79712951b | ||
|
|
101b046753 | ||
|
|
cd713e7252 | ||
|
|
a54f54eb6e | ||
|
|
f2af7a1b72 | ||
|
|
a5b48153a6 | ||
|
|
1804e486d6 | ||
|
|
b490177a77 | ||
|
|
7a90b4a6b2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -118,6 +118,7 @@ src/UI/.idea/*
|
||||
node_modules/
|
||||
_output*
|
||||
_artifacts
|
||||
_temp*
|
||||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Radarr
|
||||
|
||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
||||
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/radarr/installation/docker)
|
||||

|
||||
[](#backers)
|
||||
|
||||
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.8.2'
|
||||
majorVersion: '5.11.0'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.421'
|
||||
dotnetVersion: '6.0.424'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.2'
|
||||
windowsImage: 'windows-2022'
|
||||
|
||||
@@ -359,11 +359,16 @@ module.exports = {
|
||||
],
|
||||
|
||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
||||
'no-shadow': 'off',
|
||||
// These should be enabled after cleaning things up
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'no-shadow': 'off',
|
||||
'prettier/prettier': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
@@ -376,7 +381,41 @@ module.exports = {
|
||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
// React Hooks
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
|
||||
// React
|
||||
'react/function-component-definition': 'error',
|
||||
'react/hook-use-state': 'error',
|
||||
'react/jsx-boolean-value': ['error', 'always'],
|
||||
'react/jsx-curly-brace-presence': [
|
||||
'error',
|
||||
{ props: 'never', children: 'never' }
|
||||
],
|
||||
'react/jsx-fragments': 'error',
|
||||
'react/jsx-handler-names': [
|
||||
'error',
|
||||
{
|
||||
eventHandlerPrefix: 'on',
|
||||
eventHandlerPropPrefix: 'on'
|
||||
}
|
||||
],
|
||||
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||
'react/jsx-sort-props': [
|
||||
'error',
|
||||
{
|
||||
callbacksLast: true,
|
||||
noSortAlphabetically: true,
|
||||
reservedFirst: true
|
||||
}
|
||||
],
|
||||
'react/prop-types': 'off',
|
||||
'react/self-closing-comp': 'error'
|
||||
})
|
||||
},
|
||||
{
|
||||
|
||||
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
@@ -9,7 +9,7 @@
|
||||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
|
||||
@@ -66,7 +66,7 @@ module.exports = (env) => {
|
||||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: '[name]-[contenthash].js',
|
||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
@@ -91,7 +91,7 @@ module.exports = (env) => {
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -201,7 +201,7 @@ module.exports = (env) => {
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ const mixinsFiles = [
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'autoprefixer',
|
||||
['postcss-mixins', {
|
||||
mixinsFiles
|
||||
}],
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
||||
|
||||
class Blocklist extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmRemoveModalOpen: false,
|
||||
isConfirmClearModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
if (hasDifferentItems(prevProps.items, items)) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
};
|
||||
|
||||
onRemoveSelectedConfirmed = () => {
|
||||
this.props.onRemoveSelected(this.getSelectedIds());
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
};
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.setState({ isConfirmClearModalOpen: true });
|
||||
};
|
||||
|
||||
onClearBlocklistConfirmed = () => {
|
||||
this.props.onClearBlocklistPress();
|
||||
this.setState({ isConfirmClearModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmClearModalClose = () => {
|
||||
this.setState({ isConfirmClearModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlocklistExecuting,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isConfirmClearModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Blocklist')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={!selectedIds.length}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('Clear')}
|
||||
iconName={icons.CLEAR}
|
||||
isDisabled={!items.length}
|
||||
isSpinning={isClearingBlocklistExecuting}
|
||||
onPress={this.onClearBlocklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={BlocklistFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('BlocklistLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{
|
||||
selectedFilterKey === 'all' ?
|
||||
translate('NoHistoryBlocklist') :
|
||||
translate('BlocklistFilterHasNoItems')
|
||||
}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BlocklistRowConnector
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('RemoveSelected')}
|
||||
message={translate('RemoveSelectedBlocklistMessageText')}
|
||||
confirmLabel={translate('RemoveSelected')}
|
||||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmClearModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ClearBlocklist')}
|
||||
message={translate('ClearBlocklistMessageText')}
|
||||
confirmLabel={translate('Clear')}
|
||||
onConfirm={this.onClearBlocklistConfirmed}
|
||||
onCancel={this.onConfirmClearModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Blocklist.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: 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,
|
||||
totalRecords: PropTypes.number,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
onRemoveSelected: PropTypes.func.isRequired,
|
||||
onClearBlocklistPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Blocklist;
|
||||
329
frontend/src/Activity/Blocklist/Blocklist.tsx
Normal file
329
frontend/src/Activity/Blocklist/Blocklist.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SelectProvider } from 'App/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import usePaging from 'Components/Table/usePaging';
|
||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
clearBlocklist,
|
||||
fetchBlocklist,
|
||||
gotoBlocklistPage,
|
||||
removeBlocklistItems,
|
||||
setBlocklistFilter,
|
||||
setBlocklistSort,
|
||||
setBlocklistTableOption,
|
||||
} from 'Store/Actions/blocklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import {
|
||||
registerPagePopulator,
|
||||
unregisterPagePopulator,
|
||||
} from 'Utilities/pagePopulator';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||
import BlocklistRow from './BlocklistRow';
|
||||
|
||||
function Blocklist() {
|
||||
const requestCurrentPage = useCurrentPage();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
} = useSelector((state: AppState) => state.blocklist);
|
||||
|
||||
const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
|
||||
const isClearingBlocklistExecuting = useSelector(
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST)
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
||||
useState(false);
|
||||
const [isConfirmClearModalOpen, setIsConfirmClearModalOpen] = useState(false);
|
||||
|
||||
const [selectState, setSelectState] = useSelectState();
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
const wasClearingBlocklistExecuting = usePrevious(
|
||||
isClearingBlocklistExecuting
|
||||
);
|
||||
|
||||
const handleSelectAllChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||
setSelectState({
|
||||
type: 'toggleSelected',
|
||||
items,
|
||||
id,
|
||||
isSelected: value,
|
||||
shiftKey,
|
||||
});
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleRemoveSelectedPress = useCallback(() => {
|
||||
setIsConfirmRemoveModalOpen(true);
|
||||
}, [setIsConfirmRemoveModalOpen]);
|
||||
|
||||
const handleRemoveSelectedConfirmed = useCallback(() => {
|
||||
dispatch(removeBlocklistItems({ ids: selectedIds }));
|
||||
setIsConfirmRemoveModalOpen(false);
|
||||
}, [selectedIds, setIsConfirmRemoveModalOpen, dispatch]);
|
||||
|
||||
const handleConfirmRemoveModalClose = useCallback(() => {
|
||||
setIsConfirmRemoveModalOpen(false);
|
||||
}, [setIsConfirmRemoveModalOpen]);
|
||||
|
||||
const handleClearBlocklistPress = useCallback(() => {
|
||||
setIsConfirmClearModalOpen(true);
|
||||
}, [setIsConfirmClearModalOpen]);
|
||||
|
||||
const handleClearBlocklistConfirmed = useCallback(() => {
|
||||
dispatch(executeCommand({ name: commandNames.CLEAR_BLOCKLIST }));
|
||||
setIsConfirmClearModalOpen(false);
|
||||
}, [setIsConfirmClearModalOpen, dispatch]);
|
||||
|
||||
const handleConfirmClearModalClose = useCallback(() => {
|
||||
setIsConfirmClearModalOpen(false);
|
||||
}, [setIsConfirmClearModalOpen]);
|
||||
|
||||
const {
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
} = usePaging({
|
||||
page,
|
||||
totalPages,
|
||||
gotoPage: gotoBlocklistPage,
|
||||
});
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string) => {
|
||||
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string) => {
|
||||
dispatch(setBlocklistSort({ sortKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleTableOptionChange = useCallback(
|
||||
(payload: TableOptionsChangePayload) => {
|
||||
dispatch(setBlocklistTableOption(payload));
|
||||
|
||||
if (payload.pageSize) {
|
||||
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestCurrentPage) {
|
||||
dispatch(fetchBlocklist());
|
||||
} else {
|
||||
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||
}
|
||||
|
||||
return () => {
|
||||
dispatch(clearBlocklist());
|
||||
};
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
dispatch(fetchBlocklist());
|
||||
};
|
||||
|
||||
registerPagePopulator(repopulate);
|
||||
|
||||
return () => {
|
||||
unregisterPagePopulator(repopulate);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasClearingBlocklistExecuting && !isClearingBlocklistExecuting) {
|
||||
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||
}
|
||||
}, [isClearingBlocklistExecuting, wasClearingBlocklistExecuting, dispatch]);
|
||||
|
||||
return (
|
||||
<SelectProvider items={items}>
|
||||
<PageContent title={translate('Blocklist')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={!selectedIds.length}
|
||||
isSpinning={isRemoving}
|
||||
onPress={handleRemoveSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('Clear')}
|
||||
iconName={icons.CLEAR}
|
||||
isDisabled={!items.length}
|
||||
isSpinning={isClearingBlocklistExecuting}
|
||||
onPress={handleClearBlocklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={BlocklistFilterModal}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('BlocklistLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>
|
||||
{selectedFilterKey === 'all'
|
||||
? translate('NoBlocklistItems')
|
||||
: translate('BlocklistFilterHasNoItems')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !!items.length ? (
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
onSelectAllChange={handleSelectAllChange}
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<BlocklistRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={handleSelectedChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePager
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={handleFirstPagePress}
|
||||
onPreviousPagePress={handlePreviousPagePress}
|
||||
onNextPagePress={handleNextPagePress}
|
||||
onLastPagePress={handleLastPagePress}
|
||||
onPageSelect={handlePageSelect}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('RemoveSelected')}
|
||||
message={translate('RemoveSelectedBlocklistMessageText')}
|
||||
confirmLabel={translate('RemoveSelected')}
|
||||
onConfirm={handleRemoveSelectedConfirmed}
|
||||
onCancel={handleConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmClearModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ClearBlocklist')}
|
||||
message={translate('ClearBlocklistMessageText')}
|
||||
confirmLabel={translate('Clear')}
|
||||
onConfirm={handleClearBlocklistConfirmed}
|
||||
onCancel={handleConfirmClearModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Blocklist;
|
||||
@@ -1,161 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Blocklist from './Blocklist';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.blocklist,
|
||||
createCustomFiltersSelector('blocklist'),
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||
(blocklist, customFilters, isClearingBlocklistExecuting) => {
|
||||
return {
|
||||
isClearingBlocklistExecuting,
|
||||
customFilters,
|
||||
...blocklist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...blocklistActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class BlocklistConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchBlocklist,
|
||||
gotoBlocklistFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchBlocklist();
|
||||
} else {
|
||||
gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearBlocklist();
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchBlocklist();
|
||||
};
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoBlocklistPreviousPage();
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoBlocklistNextPage();
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoBlocklistLastPage();
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoBlocklistPage({ page });
|
||||
};
|
||||
|
||||
onRemoveSelected = (ids) => {
|
||||
this.props.removeBlocklistItems({ ids });
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlocklistSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setBlocklistFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlocklistTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Blocklist
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onRemoveSelected={this.onRemoveSelected}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlocklistConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchBlocklist: PropTypes.func.isRequired,
|
||||
gotoBlocklistFirstPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistPage: PropTypes.func.isRequired,
|
||||
removeBlocklistItems: PropTypes.func.isRequired,
|
||||
setBlocklistSort: PropTypes.func.isRequired,
|
||||
setBlocklistFilter: PropTypes.func.isRequired,
|
||||
setBlocklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlocklist: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
|
||||
);
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
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';
|
||||
|
||||
class BlocklistDetailsModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Details
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Protocol')}
|
||||
data={protocol}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlocklistDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlocklistDetailsModal;
|
||||
64
frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx
Normal file
64
frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
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 DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface BlocklistDetailsModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
protocol: DownloadProtocol;
|
||||
indexer?: string;
|
||||
message?: string;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
|
||||
const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Details</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Protocol')}
|
||||
data={protocol}
|
||||
/>
|
||||
|
||||
{message ? (
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{message ? (
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
) : null}
|
||||
</DescriptionList>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlocklistDetailsModal;
|
||||
@@ -1,4 +1,4 @@
|
||||
.language,
|
||||
.languages,
|
||||
.quality {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'indexer': string;
|
||||
'language': string;
|
||||
'languages': string;
|
||||
'quality': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||
import styles from './BlocklistRow.css';
|
||||
|
||||
class BlocklistRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
movie,
|
||||
sourceTitle,
|
||||
quality,
|
||||
customFormats,
|
||||
languages,
|
||||
date,
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onRemovePress
|
||||
} = this.props;
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink
|
||||
titleSlug={movie.titleSlug}
|
||||
title={movie.title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.quality}
|
||||
>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={date}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.indexer}
|
||||
>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.actions}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title={translate('RemoveFromBlocklist')}
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onRemovePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<BlocklistDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
sourceTitle={sourceTitle}
|
||||
protocol={protocol}
|
||||
indexer={indexer}
|
||||
message={message}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BlocklistRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
movie: PropTypes.object.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlocklistRow;
|
||||
160
frontend/src/Activity/Blocklist/BlocklistRow.tsx
Normal file
160
frontend/src/Activity/Blocklist/BlocklistRow.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||
import Blocklist from 'typings/Blocklist';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||
import styles from './BlocklistRow.css';
|
||||
|
||||
interface BlocklistRowProps extends Blocklist {
|
||||
isSelected: boolean;
|
||||
columns: Column[];
|
||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||
}
|
||||
|
||||
function BlocklistRow(props: BlocklistRowProps) {
|
||||
const {
|
||||
id,
|
||||
movieId,
|
||||
sourceTitle,
|
||||
languages,
|
||||
quality,
|
||||
customFormats,
|
||||
date,
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
} = props;
|
||||
|
||||
const movie = useMovie(movieId);
|
||||
const dispatch = useDispatch();
|
||||
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||
|
||||
const handleDetailsPress = useCallback(() => {
|
||||
setIsDetailsModalOpen(true);
|
||||
}, [setIsDetailsModalOpen]);
|
||||
|
||||
const handleDetailsModalClose = useCallback(() => {
|
||||
setIsDetailsModalOpen(false);
|
||||
}, [setIsDetailsModalOpen]);
|
||||
|
||||
const handleRemovePress = useCallback(() => {
|
||||
dispatch(removeBlocklistItem({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{columns.map((column) => {
|
||||
const { name, isVisible } = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.languages}>
|
||||
<MovieLanguages languages={languages} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.quality}>
|
||||
<MovieQuality quality={quality} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'date') {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2739)
|
||||
return <RelativeDateCell key={name} date={date} />;
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.indexer}>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.actions}>
|
||||
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||
|
||||
<IconButton
|
||||
title={translate('RemoveFromBlocklist')}
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
onPress={handleRemovePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
<BlocklistDetailsModal
|
||||
isOpen={isDetailsModalOpen}
|
||||
sourceTitle={sourceTitle}
|
||||
protocol={protocol}
|
||||
indexer={indexer}
|
||||
message={message}
|
||||
onModalClose={handleDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlocklistRow;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import BlocklistRow from './BlocklistRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
(movie) => {
|
||||
return {
|
||||
movie
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemovePress() {
|
||||
dispatch(removeBlocklistItem({ id: props.id }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);
|
||||
@@ -1,13 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
@@ -113,7 +113,7 @@ class HistoryRow extends Component {
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
<MovieLanguages
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
@@ -143,7 +143,7 @@ class HistoryRow extends Component {
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={date}
|
||||
/>
|
||||
|
||||
@@ -11,3 +11,7 @@
|
||||
border-color: var(--usenetColor);
|
||||
background-color: var(--usenetColor);
|
||||
}
|
||||
|
||||
.unknown {
|
||||
composes: label from '~Components/Label.css';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'torrent': string;
|
||||
'unknown': string;
|
||||
'usenet': string;
|
||||
}
|
||||
export const cssExports: 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;
|
||||
16
frontend/src/Activity/Queue/ProtocolLabel.tsx
Normal file
16
frontend/src/Activity/Queue/ProtocolLabel.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import styles from './ProtocolLabel.css';
|
||||
|
||||
interface ProtocolLabelProps {
|
||||
protocol: DownloadProtocol;
|
||||
}
|
||||
|
||||
function ProtocolLabel({ protocol }: ProtocolLabelProps) {
|
||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||
|
||||
return <Label className={styles[protocol]}>{protocolName}</Label>;
|
||||
}
|
||||
|
||||
export default ProtocolLabel;
|
||||
@@ -1,374 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._shouldBlockRefresh = false;
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isPendingSelected: false,
|
||||
isConfirmRemoveModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
if (this._shouldBlockRefresh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
isFetching,
|
||||
isMoviesFetching
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(!isMoviesFetching && prevProps.isMoviesFetching) ||
|
||||
(!isFetching && prevProps.isFetching) ||
|
||||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.movieId))
|
||||
) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const nextState = {};
|
||||
|
||||
if (prevProps.items !== items) {
|
||||
nextState.items = items;
|
||||
}
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||
});
|
||||
|
||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||
nextState.isPendingSelected = isPendingSelected;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(nextState)) {
|
||||
this.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQueueRowModalOpenOrClose = (isOpen) => {
|
||||
this._shouldBlockRefresh = isOpen;
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onGrabSelectedPress = () => {
|
||||
this.props.onGrabSelectedPress(this.getSelectedIds());
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true }, () => {
|
||||
this._shouldBlockRefresh = true;
|
||||
});
|
||||
};
|
||||
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this._shouldBlockRefresh = false;
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this._shouldBlockRefresh = false;
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
isMoviesFetching,
|
||||
isMoviesPopulated,
|
||||
moviesError,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
count,
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
onRefreshPress,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isPendingSelected,
|
||||
items
|
||||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length || items.every((e) => !e.movieId));
|
||||
const hasError = error || moviesError;
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const selectedCount = selectedIds.length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Queue')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('Refresh')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isRefreshing}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('GrabSelected')}
|
||||
iconName={icons.DOWNLOAD}
|
||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={this.onGrabSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={disableSelectedActions}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
maxPageSize={200}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={QueueFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isRefreshing && !isAllPopulated ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isRefreshing && hasError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('QueueLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !items.length ?
|
||||
<Alert kind={kinds.INFO}>
|
||||
{
|
||||
selectedFilterKey !== 'all' && count > 0 ?
|
||||
translate('QueueFilterHasNoItems') :
|
||||
translate('QueueIsEmpty')
|
||||
}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !!items.length ?
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<QueueRowConnector
|
||||
key={item.id}
|
||||
movieId={item.movieId}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isRefreshing}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
)}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.movieId);
|
||||
})
|
||||
)}
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Queue.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isMoviesFetching: PropTypes.bool.isRequired,
|
||||
isMoviesPopulated: PropTypes.bool.isRequired,
|
||||
moviesError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: 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,
|
||||
count: PropTypes.number.isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Queue.defaultProps = {
|
||||
count: 0
|
||||
};
|
||||
|
||||
export default Queue;
|
||||
400
frontend/src/Activity/Queue/Queue.tsx
Normal file
400
frontend/src/Activity/Queue/Queue.tsx
Normal file
@@ -0,0 +1,400 @@
|
||||
import React, {
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import usePaging from 'Components/Table/usePaging';
|
||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import {
|
||||
clearQueue,
|
||||
fetchQueue,
|
||||
gotoQueuePage,
|
||||
grabQueueItems,
|
||||
removeQueueItems,
|
||||
setQueueFilter,
|
||||
setQueueSort,
|
||||
setQueueTableOption,
|
||||
} from 'Store/Actions/queueActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import {
|
||||
registerPagePopulator,
|
||||
unregisterPagePopulator,
|
||||
} from 'Utilities/pagePopulator';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptions from './QueueOptions';
|
||||
import QueueRow from './QueueRow';
|
||||
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
||||
import createQueueStatusSelector from './Status/createQueueStatusSelector';
|
||||
|
||||
function Queue() {
|
||||
const requestCurrentPage = useCurrentPage();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
} = useSelector((state: AppState) => state.queue.paged);
|
||||
|
||||
const { count } = useSelector(createQueueStatusSelector());
|
||||
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
|
||||
createMoviesFetchingSelector()
|
||||
);
|
||||
const customFilters = useSelector(createCustomFiltersSelector('queue'));
|
||||
|
||||
const isRefreshMonitoredDownloadsExecuting = useSelector(
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS)
|
||||
);
|
||||
|
||||
const shouldBlockRefresh = useRef(false);
|
||||
const currentQueue = useRef<ReactElement | null>(null);
|
||||
|
||||
const [selectState, setSelectState] = useSelectState();
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
const isPendingSelected = useMemo(() => {
|
||||
return items.some((item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||
});
|
||||
}, [items, selectedIds]);
|
||||
|
||||
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const isRefreshing =
|
||||
isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
const isAllPopulated =
|
||||
isPopulated &&
|
||||
(isMoviesPopulated || !items.length || items.every((m) => !m.movieId));
|
||||
const hasError = error || moviesError;
|
||||
const selectedCount = selectedIds.length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
const handleSelectAllChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||
setSelectState({
|
||||
type: 'toggleSelected',
|
||||
items,
|
||||
id,
|
||||
isSelected: value,
|
||||
shiftKey,
|
||||
});
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleRefreshPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS,
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleQueueRowModalOpenOrClose = useCallback((isOpen: boolean) => {
|
||||
shouldBlockRefresh.current = isOpen;
|
||||
}, []);
|
||||
|
||||
const handleGrabSelectedPress = useCallback(() => {
|
||||
dispatch(grabQueueItems({ ids: selectedIds }));
|
||||
}, [selectedIds, dispatch]);
|
||||
|
||||
const handleRemoveSelectedPress = useCallback(() => {
|
||||
shouldBlockRefresh.current = true;
|
||||
setIsConfirmRemoveModalOpen(true);
|
||||
}, [setIsConfirmRemoveModalOpen]);
|
||||
|
||||
const handleRemoveSelectedConfirmed = useCallback(
|
||||
(payload: RemovePressProps) => {
|
||||
shouldBlockRefresh.current = false;
|
||||
dispatch(removeQueueItems({ ids: selectedIds, ...payload }));
|
||||
setIsConfirmRemoveModalOpen(false);
|
||||
},
|
||||
[selectedIds, setIsConfirmRemoveModalOpen, dispatch]
|
||||
);
|
||||
|
||||
const handleConfirmRemoveModalClose = useCallback(() => {
|
||||
shouldBlockRefresh.current = false;
|
||||
setIsConfirmRemoveModalOpen(false);
|
||||
}, [setIsConfirmRemoveModalOpen]);
|
||||
|
||||
const {
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
} = usePaging({
|
||||
page,
|
||||
totalPages,
|
||||
gotoPage: gotoQueuePage,
|
||||
});
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string) => {
|
||||
dispatch(setQueueFilter({ selectedFilterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string) => {
|
||||
dispatch(setQueueSort({ sortKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleTableOptionChange = useCallback(
|
||||
(payload: TableOptionsChangePayload) => {
|
||||
dispatch(setQueueTableOption(payload));
|
||||
|
||||
if (payload.pageSize) {
|
||||
dispatch(gotoQueuePage({ page: 1 }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestCurrentPage) {
|
||||
dispatch(fetchQueue());
|
||||
} else {
|
||||
dispatch(gotoQueuePage({ page: 1 }));
|
||||
}
|
||||
|
||||
return () => {
|
||||
dispatch(clearQueue());
|
||||
};
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
dispatch(fetchQueue());
|
||||
};
|
||||
|
||||
registerPagePopulator(repopulate);
|
||||
|
||||
return () => {
|
||||
unregisterPagePopulator(repopulate);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
if (!shouldBlockRefresh.current) {
|
||||
currentQueue.current = (
|
||||
<PageContentBody>
|
||||
{isRefreshing && !isAllPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isRefreshing && hasError ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('QueueLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isAllPopulated && !hasError && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>
|
||||
{selectedFilterKey !== 'all' && count > 0
|
||||
? translate('QueueFilterHasNoItems')
|
||||
: translate('QueueIsEmpty')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isAllPopulated && !hasError && !!items.length ? (
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
optionsComponent={QueueOptions}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
onSelectAllChange={handleSelectAllChange}
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<QueueRow
|
||||
key={item.id}
|
||||
movieId={item.movieId}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={handleSelectedChange}
|
||||
onQueueRowModalOpenOrClose={
|
||||
handleQueueRowModalOpenOrClose
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={handleFirstPagePress}
|
||||
onPreviousPagePress={handlePreviousPagePress}
|
||||
onNextPagePress={handleNextPagePress}
|
||||
onLastPagePress={handleLastPagePress}
|
||||
onPageSelect={handlePageSelect}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Queue')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Refresh"
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isRefreshing}
|
||||
onPress={handleRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('GrabSelected')}
|
||||
iconName={icons.DOWNLOAD}
|
||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={handleGrabSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={disableSelectedActions}
|
||||
isSpinning={isRemoving}
|
||||
onPress={handleRemoveSelectedPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
maxPageSize={200}
|
||||
optionsComponent={QueueOptions}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={QueueFilterModal}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
{currentQueue.current}
|
||||
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={
|
||||
isConfirmRemoveModalOpen &&
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
}
|
||||
canIgnore={
|
||||
isConfirmRemoveModalOpen &&
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.movieId);
|
||||
})
|
||||
}
|
||||
isPending={
|
||||
isConfirmRemoveModalOpen &&
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
item.status === 'delay' ||
|
||||
item.status === 'downloadClientUnavailable'
|
||||
);
|
||||
})
|
||||
}
|
||||
onRemovePress={handleRemoveSelectedConfirmed}
|
||||
onModalClose={handleConfirmRemoveModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Queue;
|
||||
@@ -1,185 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as queueActions from 'Store/Actions/queueActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Queue from './Queue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movies,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
(state) => state.queue.status.item,
|
||||
createCustomFiltersSelector('queue'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||
(movies, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
|
||||
return {
|
||||
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
customFilters,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
...options,
|
||||
...queue
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...queueActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class QueueConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchQueue,
|
||||
fetchQueueStatus,
|
||||
gotoQueueFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchQueue();
|
||||
} else {
|
||||
gotoQueueFirstPage();
|
||||
}
|
||||
|
||||
fetchQueueStatus();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
this.props.includeUnknownMovieItems !==
|
||||
prevProps.includeUnknownMovieItems
|
||||
) {
|
||||
this.repopulate();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearQueue();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchQueue();
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoQueueFirstPage();
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoQueuePreviousPage();
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoQueueNextPage();
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoQueueLastPage();
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoQueuePage({ page });
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setQueueSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setQueueFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setQueueTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||
});
|
||||
};
|
||||
|
||||
onGrabSelectedPress = (ids) => {
|
||||
this.props.grabQueueItems({ ids });
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = (payload) => {
|
||||
this.props.removeQueueItems(payload);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Queue
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||
onRemoveSelectedPress={this.onRemoveSelectedPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueConnector.propTypes = {
|
||||
includeUnknownMovieItems: PropTypes.bool.isRequired,
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchQueue: PropTypes.func.isRequired,
|
||||
fetchQueueStatus: PropTypes.func.isRequired,
|
||||
gotoQueueFirstPage: PropTypes.func.isRequired,
|
||||
gotoQueuePreviousPage: PropTypes.func.isRequired,
|
||||
gotoQueueNextPage: PropTypes.func.isRequired,
|
||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||
gotoQueuePage: PropTypes.func.isRequired,
|
||||
setQueueSort: PropTypes.func.isRequired,
|
||||
setQueueFilter: PropTypes.func.isRequired,
|
||||
setQueueTableOption: PropTypes.func.isRequired,
|
||||
clearQueue: PropTypes.func.isRequired,
|
||||
grabQueueItems: PropTypes.func.isRequired,
|
||||
removeQueueItems: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
|
||||
);
|
||||
@@ -1,36 +1,49 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import {
|
||||
QueueTrackedDownloadState,
|
||||
QueueTrackedDownloadStatus,
|
||||
StatusMessage,
|
||||
} from 'typings/Queue';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueueStatus from './QueueStatus';
|
||||
import styles from './QueueDetails.css';
|
||||
|
||||
function QueueDetails(props) {
|
||||
interface QueueDetailsProps {
|
||||
title: string;
|
||||
size: number;
|
||||
sizeleft: number;
|
||||
estimatedCompletionTime?: string;
|
||||
status: string;
|
||||
trackedDownloadState?: QueueTrackedDownloadState;
|
||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||
statusMessages?: StatusMessage[];
|
||||
errorMessage?: string;
|
||||
progressBar: React.ReactNode;
|
||||
}
|
||||
|
||||
function QueueDetails(props: QueueDetailsProps) {
|
||||
const {
|
||||
title,
|
||||
size,
|
||||
sizeleft,
|
||||
status,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState = 'downloading',
|
||||
trackedDownloadStatus = 'ok',
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
progressBar
|
||||
progressBar,
|
||||
} = props;
|
||||
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
||||
const isDownloading = status === 'downloading';
|
||||
const isPaused = status === 'paused';
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
const hasError = trackedDownloadStatus === 'error';
|
||||
|
||||
if (
|
||||
(isDownloading || isPaused) &&
|
||||
!hasWarning &&
|
||||
!hasError
|
||||
) {
|
||||
if ((isDownloading || isPaused) && !hasWarning && !hasError) {
|
||||
const state = isPaused ? translate('Paused') : translate('Downloading');
|
||||
|
||||
if (progress < 5) {
|
||||
@@ -45,11 +58,9 @@ function QueueDetails(props) {
|
||||
return (
|
||||
<Popover
|
||||
className={styles.progressBarContainer}
|
||||
anchor={progressBar}
|
||||
anchor={progressBar!}
|
||||
title={`${state} - ${progress.toFixed(1)}%`}
|
||||
body={
|
||||
<div>{title}</div>
|
||||
}
|
||||
body={<div>{title}</div>}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
);
|
||||
@@ -68,22 +79,4 @@ function QueueDetails(props) {
|
||||
);
|
||||
}
|
||||
|
||||
QueueDetails.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
progressBar: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
QueueDetails.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading'
|
||||
};
|
||||
|
||||
export default QueueDetails;
|
||||
@@ -1,78 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } 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 QueueOptions extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
includeUnknownMovieItems: props.includeUnknownMovieItems
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
includeUnknownMovieItems
|
||||
} = this.props;
|
||||
|
||||
if (includeUnknownMovieItems !== prevProps.includeUnknownMovieItems) {
|
||||
this.setState({
|
||||
includeUnknownMovieItems
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOptionChange = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onOptionChange({
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
includeUnknownMovieItems
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeUnknownMovieItems"
|
||||
value={includeUnknownMovieItems}
|
||||
helpText={translate('ShowUnknownMovieItemsHelpText')}
|
||||
onChange={this.onOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueOptions.propTypes = {
|
||||
includeUnknownMovieItems: PropTypes.bool.isRequired,
|
||||
onOptionChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QueueOptions;
|
||||
48
frontend/src/Activity/Queue/QueueOptions.tsx
Normal file
48
frontend/src/Activity/Queue/QueueOptions.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function QueueOptions() {
|
||||
const dispatch = useDispatch();
|
||||
const { includeUnknownMovieItems } = useSelector(
|
||||
(state: AppState) => state.queue.options
|
||||
);
|
||||
|
||||
const handleOptionChange = useCallback(
|
||||
({ name, value }: CheckInputChanged) => {
|
||||
dispatch(
|
||||
setQueueOption({
|
||||
[name]: value,
|
||||
})
|
||||
);
|
||||
|
||||
if (name === 'includeUnknownMovieItems') {
|
||||
dispatch(gotoQueuePage({ page: 1 }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeUnknownMovieItems"
|
||||
value={includeUnknownMovieItems}
|
||||
helpText={translate('ShowUnknownMovieItemsHelpText')}
|
||||
onChange={handleOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueueOptions;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setQueueOption } from 'Store/Actions/queueActions';
|
||||
import QueueOptions from './QueueOptions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.queue.options,
|
||||
(options) => {
|
||||
return options;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onOptionChange: setQueueOption
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);
|
||||
@@ -26,4 +26,5 @@
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueueStatusCell from './QueueStatusCell';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
import TimeleftCell from './TimeleftCell';
|
||||
import styles from './QueueRow.css';
|
||||
|
||||
class QueueRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRemoveQueueItemModalOpen: false,
|
||||
isInteractiveImportModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveQueueItemPress = () => {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
|
||||
const {
|
||||
onRemoveQueueItemPress,
|
||||
onQueueRowModalOpenOrClose
|
||||
} = this.props;
|
||||
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
onRemoveQueueItemPress(blocklist, skipRedownload);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalClose = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(false);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
};
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(true);
|
||||
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
};
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(false);
|
||||
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
downloadId,
|
||||
title,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
movie,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
languages,
|
||||
protocol,
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
isGrabbing,
|
||||
grabError,
|
||||
isRemoving,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isRemoveQueueItemModalOpen,
|
||||
isInteractiveImportModalOpen
|
||||
} = this.state;
|
||||
|
||||
const progress = 100 - (sizeleft / size * 100);
|
||||
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
||||
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<QueueStatusCell
|
||||
key={name}
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
movie ?
|
||||
<MovieTitleLink
|
||||
titleSlug={movie.titleSlug}
|
||||
title={movie.title}
|
||||
/> :
|
||||
title
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
quality ?
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormatScore') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.customFormatScore}
|
||||
>
|
||||
<Tooltip
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
tooltip={<MovieFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'protocol') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'downloadClient') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{downloadClient}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'year') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
movie ? movie.year : ''
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'title') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{title}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'outputPath') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{outputPath}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'estimatedCompletionTime') {
|
||||
return (
|
||||
<TimeleftCell
|
||||
key={name}
|
||||
status={status}
|
||||
estimatedCompletionTime={estimatedCompletionTime}
|
||||
timeleft={timeleft}
|
||||
size={size}
|
||||
sizeleft={sizeleft}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'progress') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.progress}
|
||||
>
|
||||
{
|
||||
!!progress &&
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
title={`${progress.toFixed(1)}%`}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={added}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.actions}
|
||||
>
|
||||
{
|
||||
showInteractiveImport &&
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isPending &&
|
||||
<SpinnerIconButton
|
||||
name={icons.DOWNLOAD}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={onGrabPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<SpinnerIconButton
|
||||
title={translate('RemoveFromQueue')}
|
||||
name={icons.REMOVE}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveQueueItemPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
downloadId={downloadId}
|
||||
title={title}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!movie}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QueueRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
downloadId: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
trackedDownloadState: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
movie: PropTypes.object,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
year: PropTypes.number,
|
||||
sizeleft: PropTypes.number,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.object,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
onRemoveQueueItemPress: PropTypes.func.isRequired,
|
||||
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
QueueRow.defaultProps = {
|
||||
customFormats: [],
|
||||
isGrabbing: false,
|
||||
isRemoving: false
|
||||
};
|
||||
|
||||
export default QueueRow;
|
||||
361
frontend/src/Activity/Queue/QueueRow.tsx
Normal file
361
frontend/src/Activity/Queue/QueueRow.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import { Error } from 'App/State/AppSectionState';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import Language from 'Language/Language';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import {
|
||||
QueueTrackedDownloadState,
|
||||
QueueTrackedDownloadStatus,
|
||||
StatusMessage,
|
||||
} from 'typings/Queue';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueueStatusCell from './QueueStatusCell';
|
||||
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
||||
import TimeleftCell from './TimeleftCell';
|
||||
import styles from './QueueRow.css';
|
||||
|
||||
interface QueueRowProps {
|
||||
id: number;
|
||||
movieId?: number;
|
||||
downloadId?: string;
|
||||
title: string;
|
||||
status: string;
|
||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||
trackedDownloadState?: QueueTrackedDownloadState;
|
||||
statusMessages?: StatusMessage[];
|
||||
errorMessage?: string;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
customFormats?: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
protocol: DownloadProtocol;
|
||||
indexer?: string;
|
||||
outputPath?: string;
|
||||
downloadClient?: string;
|
||||
downloadClientHasPostImportCategory?: boolean;
|
||||
estimatedCompletionTime?: string;
|
||||
added?: string;
|
||||
timeleft?: string;
|
||||
size: number;
|
||||
sizeleft: number;
|
||||
isGrabbing?: boolean;
|
||||
grabError?: Error;
|
||||
isRemoving?: boolean;
|
||||
isSelected?: boolean;
|
||||
columns: Column[];
|
||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||
onQueueRowModalOpenOrClose: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
function QueueRow(props: QueueRowProps) {
|
||||
const {
|
||||
id,
|
||||
movieId,
|
||||
downloadId,
|
||||
title,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
languages,
|
||||
quality,
|
||||
customFormats = [],
|
||||
customFormatScore,
|
||||
protocol,
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
isGrabbing = false,
|
||||
grabError,
|
||||
isRemoving = false,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onQueueRowModalOpenOrClose,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const movie = useMovie(movieId);
|
||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
||||
const [isRemoveQueueItemModalOpen, setIsRemoveQueueItemModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleGrabPress = useCallback(() => {
|
||||
dispatch(grabQueueItem({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleInteractiveImportPress = useCallback(() => {
|
||||
onQueueRowModalOpenOrClose(true);
|
||||
setIsInteractiveImportModalOpen(true);
|
||||
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
||||
|
||||
const handleInteractiveImportModalClose = useCallback(() => {
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
setIsInteractiveImportModalOpen(false);
|
||||
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
||||
|
||||
const handleRemoveQueueItemPress = useCallback(() => {
|
||||
onQueueRowModalOpenOrClose(true);
|
||||
setIsRemoveQueueItemModalOpen(true);
|
||||
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
||||
|
||||
const handleRemoveQueueItemModalConfirmed = useCallback(
|
||||
(payload: RemovePressProps) => {
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
dispatch(removeQueueItem({ id, ...payload }));
|
||||
setIsRemoveQueueItemModalOpen(false);
|
||||
},
|
||||
[id, setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose, dispatch]
|
||||
);
|
||||
|
||||
const handleRemoveQueueItemModalClose = useCallback(() => {
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
setIsRemoveQueueItemModalOpen(false);
|
||||
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
||||
|
||||
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
||||
const showInteractiveImport =
|
||||
status === 'completed' && trackedDownloadStatus === 'warning';
|
||||
const isPending =
|
||||
status === 'delay' || status === 'downloadClientUnavailable';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{columns.map((column) => {
|
||||
const { name, isVisible } = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<QueueStatusCell
|
||||
key={name}
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{movie ? (
|
||||
<MovieTitleLink
|
||||
titleSlug={movie.titleSlug}
|
||||
title={movie.title}
|
||||
/>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'year') {
|
||||
return (
|
||||
<TableRowCell key={name}>{movie ? movie.year : ''}</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguages languages={languages} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{quality ? <MovieQuality quality={quality} /> : null}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormatScore') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.customFormatScore}>
|
||||
<Tooltip
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
tooltip={<MovieFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'protocol') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ProtocolLabel protocol={protocol} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return <TableRowCell key={name}>{indexer}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'downloadClient') {
|
||||
return <TableRowCell key={name}>{downloadClient}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'title') {
|
||||
return <TableRowCell key={name}>{title}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'outputPath') {
|
||||
return <TableRowCell key={name}>{outputPath}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'estimatedCompletionTime') {
|
||||
return (
|
||||
<TimeleftCell
|
||||
key={name}
|
||||
status={status}
|
||||
estimatedCompletionTime={estimatedCompletionTime}
|
||||
timeleft={timeleft}
|
||||
size={size}
|
||||
sizeleft={sizeleft}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'progress') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.progress}>
|
||||
{!!progress && (
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
title={`${progress.toFixed(1)}%`}
|
||||
/>
|
||||
)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return <RelativeDateCell key={name} date={added} />;
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.actions}>
|
||||
{showInteractiveImport ? (
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
onPress={handleInteractiveImportPress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isPending ? (
|
||||
<SpinnerIconButton
|
||||
name={icons.DOWNLOAD}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={handleGrabPress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<SpinnerIconButton
|
||||
title={translate('RemoveFromQueue')}
|
||||
name={icons.REMOVE}
|
||||
isSpinning={isRemoving}
|
||||
onPress={handleRemoveQueueItemPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
downloadId={downloadId}
|
||||
modalTitle={title}
|
||||
onModalClose={handleInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!movie}
|
||||
isPending={isPending}
|
||||
onRemovePress={handleRemoveQueueItemModalConfirmed}
|
||||
onModalClose={handleRemoveQueueItemModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueueRow;
|
||||
@@ -1,67 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import QueueRow from './QueueRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
createUISettingsSelector(),
|
||||
(movie, uiSettings) => {
|
||||
const result = {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
|
||||
result.movie = movie;
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
grabQueueItem,
|
||||
removeQueueItem
|
||||
};
|
||||
|
||||
class QueueRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onGrabPress = () => {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
};
|
||||
|
||||
onRemoveQueueItemPress = (payload) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QueueRow
|
||||
{...this.props}
|
||||
onGrabPress={this.onGrabPress}
|
||||
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
movie: PropTypes.object,
|
||||
grabQueueItem: PropTypes.func.isRequired,
|
||||
removeQueueItem: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);
|
||||
@@ -1,51 +1,59 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Icon, { IconProps } from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import TooltipPosition from 'Helpers/Props/TooltipPosition';
|
||||
import {
|
||||
QueueTrackedDownloadState,
|
||||
QueueTrackedDownloadStatus,
|
||||
StatusMessage,
|
||||
} from 'typings/Queue';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueueStatus.css';
|
||||
|
||||
function getDetailedPopoverBody(statusMessages) {
|
||||
function getDetailedPopoverBody(statusMessages: StatusMessage[]) {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
statusMessages.map(({ title, messages }) => {
|
||||
return (
|
||||
<div
|
||||
key={title}
|
||||
className={messages.length ? undefined: styles.noMessages}
|
||||
>
|
||||
{title}
|
||||
<ul>
|
||||
{
|
||||
messages.map((message) => {
|
||||
return (
|
||||
<li key={message}>
|
||||
{message}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
{statusMessages.map(({ title, messages }) => {
|
||||
return (
|
||||
<div
|
||||
key={title}
|
||||
className={messages.length ? undefined : styles.noMessages}
|
||||
>
|
||||
{title}
|
||||
<ul>
|
||||
{messages.map((message) => {
|
||||
return <li key={message}>{message}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QueueStatus(props) {
|
||||
interface QueueStatusProps {
|
||||
sourceTitle: string;
|
||||
status: string;
|
||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||
trackedDownloadState?: QueueTrackedDownloadState;
|
||||
statusMessages?: StatusMessage[];
|
||||
errorMessage?: string;
|
||||
position: TooltipPosition;
|
||||
canFlip?: boolean;
|
||||
}
|
||||
|
||||
function QueueStatus(props: QueueStatusProps) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
trackedDownloadStatus = 'ok',
|
||||
trackedDownloadState = 'downloading',
|
||||
statusMessages = [],
|
||||
errorMessage,
|
||||
position,
|
||||
canFlip
|
||||
canFlip = false,
|
||||
} = props;
|
||||
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
@@ -53,7 +61,7 @@ function QueueStatus(props) {
|
||||
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let iconKind: IconProps['kind'] = kinds.DEFAULT;
|
||||
let title = translate('Downloading');
|
||||
|
||||
if (status === 'paused') {
|
||||
@@ -115,7 +123,8 @@ function QueueStatus(props) {
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
||||
const warningMessage =
|
||||
errorMessage || translate('CheckDownloadClientForDetails');
|
||||
title = translate('DownloadWarning', { warningMessage });
|
||||
}
|
||||
|
||||
@@ -133,35 +142,17 @@ function QueueStatus(props) {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
}
|
||||
anchor={<Icon name={iconName} kind={iconKind} />}
|
||||
title={title}
|
||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
||||
body={
|
||||
hasWarning || hasError
|
||||
? getDetailedPopoverBody(statusMessages)
|
||||
: sourceTitle
|
||||
}
|
||||
position={position}
|
||||
canFlip={canFlip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
QueueStatus.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
|
||||
canFlip: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
QueueStatus.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading',
|
||||
canFlip: false
|
||||
};
|
||||
|
||||
export default QueueStatus;
|
||||
@@ -1,47 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { tooltipPositions } from 'Helpers/Props';
|
||||
import QueueStatus from './QueueStatus';
|
||||
import styles from './QueueStatusCell.css';
|
||||
|
||||
function QueueStatusCell(props) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.status}>
|
||||
<QueueStatus
|
||||
sourceTitle={sourceTitle}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
QueueStatusCell.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
QueueStatusCell.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading'
|
||||
};
|
||||
|
||||
export default QueueStatusCell;
|
||||
45
frontend/src/Activity/Queue/QueueStatusCell.tsx
Normal file
45
frontend/src/Activity/Queue/QueueStatusCell.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import {
|
||||
QueueTrackedDownloadState,
|
||||
QueueTrackedDownloadStatus,
|
||||
StatusMessage,
|
||||
} from 'typings/Queue';
|
||||
import QueueStatus from './QueueStatus';
|
||||
import styles from './QueueStatusCell.css';
|
||||
|
||||
interface QueueStatusCellProps {
|
||||
sourceTitle: string;
|
||||
status: string;
|
||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||
trackedDownloadState?: QueueTrackedDownloadState;
|
||||
statusMessages?: StatusMessage[];
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
function QueueStatusCell(props: QueueStatusCellProps) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus = 'ok',
|
||||
trackedDownloadState = 'downloading',
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.status}>
|
||||
<QueueStatus
|
||||
sourceTitle={sourceTitle}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
position="right"
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueueStatusCell;
|
||||
@@ -12,7 +12,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
interface RemovePressProps {
|
||||
export interface RemovePressProps {
|
||||
remove: boolean;
|
||||
changeCategory: boolean;
|
||||
blocklist: boolean;
|
||||
@@ -21,7 +21,7 @@ interface RemovePressProps {
|
||||
|
||||
interface RemoveQueueItemModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
sourceTitle?: string;
|
||||
canChangeCategory: boolean;
|
||||
canIgnore: boolean;
|
||||
isPending: boolean;
|
||||
@@ -39,7 +39,7 @@ type BlocklistMethod =
|
||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
sourceTitle = '',
|
||||
canIgnore,
|
||||
canChangeCategory,
|
||||
isPending,
|
||||
|
||||
37
frontend/src/Activity/Queue/Status/QueueStatus.tsx
Normal file
37
frontend/src/Activity/Queue/Status/QueueStatus.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||
import createQueueStatusSelector from './createQueueStatusSelector';
|
||||
|
||||
function QueueStatus() {
|
||||
const dispatch = useDispatch();
|
||||
const { isConnected, isReconnecting } = useSelector(
|
||||
(state: AppState) => state.app
|
||||
);
|
||||
const { isPopulated, count, errors, warnings } = useSelector(
|
||||
createQueueStatusSelector()
|
||||
);
|
||||
|
||||
const wasReconnecting = usePrevious(isReconnecting);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPopulated) {
|
||||
dispatch(fetchQueueStatus());
|
||||
}
|
||||
}, [isPopulated, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && wasReconnecting) {
|
||||
dispatch(fetchQueueStatus());
|
||||
}
|
||||
}, [isConnected, wasReconnecting, dispatch]);
|
||||
|
||||
return (
|
||||
<PageSidebarStatus count={count} errors={errors} warnings={warnings} />
|
||||
);
|
||||
}
|
||||
|
||||
export default QueueStatus;
|
||||
@@ -1,76 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app,
|
||||
(state) => state.queue.status,
|
||||
(state) => state.queue.options.includeUnknownMovieItems,
|
||||
(app, status, includeUnknownMovieItems) => {
|
||||
const {
|
||||
errors,
|
||||
warnings,
|
||||
unknownErrors,
|
||||
unknownWarnings,
|
||||
count,
|
||||
totalCount
|
||||
} = status.item;
|
||||
|
||||
return {
|
||||
isConnected: app.isConnected,
|
||||
isReconnecting: app.isReconnecting,
|
||||
isPopulated: status.isPopulated,
|
||||
...status.item,
|
||||
count: includeUnknownMovieItems ? totalCount : count,
|
||||
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
|
||||
warnings: includeUnknownMovieItems ? warnings || unknownWarnings : warnings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchQueueStatus
|
||||
};
|
||||
|
||||
class QueueStatusConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.fetchQueueStatus();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.isConnected && prevProps.isReconnecting) {
|
||||
this.props.fetchQueueStatus();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageSidebarStatus
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueStatusConnector.propTypes = {
|
||||
isConnected: PropTypes.bool.isRequired,
|
||||
isReconnecting: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
fetchQueueStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createQueueStatusSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.status.isPopulated,
|
||||
(state: AppState) => state.queue.status.item,
|
||||
(state: AppState) => state.queue.options.includeUnknownMovieItems,
|
||||
(isPopulated, status, includeUnknownMovieItems) => {
|
||||
const {
|
||||
errors,
|
||||
warnings,
|
||||
unknownErrors,
|
||||
unknownWarnings,
|
||||
count,
|
||||
totalCount,
|
||||
} = status;
|
||||
|
||||
return {
|
||||
...status,
|
||||
isPopulated,
|
||||
count: includeUnknownMovieItems ? totalCount : count,
|
||||
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
|
||||
warnings: includeUnknownMovieItems
|
||||
? warnings || unknownWarnings
|
||||
: warnings,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createQueueStatusSelector;
|
||||
@@ -1,4 +1,3 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
@@ -11,7 +10,18 @@ import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TimeleftCell.css';
|
||||
|
||||
function TimeleftCell(props) {
|
||||
interface TimeleftCellProps {
|
||||
estimatedCompletionTime?: string;
|
||||
timeleft?: string;
|
||||
status: string;
|
||||
size: number;
|
||||
sizeleft: number;
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
timeFormat: string;
|
||||
}
|
||||
|
||||
function TimeleftCell(props: TimeleftCellProps) {
|
||||
const {
|
||||
estimatedCompletionTime,
|
||||
timeleft,
|
||||
@@ -20,12 +30,18 @@ function TimeleftCell(props) {
|
||||
sizeleft,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
timeFormat,
|
||||
} = props;
|
||||
|
||||
if (status === 'delay') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
const date = getRelativeDate({
|
||||
date: estimatedCompletionTime,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
});
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||
includeMinuteZero: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
@@ -40,8 +56,14 @@ function TimeleftCell(props) {
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
const date = getRelativeDate({
|
||||
date: estimatedCompletionTime,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
});
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||
includeMinuteZero: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
@@ -56,11 +78,7 @@ function TimeleftCell(props) {
|
||||
}
|
||||
|
||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
return <TableRowCell className={styles.timeleft}>-</TableRowCell>;
|
||||
}
|
||||
|
||||
const totalSize = formatBytes(size);
|
||||
@@ -76,15 +94,4 @@ function TimeleftCell(props) {
|
||||
);
|
||||
}
|
||||
|
||||
TimeleftCell.propTypes = {
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default TimeleftCell;
|
||||
@@ -6,7 +6,6 @@ import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
@@ -36,7 +35,6 @@ const mapDispatchToProps = {
|
||||
lookupMovie,
|
||||
clearAddMovie,
|
||||
fetchRootFolders,
|
||||
fetchImportExclusions,
|
||||
fetchQueueDetails,
|
||||
clearQueueDetails,
|
||||
fetchMovieFiles,
|
||||
@@ -56,7 +54,6 @@ class AddNewMovieConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchRootFolders();
|
||||
this.props.fetchImportExclusions();
|
||||
this.props.fetchQueueDetails();
|
||||
}
|
||||
|
||||
@@ -131,7 +128,6 @@ AddNewMovieConnector.propTypes = {
|
||||
lookupMovie: PropTypes.func.isRequired,
|
||||
clearAddMovie: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired,
|
||||
fetchImportExclusions: PropTypes.func.isRequired,
|
||||
fetchQueueDetails: PropTypes.func.isRequired,
|
||||
clearQueueDetails: PropTypes.func.isRequired,
|
||||
fetchMovieFiles: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
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 { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AddNewMovieModalContent.css';
|
||||
@@ -115,13 +118,28 @@ class AddNewMovieModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
onChange={onInputChange}
|
||||
{...minimumAvailability}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.originalLanguage,
|
||||
.studio,
|
||||
.genres {
|
||||
margin-left: 5px;
|
||||
|
||||
@@ -8,6 +8,7 @@ interface CssExports {
|
||||
'genres': string;
|
||||
'icons': string;
|
||||
'links': string;
|
||||
'originalLanguage': string;
|
||||
'overlay': string;
|
||||
'overview': string;
|
||||
'poster': string;
|
||||
|
||||
@@ -62,6 +62,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
titleSlug,
|
||||
year,
|
||||
studio,
|
||||
originalLanguage,
|
||||
genres,
|
||||
status,
|
||||
overview,
|
||||
@@ -70,7 +71,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
images,
|
||||
existingMovieId,
|
||||
isExistingMovie,
|
||||
isExclusionMovie,
|
||||
isExcluded,
|
||||
isSmallScreen,
|
||||
colorImpairedMode,
|
||||
id,
|
||||
@@ -154,26 +155,27 @@ class AddNewMovieSearchResult extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.icons}>
|
||||
<div>
|
||||
{
|
||||
isExistingMovie &&
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title={translate('AlreadyInYourLibrary')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isExistingMovie &&
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title={translate('AlreadyInYourLibrary')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isExclusionMovie &&
|
||||
<Icon
|
||||
className={styles.exclusionIcon}
|
||||
name={icons.DANGER}
|
||||
size={36}
|
||||
title={translate('MovieIsOnImportExclusionList')}
|
||||
/>
|
||||
}
|
||||
{
|
||||
isExcluded &&
|
||||
<Icon
|
||||
className={styles.exclusionIcon}
|
||||
name={icons.DANGER}
|
||||
size={36}
|
||||
title={translate('MovieIsOnImportExclusionList')}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -213,17 +215,31 @@ class AddNewMovieSearchResult extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!!studio &&
|
||||
originalLanguage?.name ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<Icon
|
||||
name={icons.LANGUAGE}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.originalLanguage}>
|
||||
{originalLanguage.name}
|
||||
</span>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
studio ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<Icon
|
||||
name={icons.STUDIO}
|
||||
size={13}
|
||||
/>
|
||||
|
||||
<span className={styles.studio}>
|
||||
{studio}
|
||||
</span>
|
||||
</Label>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
@@ -233,7 +249,6 @@ class AddNewMovieSearchResult extends Component {
|
||||
name={icons.GENRE}
|
||||
size={13}
|
||||
/>
|
||||
|
||||
<span className={styles.genres}>
|
||||
{genres.slice(0, 3).join(', ')}
|
||||
</span>
|
||||
@@ -271,6 +286,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
{
|
||||
isExistingMovie && isSmallScreen &&
|
||||
<MovieStatusLabel
|
||||
status={status}
|
||||
hasMovieFiles={hasMovieFile}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
@@ -311,6 +327,7 @@ AddNewMovieSearchResult.propTypes = {
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
studio: PropTypes.string,
|
||||
originalLanguage: PropTypes.object,
|
||||
genres: PropTypes.arrayOf(PropTypes.string),
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
@@ -319,7 +336,7 @@ AddNewMovieSearchResult.propTypes = {
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
existingMovieId: PropTypes.number,
|
||||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
isExclusionMovie: PropTypes.bool.isRequired,
|
||||
isExcluded: PropTypes.bool,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
id: PropTypes.number,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
@@ -333,7 +350,8 @@ AddNewMovieSearchResult.propTypes = {
|
||||
};
|
||||
|
||||
AddNewMovieSearchResult.defaultProps = {
|
||||
genres: []
|
||||
genres: [],
|
||||
isExcluded: false
|
||||
};
|
||||
|
||||
export default AddNewMovieSearchResult;
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector';
|
||||
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
|
||||
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingMovieSelector(),
|
||||
createExclusionMovieSelector(),
|
||||
createDimensionsSelector(),
|
||||
(state) => state.queue.details.items,
|
||||
(state) => state.movieFiles.items,
|
||||
(state, { internalId }) => internalId,
|
||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||
(isExistingMovie, isExclusionMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
||||
(isExistingMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
||||
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
|
||||
return {
|
||||
existingMovieId: internalId,
|
||||
isExistingMovie,
|
||||
isExclusionMovie,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
queueItem,
|
||||
movieFile,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import Icon from 'Components/Icon';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ImportMovieHeader.css';
|
||||
|
||||
@@ -46,7 +50,19 @@ function ImportMovieHeader(props) {
|
||||
className={styles.minimumAvailability}
|
||||
name="minimumAvailability"
|
||||
>
|
||||
{translate('MinAvailability')}
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.detailsIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function MovieMinimumAvailabilityPopoverContent() {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Announced')}
|
||||
data={translate('AnnouncedMovieAvailabilityDescription')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('InCinemas')}
|
||||
data={translate('InCinemasMovieAvailabilityDescription')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Released')}
|
||||
data={translate('ReleasedMovieAvailabilityDescription')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieMinimumAvailabilityPopoverContent;
|
||||
@@ -1,9 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
|
||||
import Blocklist from 'Activity/Blocklist/Blocklist';
|
||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||
import Queue from 'Activity/Queue/Queue';
|
||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||
@@ -115,12 +115,12 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/activity/queue"
|
||||
component={QueueConnector}
|
||||
component={Queue}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/activity/blocklist"
|
||||
component={BlocklistConnector}
|
||||
component={Blocklist}
|
||||
/>
|
||||
|
||||
{/*
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
import AppState from './State/AppState';
|
||||
|
||||
interface ApplyThemeProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Radarr.theme,
|
||||
@@ -17,7 +13,7 @@ function createThemeSelector() {
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ children }: ApplyThemeProps) {
|
||||
function ApplyTheme() {
|
||||
const theme = useSelector(createThemeSelector());
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
@@ -31,7 +27,7 @@ function ApplyTheme({ children }: ApplyThemeProps) {
|
||||
updateCSSVariables();
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
return null;
|
||||
}
|
||||
|
||||
export default ApplyTheme;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Column from 'Components/Table/Column';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import { FilterBuilderProp } from './AppState';
|
||||
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
@@ -18,10 +19,18 @@ export interface AppSectionSaveState {
|
||||
}
|
||||
|
||||
export interface PagedAppSectionState {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
totalRecords?: number;
|
||||
}
|
||||
export interface TableAppSectionState {
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
export interface AppSectionFilterState<T> {
|
||||
selectedFilterKey: string;
|
||||
filters: PropertyFilter[];
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
}
|
||||
|
||||
@@ -38,6 +47,7 @@ export interface AppSectionItemState<T> {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
pendingChanges: Partial<T>;
|
||||
item: T;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||
import MovieCreditAppState from './MovieCreditAppState';
|
||||
import MovieFilesAppState from './MovieFilesAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||
import ParseAppState from './ParseAppState';
|
||||
@@ -46,6 +47,9 @@ export interface CustomFilter {
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
version: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
@@ -61,6 +65,7 @@ interface AppState {
|
||||
history: HistoryAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieCollections: MovieCollectionAppState;
|
||||
movieCredits: MovieCreditAppState;
|
||||
movieFiles: MovieFilesAppState;
|
||||
movieIndex: MovieIndexAppState;
|
||||
movies: MoviesAppState;
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import Blocklist from 'typings/Blocklist';
|
||||
import AppSectionState, { AppSectionFilterState } from './AppSectionState';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState,
|
||||
} from './AppSectionState';
|
||||
|
||||
interface BlocklistAppState
|
||||
extends AppSectionState<Blocklist>,
|
||||
AppSectionFilterState<Blocklist> {}
|
||||
AppSectionFilterState<Blocklist>,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState {
|
||||
isRemoving: boolean;
|
||||
}
|
||||
|
||||
export default BlocklistAppState;
|
||||
|
||||
6
frontend/src/App/State/MovieCreditAppState.ts
Normal file
6
frontend/src/App/State/MovieCreditAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import MovieCredit from 'typings/MovieCredit';
|
||||
|
||||
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
|
||||
|
||||
export default MovieCreditAppState;
|
||||
@@ -20,11 +20,15 @@ export interface MovieIndexAppState {
|
||||
showTitle: boolean;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showReleaseDate: boolean;
|
||||
showCinemaRelease: boolean;
|
||||
showDigitalRelease: boolean;
|
||||
showPhysicalRelease: boolean;
|
||||
showReleaseDate: boolean;
|
||||
showTmdbRating: boolean;
|
||||
showImdbRating: boolean;
|
||||
showRottenTomatoesRating: boolean;
|
||||
showTraktRating: boolean;
|
||||
showTags: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
@@ -37,6 +41,7 @@ export interface MovieIndexAppState {
|
||||
showAdded: boolean;
|
||||
showPath: boolean;
|
||||
showSizeOnDisk: boolean;
|
||||
showTags: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,15 +3,29 @@ import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
AppSectionItemState,
|
||||
Error,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState,
|
||||
} from './AppSectionState';
|
||||
|
||||
export interface QueueStatus {
|
||||
totalCount: number;
|
||||
count: number;
|
||||
unknownCount: number;
|
||||
errors: boolean;
|
||||
warnings: boolean;
|
||||
unknownErrors: boolean;
|
||||
unknownWarnings: boolean;
|
||||
}
|
||||
|
||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||
params: unknown;
|
||||
}
|
||||
|
||||
export interface QueuePagedAppState
|
||||
extends AppSectionState<Queue>,
|
||||
AppSectionFilterState<Queue> {
|
||||
AppSectionFilterState<Queue>,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState {
|
||||
isGrabbing: boolean;
|
||||
grabError: Error;
|
||||
isRemoving: boolean;
|
||||
@@ -19,9 +33,12 @@ export interface QueuePagedAppState
|
||||
}
|
||||
|
||||
interface QueueAppState {
|
||||
status: AppSectionItemState<Queue>;
|
||||
status: AppSectionItemState<QueueStatus>;
|
||||
details: QueueDetailsAppState;
|
||||
paged: QueuePagedAppState;
|
||||
options: {
|
||||
includeUnknownMovieItems: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default QueueAppState;
|
||||
|
||||
@@ -3,19 +3,30 @@ import AppSectionState, {
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
PagedAppSectionState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Language from 'Language/Language';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
import General from 'typings/Settings/General';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export interface GeneralAppState
|
||||
extends AppSectionItemState<General>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListAppState
|
||||
@@ -26,7 +37,9 @@ export interface ImportListAppState
|
||||
export interface IndexerAppState
|
||||
extends AppSectionState<Indexer>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
AppSectionSaveState {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationAppState
|
||||
extends AppSectionState<Notification>,
|
||||
@@ -36,12 +49,34 @@ export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export interface CustomFormatAppState
|
||||
extends AppSectionState<CustomFormat>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListOptionsSettingsAppState
|
||||
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListExclusionsSettingsAppState
|
||||
extends AppSectionState<ImportListExclusion>,
|
||||
AppSectionSaveState,
|
||||
PagedAppSectionState,
|
||||
AppSectionDeleteState {
|
||||
pendingChanges: Partial<ImportListExclusion>;
|
||||
}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
customFormats: CustomFormatAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import DiskSpace from 'typings/DiskSpace';
|
||||
import Health from 'typings/Health';
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
import Task from 'typings/Task';
|
||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
||||
export type HealthAppState = AppSectionState<Health>;
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type TaskAppState = AppSectionState<Task>;
|
||||
|
||||
interface SystemAppState {
|
||||
diskSpace: DiskSpaceAppState;
|
||||
health: HealthAppState;
|
||||
status: SystemStatusAppState;
|
||||
tasks: TaskAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
|
||||
@@ -3,10 +3,10 @@ import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AgendaEvent.css';
|
||||
|
||||
@@ -82,7 +82,7 @@ class AgendaEvent extends Component {
|
||||
startTime = moment(startTime);
|
||||
const downloading = !!(queueItem || grabbed);
|
||||
const isMonitored = monitored;
|
||||
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', downloading);
|
||||
const statusStyle = getStatusStyle(hasFile, downloading, isMonitored, isAvailable);
|
||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||
const link = `/movie/${titleSlug}`;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class DayOfWeek extends Component {
|
||||
if (view === calendarViews.WEEK) {
|
||||
formatedDate = momentDate.format(calendarWeekColumnHeader);
|
||||
} else if (view === calendarViews.FORECAST) {
|
||||
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
|
||||
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,10 +2,10 @@ import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
||||
import styles from './CalendarEvent.css';
|
||||
@@ -39,7 +39,7 @@ class CalendarEvent extends Component {
|
||||
|
||||
const isDownloading = !!(queueItem || grabbed);
|
||||
const isMonitored = monitored;
|
||||
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', isDownloading);
|
||||
const statusStyle = getStatusStyle(hasFile, isDownloading, isMonitored, isAvailable);
|
||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||
const link = `/movie/${titleSlug}`;
|
||||
const eventType = [];
|
||||
|
||||
25
frontend/src/Calendar/getStatusStyle.js
Normal file
25
frontend/src/Calendar/getStatusStyle.js
Normal file
@@ -0,0 +1,25 @@
|
||||
function getStatusStyle(hasFile, downloading, isMonitored, isAvailable) {
|
||||
if (downloading) {
|
||||
return 'queue';
|
||||
}
|
||||
|
||||
if (hasFile && isMonitored) {
|
||||
return 'downloaded';
|
||||
}
|
||||
|
||||
if (hasFile && !isMonitored) {
|
||||
return 'unmonitored';
|
||||
}
|
||||
|
||||
if (isAvailable && isMonitored) {
|
||||
return 'missingMonitored';
|
||||
}
|
||||
|
||||
if (!isMonitored) {
|
||||
return 'missingUnmonitored';
|
||||
}
|
||||
|
||||
return 'continuing';
|
||||
}
|
||||
|
||||
export default getStatusStyle;
|
||||
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import getProgressBarKind from 'Utilities/Movie/getProgressBarKind';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './CollectionMovieLabel.css';
|
||||
|
||||
@@ -45,7 +45,7 @@ class CollectionMovieLabel extends Component {
|
||||
<div
|
||||
className={classNames(
|
||||
styles.movieStatus,
|
||||
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
|
||||
styles[getProgressBarKind(status, monitored, hasFile, isAvailable)]
|
||||
)}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import styles from './CircularProgressBar.css';
|
||||
|
||||
class CircularProgressBar extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
progress: 0
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._progressStep();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const progress = this.props.progress;
|
||||
|
||||
if (prevProps.progress !== progress) {
|
||||
this._cancelProgressStep();
|
||||
this._progressStep();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._cancelProgressStep();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_progressStep() {
|
||||
this.requestAnimationFrame = window.requestAnimationFrame(() => {
|
||||
this.setState({
|
||||
progress: this.state.progress + 1
|
||||
}, () => {
|
||||
if (this.state.progress < this.props.progress) {
|
||||
this._progressStep();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_cancelProgressStep() {
|
||||
if (this.requestAnimationFrame) {
|
||||
window.cancelAnimationFrame(this.requestAnimationFrame);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
containerClassName,
|
||||
size,
|
||||
strokeWidth,
|
||||
strokeColor,
|
||||
showProgressText
|
||||
} = this.props;
|
||||
|
||||
const progress = this.state.progress;
|
||||
|
||||
const center = size / 2;
|
||||
const radius = center - strokeWidth;
|
||||
const circumference = Math.PI * (radius * 2);
|
||||
const sizeInPixels = `${size}px`;
|
||||
const strokeDashoffset = ((100 - progress) / 100) * circumference;
|
||||
const progressText = `${Math.round(progress)}%`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={containerClassName}
|
||||
style={{
|
||||
width: sizeInPixels,
|
||||
height: sizeInPixels,
|
||||
lineHeight: sizeInPixels
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={className}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
>
|
||||
<circle
|
||||
fill="transparent"
|
||||
r={radius}
|
||||
cx={center}
|
||||
cy={center}
|
||||
strokeDasharray={circumference}
|
||||
style={{
|
||||
stroke: strokeColor,
|
||||
strokeWidth,
|
||||
strokeDashoffset
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{
|
||||
showProgressText &&
|
||||
<div className={styles.circularProgressBarText}>
|
||||
{progressText}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CircularProgressBar.propTypes = {
|
||||
className: PropTypes.string,
|
||||
containerClassName: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
progress: PropTypes.number.isRequired,
|
||||
strokeWidth: PropTypes.number,
|
||||
strokeColor: PropTypes.string,
|
||||
showProgressText: PropTypes.bool
|
||||
};
|
||||
|
||||
CircularProgressBar.defaultProps = {
|
||||
className: styles.circularProgressBar,
|
||||
containerClassName: styles.circularProgressBarContainer,
|
||||
size: 60,
|
||||
strokeWidth: 5,
|
||||
strokeColor: '#ffc230',
|
||||
showProgressText: false
|
||||
};
|
||||
|
||||
export default CircularProgressBar;
|
||||
99
frontend/src/Components/CircularProgressBar.tsx
Normal file
99
frontend/src/Components/CircularProgressBar.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import styles from './CircularProgressBar.css';
|
||||
|
||||
interface CircularProgressBarProps {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
size?: number;
|
||||
progress: number;
|
||||
strokeWidth?: number;
|
||||
strokeColor?: string;
|
||||
showProgressText?: boolean;
|
||||
}
|
||||
|
||||
function CircularProgressBar({
|
||||
className = styles.circularProgressBar,
|
||||
containerClassName = styles.circularProgressBarContainer,
|
||||
size = 60,
|
||||
strokeWidth = 5,
|
||||
strokeColor = '#ffc230',
|
||||
showProgressText = false,
|
||||
progress,
|
||||
}: CircularProgressBarProps) {
|
||||
const [currentProgress, setCurrentProgress] = useState(0);
|
||||
const raf = React.useRef<number>(0);
|
||||
const center = size / 2;
|
||||
const radius = center - strokeWidth;
|
||||
const circumference = Math.PI * (radius * 2);
|
||||
const sizeInPixels = `${size}px`;
|
||||
const strokeDashoffset = ((100 - currentProgress) / 100) * circumference;
|
||||
const progressText = `${Math.round(currentProgress)}%`;
|
||||
|
||||
const handleAnimation = useCallback(
|
||||
(p: number) => {
|
||||
setCurrentProgress((prevProgress) => {
|
||||
if (prevProgress < p) {
|
||||
return prevProgress + Math.min(1, p - prevProgress);
|
||||
}
|
||||
|
||||
return prevProgress;
|
||||
});
|
||||
},
|
||||
[setCurrentProgress]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (progress > currentProgress) {
|
||||
cancelAnimationFrame(raf.current);
|
||||
|
||||
raf.current = requestAnimationFrame(() => handleAnimation(progress));
|
||||
}
|
||||
}, [progress, currentProgress, handleAnimation]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
return () => cancelAnimationFrame(raf.current);
|
||||
},
|
||||
// We only want to run this effect once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={containerClassName}
|
||||
style={{
|
||||
width: sizeInPixels,
|
||||
height: sizeInPixels,
|
||||
lineHeight: sizeInPixels,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={className}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
>
|
||||
<circle
|
||||
fill="transparent"
|
||||
r={radius}
|
||||
cx={center}
|
||||
cy={center}
|
||||
strokeDasharray={circumference}
|
||||
style={{
|
||||
stroke: strokeColor,
|
||||
strokeWidth,
|
||||
strokeDashoffset,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{showProgressText && (
|
||||
<div className={styles.circularProgressBarText}>{progressText}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CircularProgressBar;
|
||||
@@ -63,7 +63,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
||||
<div>{info.componentStack}</div>
|
||||
)}
|
||||
|
||||
{<div className={styles.version}>Version: {window.Radarr.version}</div>}
|
||||
<div className={styles.version}>Version: {window.Radarr.version}</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
@@ -14,7 +15,7 @@ import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilte
|
||||
import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
||||
import ReleaseStatusFilterBuilderRowValue from './ReleaseStatusFilterBuilderRowValue';
|
||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||
import styles from './FilterBuilderRow.css';
|
||||
@@ -77,7 +78,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
return QualityFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||
return QualityProfileFilterBuilderRowValueConnector;
|
||||
return QualityProfileFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.MOVIE:
|
||||
return MovieFilterBuilderRowValue;
|
||||
@@ -228,7 +229,7 @@ class FilterBuilderRow extends Component {
|
||||
key: name,
|
||||
value: typeof label === 'function' ? label() : label
|
||||
};
|
||||
}).sort((a, b) => a.value.localeCompare(b.value));
|
||||
}).sort(sortByProp('value'));
|
||||
|
||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||
|
||||
|
||||
@@ -56,7 +56,9 @@ function getValue(input, selectedFilterBuilderProp) {
|
||||
}
|
||||
|
||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
||||
return parseInt(input);
|
||||
const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
|
||||
|
||||
return Number(input).toFixed(numberFractionDigits);
|
||||
}
|
||||
|
||||
return input;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { filterBuilderTypes } from 'Helpers/Props';
|
||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createTagListSelector() {
|
||||
@@ -38,7 +38,7 @@ function createTagListSelector() {
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).sort(sortByName);
|
||||
}, []).sort(sortByProp('name'));
|
||||
}
|
||||
|
||||
return _.uniqBy(items, 'id');
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Movie from 'Movie/Movie';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
@@ -11,7 +11,7 @@ function MovieFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
|
||||
const tagList = allMovies
|
||||
.map((movie) => ({ id: movie.id, name: movie.title }))
|
||||
.sort(sortByName);
|
||||
.sort(sortByProp('name'));
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createQualityProfilesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.qualityProfiles.items,
|
||||
(qualityProfiles) => {
|
||||
return qualityProfiles;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function QualityProfileFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
const qualityProfiles = useSelector(createQualityProfilesSelector());
|
||||
|
||||
const tagList = qualityProfiles
|
||||
.map(({ id, name }) => ({ id, name }))
|
||||
.sort(sortByProp('name'));
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default QualityProfileFilterBuilderRowValue;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const tagList = qualityProfiles.items.map((qualityProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = qualityProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
||||
@@ -5,6 +5,7 @@ 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 sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFilter from './CustomFilter';
|
||||
import styles from './CustomFiltersModalContent.css';
|
||||
@@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) {
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.sort((a, b) => sortByProp(a, b, 'label'))
|
||||
.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const availabilityOptions = [
|
||||
{
|
||||
key: 'announced',
|
||||
get value() {
|
||||
return translate('Announced');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'inCinemas',
|
||||
get value() {
|
||||
return translate('InCinemas');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'released',
|
||||
get value() {
|
||||
return translate('Released');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function AvailabilitySelectInput(props) {
|
||||
const values = [...availabilityOptions];
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeMixed
|
||||
} = props;
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...props}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
AvailabilitySelectInput.propTypes = {
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
includeMixed: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
AvailabilitySelectInput.defaultProps = {
|
||||
includeNoChange: false,
|
||||
includeMixed: false
|
||||
};
|
||||
|
||||
export default AvailabilitySelectInput;
|
||||
67
frontend/src/Components/Form/AvailabilitySelectInput.tsx
Normal file
67
frontend/src/Components/Form/AvailabilitySelectInput.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
interface AvailabilitySelectInputProps {
|
||||
includeNoChange: boolean;
|
||||
includeNoChangeDisabled?: boolean;
|
||||
includeMixed?: boolean;
|
||||
}
|
||||
|
||||
interface IMovieAvailabilityOption {
|
||||
key: string;
|
||||
value: string;
|
||||
format?: string;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const movieAvailabilityOptions: IMovieAvailabilityOption[] = [
|
||||
{
|
||||
key: 'announced',
|
||||
get value() {
|
||||
return translate('Announced');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'inCinemas',
|
||||
get value() {
|
||||
return translate('InCinemas');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'released',
|
||||
get value() {
|
||||
return translate('Released');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function AvailabilitySelectInput(props: AvailabilitySelectInputProps) {
|
||||
const values = [...movieAvailabilityOptions];
|
||||
|
||||
const {
|
||||
includeNoChange = false,
|
||||
includeNoChangeDisabled = true,
|
||||
includeMixed = false,
|
||||
} = props;
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
isDisabled: includeNoChangeDisabled,
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: `(${translate('Mixed')})`,
|
||||
isDisabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
return <EnhancedSelectInput {...props} values={values} />;
|
||||
}
|
||||
|
||||
export default AvailabilitySelectInput;
|
||||
@@ -4,7 +4,8 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -22,7 +23,7 @@ function createMapStateToProps() {
|
||||
|
||||
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
||||
|
||||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||
const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => {
|
||||
return {
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name,
|
||||
@@ -33,7 +34,7 @@ function createMapStateToProps() {
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
value: `(${translate('Any')})`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -18,7 +18,7 @@ function createMapStateToProps() {
|
||||
items
|
||||
} = indexers;
|
||||
|
||||
const values = items.sort(sortByName).map((indexer) => ({
|
||||
const values = items.sort(sortByProp('name')).map((indexer) => ({
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
}));
|
||||
|
||||
@@ -5,17 +5,20 @@ import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function MovieMonitoredSelectInput(props) {
|
||||
const values = [...monitorOptions];
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeMixed
|
||||
includeMixed,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const values = [...monitorOptions];
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
@@ -23,14 +26,16 @@ function MovieMonitoredSelectInput(props) {
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
get value() {
|
||||
return `(${translate('Mixed')})`;
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...props}
|
||||
{...otherProps}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -4,13 +4,13 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
||||
createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')),
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
|
||||
@@ -52,6 +52,7 @@ class SelectInput extends Component {
|
||||
const {
|
||||
key,
|
||||
value: optionValue,
|
||||
isDisabled: optionIsDisabled = false,
|
||||
...otherOptionProps
|
||||
} = option;
|
||||
|
||||
@@ -59,6 +60,7 @@ class SelectInput extends Component {
|
||||
<option
|
||||
key={key}
|
||||
value={key}
|
||||
disabled={optionIsDisabled}
|
||||
{...otherOptionProps}
|
||||
>
|
||||
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.hasError {
|
||||
composes: hasError from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.hasWarning {
|
||||
composes: hasWarning from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.internalInput {
|
||||
flex: 1 1 0%;
|
||||
margin-left: 3px;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'hasError': string;
|
||||
'hasWarning': string;
|
||||
'input': string;
|
||||
'internalInput': string;
|
||||
'isFocused': string;
|
||||
|
||||
@@ -225,6 +225,8 @@ class TagInput extends Component {
|
||||
const {
|
||||
className,
|
||||
inputContainerClassName,
|
||||
hasError,
|
||||
hasWarning,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -241,7 +243,9 @@ class TagInput extends Component {
|
||||
className={className}
|
||||
inputContainerClassName={classNames(
|
||||
inputContainerClassName,
|
||||
isFocused && styles.isFocused
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning
|
||||
)}
|
||||
value={value}
|
||||
suggestions={suggestions}
|
||||
|
||||
@@ -7,17 +7,11 @@
|
||||
}
|
||||
|
||||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
max-width: 100%;
|
||||
line-height: 1px;
|
||||
}
|
||||
|
||||
.linkWithEdit {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
max-width: calc(100% - 9px - 4px - 2px);
|
||||
line-height: 1px;
|
||||
}
|
||||
|
||||
.editContainer {
|
||||
@@ -36,5 +30,6 @@
|
||||
.label {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,11 @@ class TextTagInputConnector extends Component {
|
||||
const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
|
||||
|
||||
newTags.forEach((newTag) => {
|
||||
newValue.push(newTag.trim());
|
||||
const newTagValue = newTag.trim();
|
||||
|
||||
if (newTagValue) {
|
||||
newValue.push(newTagValue);
|
||||
}
|
||||
});
|
||||
|
||||
onChange({ name, value: newValue });
|
||||
@@ -80,7 +84,12 @@ class TextTagInputConnector extends Component {
|
||||
|
||||
const newValue = [...valueArray];
|
||||
newValue.splice(tagToReplace.index, 1);
|
||||
newValue.push(newTag.name.trim());
|
||||
|
||||
const newTagValue = newTag.name.trim();
|
||||
|
||||
if (newTagValue) {
|
||||
newValue.push(newTagValue);
|
||||
}
|
||||
|
||||
onChange({ name, value: newValue });
|
||||
};
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import styles from './Icon.css';
|
||||
|
||||
class Icon extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
containerClassName,
|
||||
className,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
title,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const icon = (
|
||||
<FontAwesomeIcon
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind]
|
||||
)}
|
||||
icon={name}
|
||||
spin={isSpinning}
|
||||
style={{
|
||||
fontSize: `${size}px`
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
|
||||
if (title) {
|
||||
return (
|
||||
<span
|
||||
className={containerClassName}
|
||||
title={typeof title === 'function' ? title() : title}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
Icon.propTypes = {
|
||||
containerClassName: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.object.isRequired,
|
||||
kind: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
fixedWidth: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
Icon.defaultProps = {
|
||||
kind: kinds.DEFAULT,
|
||||
size: 14,
|
||||
isSpinning: false,
|
||||
fixedWidth: false
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
60
frontend/src/Components/Icon.tsx
Normal file
60
frontend/src/Components/Icon.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconProps,
|
||||
} from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import styles from './Icon.css';
|
||||
|
||||
export interface IconProps
|
||||
extends Omit<
|
||||
FontAwesomeIconProps,
|
||||
'icon' | 'spin' | 'name' | 'title' | 'size'
|
||||
> {
|
||||
containerClassName?: ComponentProps<'span'>['className'];
|
||||
name: FontAwesomeIconProps['icon'];
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: number;
|
||||
isSpinning?: FontAwesomeIconProps['spin'];
|
||||
title?: string | (() => string);
|
||||
}
|
||||
|
||||
export default function Icon({
|
||||
containerClassName,
|
||||
className,
|
||||
name,
|
||||
kind = kinds.DEFAULT,
|
||||
size = 14,
|
||||
title,
|
||||
isSpinning = false,
|
||||
fixedWidth = false,
|
||||
...otherProps
|
||||
}: IconProps) {
|
||||
const icon = (
|
||||
<FontAwesomeIcon
|
||||
className={classNames(className, styles[kind])}
|
||||
icon={name}
|
||||
spin={isSpinning}
|
||||
fixedWidth={fixedWidth}
|
||||
style={{
|
||||
fontSize: `${size}px`,
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
|
||||
if (title) {
|
||||
return (
|
||||
<span
|
||||
className={containerClassName}
|
||||
title={typeof title === 'function' ? title() : title}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
|
||||
1
frontend/src/Components/ImdbRating.css.d.ts
vendored
1
frontend/src/Components/ImdbRating.css.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'image': string;
|
||||
'wrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
File diff suppressed because one or more lines are too long
47
frontend/src/Components/ImdbRating.tsx
Normal file
47
frontend/src/Components/ImdbRating.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.title {
|
||||
.name {
|
||||
margin-bottom: 2px;
|
||||
color: var(--helpTextColor);
|
||||
font-size: 10px;
|
||||
|
||||
2
frontend/src/Components/InfoLabel.css.d.ts
vendored
2
frontend/src/Components/InfoLabel.css.d.ts
vendored
@@ -4,9 +4,9 @@ interface CssExports {
|
||||
'label': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
'name': string;
|
||||
'outline': string;
|
||||
'small': string;
|
||||
'title': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -7,7 +7,7 @@ import styles from './InfoLabel.css';
|
||||
function InfoLabel(props) {
|
||||
const {
|
||||
className,
|
||||
title,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
outline,
|
||||
@@ -25,8 +25,8 @@ function InfoLabel(props) {
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
<div>
|
||||
{children}
|
||||
@@ -37,7 +37,7 @@ function InfoLabel(props) {
|
||||
|
||||
InfoLabel.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import styles from './Label.css';
|
||||
|
||||
function Label(props) {
|
||||
const {
|
||||
className,
|
||||
kind,
|
||||
size,
|
||||
outline,
|
||||
children,
|
||||
colorImpairedMode,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline,
|
||||
colorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Label.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
colorImpairedMode: PropTypes.bool
|
||||
};
|
||||
|
||||
Label.defaultProps = {
|
||||
className: styles.label,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.SMALL,
|
||||
outline: false,
|
||||
colorImpairedMode: false
|
||||
};
|
||||
|
||||
export default Label;
|
||||
33
frontend/src/Components/Label.tsx
Normal file
33
frontend/src/Components/Label.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ComponentProps, ReactNode } from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import styles from './Label.css';
|
||||
|
||||
export interface LabelProps extends ComponentProps<'span'> {
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
outline?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Label({
|
||||
className = styles.label,
|
||||
kind = kinds.DEFAULT,
|
||||
size = sizes.SMALL,
|
||||
outline = false,
|
||||
...otherProps
|
||||
}: LabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { align, kinds, sizes } from 'Helpers/Props';
|
||||
import Link from './Link';
|
||||
import styles from './Button.css';
|
||||
|
||||
class Button extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
buttonGroupPosition,
|
||||
kind,
|
||||
size,
|
||||
children,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
buttonGroupPosition && styles[buttonGroupPosition]
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
buttonGroupPosition: PropTypes.oneOf(align.all),
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
size: PropTypes.oneOf(sizes.all),
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
className: styles.button,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.MEDIUM
|
||||
};
|
||||
|
||||
export default Button;
|
||||
37
frontend/src/Components/Link/Button.tsx
Normal file
37
frontend/src/Components/Link/Button.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { align, kinds, sizes } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import Link, { LinkProps } from './Link';
|
||||
import styles from './Button.css';
|
||||
|
||||
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
|
||||
buttonGroupPosition?: Extract<
|
||||
(typeof align.all)[number],
|
||||
keyof typeof styles
|
||||
>;
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
children: Required<LinkProps['children']>;
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
className = styles.button,
|
||||
buttonGroupPosition,
|
||||
kind = kinds.DEFAULT,
|
||||
size = sizes.MEDIUM,
|
||||
...otherProps
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
buttonGroupPosition && styles[buttonGroupPosition]
|
||||
)}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Link from './Link';
|
||||
import styles from './IconButton.css';
|
||||
|
||||
function IconButton(props) {
|
||||
const {
|
||||
className,
|
||||
iconClassName,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
isSpinning,
|
||||
isDisabled,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
aria-label={translate('TableOptionsButton')}
|
||||
isDisabled={isDisabled}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
className={iconClassName}
|
||||
name={name}
|
||||
kind={kind}
|
||||
size={size}
|
||||
isSpinning={isSpinning}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
IconButton.propTypes = {
|
||||
...Link.propTypes,
|
||||
className: PropTypes.string.isRequired,
|
||||
iconClassName: PropTypes.string,
|
||||
kind: PropTypes.string,
|
||||
name: PropTypes.object.isRequired,
|
||||
size: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
isSpinning: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool
|
||||
};
|
||||
|
||||
IconButton.defaultProps = {
|
||||
className: styles.button,
|
||||
size: 12
|
||||
};
|
||||
|
||||
export default IconButton;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user