mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-18 21:34:28 -04:00
Compare commits
88 Commits
v0.2.2.194
...
sonarr-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dc3d69a11 | ||
|
|
695781dde5 | ||
|
|
4e8ddd3018 | ||
|
|
eb67231a45 | ||
|
|
3d3a458828 | ||
|
|
a11930a03f | ||
|
|
abaf39d67e | ||
|
|
894a5943e4 | ||
|
|
be26647afb | ||
|
|
b319a4bce7 | ||
|
|
f03fd7e95e | ||
|
|
7f25a3c4b1 | ||
|
|
e3247dc505 | ||
|
|
3677fd6d34 | ||
|
|
4f6901b1ff | ||
|
|
ce820f6f73 | ||
|
|
53e6cb24b7 | ||
|
|
7c1ca8acc1 | ||
|
|
5e9e578101 | ||
|
|
156407c541 | ||
|
|
1ef6c60318 | ||
|
|
73b3b1848b | ||
|
|
33fbd95707 | ||
|
|
704635f758 | ||
|
|
263e807de2 | ||
|
|
9ac9bd25c1 | ||
|
|
4ead5186ae | ||
|
|
dea797c375 | ||
|
|
58ba24762b | ||
|
|
fbd7b4fe33 | ||
|
|
fee7fbbff6 | ||
|
|
18253a298e | ||
|
|
22f92150c3 | ||
|
|
4d7a762ee8 | ||
|
|
b11517e2ac | ||
|
|
d5af254f47 | ||
|
|
f09da06f80 | ||
|
|
d3443510b4 | ||
|
|
d73eb1b5f9 | ||
|
|
39778a95bf | ||
|
|
9fccca1154 | ||
|
|
e165663616 | ||
|
|
b49d2312ab | ||
|
|
52221c7cf4 | ||
|
|
ad7b110a0b | ||
|
|
b04b483f86 | ||
|
|
b79941e0a1 | ||
|
|
84d47b1f23 | ||
|
|
17df4d47fb | ||
|
|
b9f89dddc9 | ||
|
|
e3fc469cd3 | ||
|
|
4304685a65 | ||
|
|
7d77b1fbe5 | ||
|
|
1989174801 | ||
|
|
ac4ae9bb4d | ||
|
|
f399d27470 | ||
|
|
c5fd2e3aa0 | ||
|
|
e971d68d67 | ||
|
|
af858ac4aa | ||
|
|
63ea253a6b | ||
|
|
484f2eb3ec | ||
|
|
15190aa61a | ||
|
|
a3aac90bf7 | ||
|
|
dd9cbc4f54 | ||
|
|
4bca0d77b7 | ||
|
|
1316b388ad | ||
|
|
243c88ce56 | ||
|
|
921f170234 | ||
|
|
3e102627f5 | ||
|
|
f3b5f0c5cb | ||
|
|
a53516e821 | ||
|
|
f0f95be57f | ||
|
|
f436d730fe | ||
|
|
f7c135faaf | ||
|
|
8bb52105fd | ||
|
|
e5a1b7a72e | ||
|
|
2f2a521391 | ||
|
|
304d1e3462 | ||
|
|
1d1cc6526d | ||
|
|
690e0b5d96 | ||
|
|
212eedd345 | ||
|
|
0b38743292 | ||
|
|
1def54f246 | ||
|
|
0eeaa1e443 | ||
|
|
7beee07a2c | ||
|
|
924f739d1f | ||
|
|
b187fb23e3 | ||
|
|
ca043b3820 |
@@ -275,7 +275,7 @@ dotnet_diagnostic.CA5397.severity = suggestion
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[*.{js,html,js,hbs,less,css}]
|
[*.{js,html,hbs,less,css,ts,tsx}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '0.2.2'
|
majorVersion: '0.3.2'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||||
|
|||||||
13
build.sh
13
build.sh
@@ -391,22 +391,21 @@ then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$FRONTEND" = "YES" ];
|
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
|
||||||
then
|
then
|
||||||
YarnInstall
|
YarnInstall
|
||||||
RunWebpack
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$LINT" = "YES" ];
|
if [ "$LINT" = "YES" ];
|
||||||
then
|
then
|
||||||
if [ -z "$FRONTEND" ];
|
|
||||||
then
|
|
||||||
YarnInstall
|
|
||||||
fi
|
|
||||||
|
|
||||||
LintUI
|
LintUI
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$FRONTEND" = "YES" ];
|
||||||
|
then
|
||||||
|
RunWebpack
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$PACKAGES" = "YES" ];
|
if [ "$PACKAGES" = "YES" ];
|
||||||
then
|
then
|
||||||
UpdateVersionNumber
|
UpdateVersionNumber
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ module.exports = (env) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
entry: {
|
entry: {
|
||||||
index: 'index.js'
|
index: 'index.ts'
|
||||||
},
|
},
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -67,23 +67,23 @@ module.exports = (env) => {
|
|||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: '[name].js',
|
filename: '[name]-[contenthash].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
optimization: {
|
optimization: {
|
||||||
moduleIds: 'deterministic',
|
moduleIds: 'deterministic',
|
||||||
chunkIds: 'named',
|
chunkIds: isProduction ? 'deterministic' : 'named'
|
||||||
splitChunks: {
|
|
||||||
chunks: 'initial',
|
|
||||||
name: 'vendors'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
performance: {
|
performance: {
|
||||||
hints: false
|
hints: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
experiments: {
|
||||||
|
topLevelAwait: true
|
||||||
|
},
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
__DEV__: !isProduction,
|
__DEV__: !isProduction,
|
||||||
@@ -91,13 +91,15 @@ module.exports = (env) => {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'Content/styles.css'
|
filename: 'Content/styles.css',
|
||||||
|
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: 'frontend/src/index.ejs',
|
template: 'frontend/src/index.ejs',
|
||||||
filename: 'index.html',
|
filename: 'index.html',
|
||||||
publicPath: '/'
|
publicPath: '/',
|
||||||
|
inject: false
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new FileManagerPlugin({
|
new FileManagerPlugin({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Link from 'Components/Link/Link';
|
|||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryDetails.css';
|
import styles from './HistoryDetails.css';
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ function HistoryDetails(props) {
|
|||||||
customFormatScore && customFormatScore !== '0' ?
|
customFormatScore && customFormatScore !== '0' ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('CustomFormatScore')}
|
title={translate('CustomFormatScore')}
|
||||||
data={formatPreferredWordScore(customFormatScore)}
|
data={formatCustomFormatScore(customFormatScore)}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -225,7 +225,7 @@ function HistoryDetails(props) {
|
|||||||
customFormatScore && customFormatScore !== '0' ?
|
customFormatScore && customFormatScore !== '0' ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('CustomFormatScore')}
|
title={translate('CustomFormatScore')}
|
||||||
data={formatPreferredWordScore(customFormatScore)}
|
data={formatCustomFormatScore(customFormatScore)}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -271,7 +271,7 @@ function HistoryDetails(props) {
|
|||||||
customFormatScore && customFormatScore !== '0' ?
|
customFormatScore && customFormatScore !== '0' ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('CustomFormatScore')}
|
title={translate('CustomFormatScore')}
|
||||||
data={formatPreferredWordScore(customFormatScore)}
|
data={formatCustomFormatScore(customFormatScore)}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import IconButton from 'Components/Link/IconButton';
|
|||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import { icons } from 'Helpers/Props';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||||
import styles from './HistoryRow.css';
|
import styles from './HistoryRow.css';
|
||||||
@@ -57,6 +58,7 @@ class HistoryRow extends Component {
|
|||||||
book,
|
book,
|
||||||
quality,
|
quality,
|
||||||
customFormats,
|
customFormats,
|
||||||
|
customFormatScore,
|
||||||
qualityCutoffNotMet,
|
qualityCutoffNotMet,
|
||||||
eventType,
|
eventType,
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
@@ -177,7 +179,14 @@ class HistoryRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.customFormatScore}
|
className={styles.customFormatScore}
|
||||||
>
|
>
|
||||||
{formatPreferredWordScore(data.customFormatScore)}
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<BookFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -244,6 +253,7 @@ HistoryRow.propTypes = {
|
|||||||
book: PropTypes.object,
|
book: PropTypes.object,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||||
eventType: PropTypes.string.isRequired,
|
eventType: PropTypes.string.isRequired,
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
@@ -257,4 +267,8 @@ HistoryRow.propTypes = {
|
|||||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
HistoryRow.defaultProps = {
|
||||||
|
customFormats: []
|
||||||
|
};
|
||||||
|
|
||||||
export default HistoryRow;
|
export default HistoryRow;
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customFormatScore {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
|
'customFormatScore': string;
|
||||||
'progress': string;
|
'progress': string;
|
||||||
'protocol': string;
|
'protocol': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QueueStatusCell from './QueueStatusCell';
|
import QueueStatusCell from './QueueStatusCell';
|
||||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||||
@@ -44,14 +46,14 @@ class QueueRow extends Component {
|
|||||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => {
|
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
|
||||||
const {
|
const {
|
||||||
onRemoveQueueItemPress,
|
onRemoveQueueItemPress,
|
||||||
onQueueRowModalOpenOrClose
|
onQueueRowModalOpenOrClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
onQueueRowModalOpenOrClose(false);
|
onQueueRowModalOpenOrClose(false);
|
||||||
onRemoveQueueItemPress(blocklist, skipredownload);
|
onRemoveQueueItemPress(blocklist, skipRedownload);
|
||||||
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||||
};
|
};
|
||||||
@@ -91,6 +93,7 @@ class QueueRow extends Component {
|
|||||||
book,
|
book,
|
||||||
quality,
|
quality,
|
||||||
customFormats,
|
customFormats,
|
||||||
|
customFormatScore,
|
||||||
protocol,
|
protocol,
|
||||||
indexer,
|
indexer,
|
||||||
outputPath,
|
outputPath,
|
||||||
@@ -222,6 +225,24 @@ class QueueRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.customFormatScore}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<BookFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'protocol') {
|
if (name === 'protocol') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
@@ -392,6 +413,7 @@ QueueRow.propTypes = {
|
|||||||
book: PropTypes.object,
|
book: PropTypes.object,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
protocol: PropTypes.string.isRequired,
|
protocol: PropTypes.string.isRequired,
|
||||||
indexer: PropTypes.string,
|
indexer: PropTypes.string,
|
||||||
outputPath: PropTypes.string,
|
outputPath: PropTypes.string,
|
||||||
@@ -416,6 +438,7 @@ QueueRow.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
QueueRow.defaultProps = {
|
QueueRow.defaultProps = {
|
||||||
|
customFormats: [],
|
||||||
isGrabbing: false,
|
isGrabbing: false,
|
||||||
isRemoving: false
|
isRemoving: false
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class RemoveQueueItemModal extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
remove: true,
|
remove: true,
|
||||||
blocklist: false,
|
blocklist: false,
|
||||||
skipredownload: false
|
skipRedownload: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class RemoveQueueItemModal extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
remove: true,
|
remove: true,
|
||||||
blocklist: false,
|
blocklist: false,
|
||||||
skipredownload: false
|
skipRedownload: false
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@ class RemoveQueueItemModal extends Component {
|
|||||||
this.setState({ blocklist: value });
|
this.setState({ blocklist: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSkipReDownloadChange = ({ value }) => {
|
onSkipRedownloadChange = ({ value }) => {
|
||||||
this.setState({ skipredownload: value });
|
this.setState({ skipRedownload: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onRemoveConfirmed = () => {
|
onRemoveConfirmed = () => {
|
||||||
@@ -76,7 +76,7 @@ class RemoveQueueItemModal extends Component {
|
|||||||
isPending
|
isPending
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { remove, blocklist, skipredownload } = this.state;
|
const { remove, blocklist, skipRedownload } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -108,7 +108,7 @@ class RemoveQueueItemModal extends Component {
|
|||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="remove"
|
name="remove"
|
||||||
value={remove}
|
value={remove}
|
||||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||||
isDisabled={!canIgnore}
|
isDisabled={!canIgnore}
|
||||||
onChange={this.onRemoveChange}
|
onChange={this.onRemoveChange}
|
||||||
/>
|
/>
|
||||||
@@ -137,10 +137,10 @@ class RemoveQueueItemModal extends Component {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="skipredownload"
|
name="skipRedownload"
|
||||||
value={skipredownload}
|
value={skipRedownload}
|
||||||
helpText={translate('SkipredownloadHelpText')}
|
helpText={translate('SkipRedownloadHelpText')}
|
||||||
onChange={this.onSkipReDownloadChange}
|
onChange={this.onSkipRedownloadChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
remove: true,
|
remove: true,
|
||||||
blocklist: false,
|
blocklist: false,
|
||||||
skipredownload: false
|
skipRedownload: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
remove: true,
|
remove: true,
|
||||||
blocklist: false,
|
blocklist: false,
|
||||||
skipredownload: false
|
skipRedownload: false
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,8 +50,8 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
this.setState({ blocklist: value });
|
this.setState({ blocklist: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSkipReDownloadChange = ({ value }) => {
|
onSkipRedownloadChange = ({ value }) => {
|
||||||
this.setState({ skipredownload: value });
|
this.setState({ skipRedownload: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onRemoveConfirmed = () => {
|
onRemoveConfirmed = () => {
|
||||||
@@ -77,7 +77,7 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
allPending
|
allPending
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { remove, blocklist, skipredownload } = this.state;
|
const { remove, blocklist, skipRedownload } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -138,10 +138,10 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="skipredownload"
|
name="skipRedownload"
|
||||||
value={skipredownload}
|
value={skipRedownload}
|
||||||
helpText={translate('SkipredownloadHelpText')}
|
helpText={translate('SkipRedownloadHelpText')}
|
||||||
onChange={this.onSkipReDownloadChange}
|
onChange={this.onSkipRedownloadChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
}
|
}
|
||||||
|
|||||||
5
frontend/src/App/ModelBase.ts
Normal file
5
frontend/src/App/ModelBase.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
interface ModelBase {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModelBase;
|
||||||
48
frontend/src/App/State/AppSectionState.ts
Normal file
48
frontend/src/App/State/AppSectionState.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
|
||||||
|
export interface Error {
|
||||||
|
responseJSON: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSectionDeleteState {
|
||||||
|
isDeleting: boolean;
|
||||||
|
deleteError: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSectionSaveState {
|
||||||
|
isSaving: boolean;
|
||||||
|
saveError: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PagedAppSectionState {
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSectionSchemaState<T> {
|
||||||
|
isSchemaFetching: boolean;
|
||||||
|
isSchemaPopulated: boolean;
|
||||||
|
schemaError: Error;
|
||||||
|
schema: {
|
||||||
|
items: T[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSectionItemState<T> {
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
item: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppSectionState<T> {
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
items: T[];
|
||||||
|
sortKey: string;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppSectionState;
|
||||||
41
frontend/src/App/State/AppState.ts
Normal file
41
frontend/src/App/State/AppState.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import SettingsAppState from './SettingsAppState';
|
||||||
|
import TagsAppState from './TagsAppState';
|
||||||
|
|
||||||
|
interface FilterBuilderPropOption {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterBuilderProp<T> {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
valueType?: string;
|
||||||
|
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropertyFilter {
|
||||||
|
key: string;
|
||||||
|
value: boolean | string | number | string[] | number[];
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Filter {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
filers: PropertyFilter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomFilter {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
filers: PropertyFilter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
settings: SettingsAppState;
|
||||||
|
tags: TagsAppState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppState;
|
||||||
40
frontend/src/App/State/SettingsAppState.ts
Normal file
40
frontend/src/App/State/SettingsAppState.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import AppSectionState, {
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
|
import ImportList from 'typings/ImportList';
|
||||||
|
import Indexer from 'typings/Indexer';
|
||||||
|
import Notification from 'typings/Notification';
|
||||||
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
|
|
||||||
|
export interface DownloadClientAppState
|
||||||
|
extends AppSectionState<DownloadClient>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface ImportListAppState
|
||||||
|
extends AppSectionState<ImportList>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface IndexerAppState
|
||||||
|
extends AppSectionState<Indexer>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface NotificationAppState
|
||||||
|
extends AppSectionState<Notification>,
|
||||||
|
AppSectionDeleteState {}
|
||||||
|
|
||||||
|
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||||
|
|
||||||
|
interface SettingsAppState {
|
||||||
|
downloadClients: DownloadClientAppState;
|
||||||
|
importLists: ImportListAppState;
|
||||||
|
indexers: IndexerAppState;
|
||||||
|
notifications: NotificationAppState;
|
||||||
|
uiSettings: UiSettingsAppState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsAppState;
|
||||||
12
frontend/src/App/State/TagsAppState.ts
Normal file
12
frontend/src/App/State/TagsAppState.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import AppSectionState, {
|
||||||
|
AppSectionDeleteState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
|
||||||
|
export interface Tag extends ModelBase {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||||
|
|
||||||
|
export default TagsAppState;
|
||||||
@@ -392,10 +392,7 @@ class AuthorDetails extends Component {
|
|||||||
name={icons.ARROW_UP}
|
name={icons.ARROW_UP}
|
||||||
size={30}
|
size={30}
|
||||||
title={translate('GoToAuthorListing')}
|
title={translate('GoToAuthorListing')}
|
||||||
to={{
|
to={'/'}
|
||||||
pathname: '/',
|
|
||||||
state: { restoreScrollPosition: true }
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
|
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
|
||||||
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
|
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
|
||||||
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
|
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
|
||||||
@@ -9,6 +10,7 @@ import SelectInput from 'Components/Form/SelectInput';
|
|||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { fetchRootFolders } from 'Store/Actions/Settings/rootFolders';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AuthorEditorFooterLabel from './AuthorEditorFooterLabel';
|
import AuthorEditorFooterLabel from './AuthorEditorFooterLabel';
|
||||||
import DeleteAuthorModal from './Delete/DeleteAuthorModal';
|
import DeleteAuthorModal from './Delete/DeleteAuthorModal';
|
||||||
@@ -17,6 +19,10 @@ import styles from './AuthorEditorFooter.css';
|
|||||||
|
|
||||||
const NO_CHANGE = 'noChange';
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchRootFolders: fetchRootFolders
|
||||||
|
};
|
||||||
|
|
||||||
class AuthorEditorFooter extends Component {
|
class AuthorEditorFooter extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -39,6 +45,13 @@ class AuthorEditorFooter extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.dispatchFetchRootFolders();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const {
|
const {
|
||||||
isSaving,
|
isSaving,
|
||||||
@@ -160,9 +173,9 @@ class AuthorEditorFooter extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const monitoredOptions = [
|
const monitoredOptions = [
|
||||||
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||||
{ key: 'monitored', value: 'Monitored' },
|
{ key: 'monitored', value: translate('Monitored') },
|
||||||
{ key: 'unmonitored', value: 'Unmonitored' }
|
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -341,7 +354,8 @@ AuthorEditorFooter.propTypes = {
|
|||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
showMetadataProfile: PropTypes.bool.isRequired,
|
||||||
onSaveSelected: PropTypes.func.isRequired,
|
onSaveSelected: PropTypes.func.isRequired,
|
||||||
onOrganizeAuthorPress: PropTypes.func.isRequired,
|
onOrganizeAuthorPress: PropTypes.func.isRequired,
|
||||||
onRetagAuthorPress: PropTypes.func.isRequired
|
onRetagAuthorPress: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchRootFolders: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthorEditorFooter;
|
export default connect(undefined, mapDispatchToProps)(AuthorEditorFooter);
|
||||||
|
|||||||
@@ -98,10 +98,10 @@ class TagsModalContent extends Component {
|
|||||||
value={applyTags}
|
value={applyTags}
|
||||||
values={applyTagsOptions}
|
values={applyTagsOptions}
|
||||||
helpTexts={[
|
helpTexts={[
|
||||||
translate('ApplyTagsHelpTexts1'),
|
translate('ApplyTagsHelpTextHowToApplyAuthors'),
|
||||||
translate('ApplyTagsHelpTexts2'),
|
translate('ApplyTagsHelpTextAdd'),
|
||||||
translate('ApplyTagsHelpTexts3'),
|
translate('ApplyTagsHelpTextRemove'),
|
||||||
translate('ApplyTagsHelpTexts4')
|
translate('ApplyTagsHelpTextReplace')
|
||||||
]}
|
]}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,14 +14,39 @@ import { inputTypes } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
const nameOptions = [
|
const nameOptions = [
|
||||||
{ key: 'firstLast', value: translate('NameFirstLast') },
|
{
|
||||||
{ key: 'lastFirst', value: translate('NameLastFirst') }
|
key: 'firstLast',
|
||||||
|
get value() {
|
||||||
|
return translate('NameFirstLast');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lastFirst',
|
||||||
|
get value() {
|
||||||
|
return translate('NameLastFirst');
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const posterSizeOptions = [
|
const posterSizeOptions = [
|
||||||
{ key: 'small', value: 'Small' },
|
{
|
||||||
{ key: 'medium', value: 'Medium' },
|
key: 'small',
|
||||||
{ key: 'large', value: 'Large' }
|
get value() {
|
||||||
|
return translate('Small');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'medium',
|
||||||
|
get value() {
|
||||||
|
return translate('Medium');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'large',
|
||||||
|
get value() {
|
||||||
|
return translate('Large');
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
class AuthorIndexOverviewOptionsModalContent extends Component {
|
class AuthorIndexOverviewOptionsModalContent extends Component {
|
||||||
|
|||||||
@@ -14,15 +14,45 @@ import { inputTypes } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
const posterSizeOptions = [
|
const posterSizeOptions = [
|
||||||
{ key: 'small', value: 'Small' },
|
{
|
||||||
{ key: 'medium', value: 'Medium' },
|
key: 'small',
|
||||||
{ key: 'large', value: 'Large' }
|
get value() {
|
||||||
|
return translate('Small');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'medium',
|
||||||
|
get value() {
|
||||||
|
return translate('Medium');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'large',
|
||||||
|
get value() {
|
||||||
|
return translate('Large');
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const nameOptions = [
|
const nameOptions = [
|
||||||
{ key: 'no', value: translate('NoName') },
|
{
|
||||||
{ key: 'firstLast', value: translate('NameFirstLast') },
|
key: 'no',
|
||||||
{ key: 'lastFirst', value: translate('NameLastFirst') }
|
get value() {
|
||||||
|
return translate('NoName');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'firstLast',
|
||||||
|
get value() {
|
||||||
|
return translate('NameFirstLast');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lastFirst',
|
||||||
|
get value() {
|
||||||
|
return translate('NameLastFirst');
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
class AuthorIndexPosterOptionsModalContent extends Component {
|
class AuthorIndexPosterOptionsModalContent extends Component {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ function AuthorIndexProgressBar(props) {
|
|||||||
detailedProgressBar
|
detailedProgressBar
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const progress = bookCount ? bookFileCount / bookCount * 100 : 100;
|
const progress = bookCount ? bookCount / totalBookCount * 100 : 100;
|
||||||
const text = `${bookFileCount} / ${bookCount}`;
|
const text = `${bookCount} / ${totalBookCount}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ class AuthorIndexRow extends Component {
|
|||||||
progress={progress}
|
progress={progress}
|
||||||
kind={getProgressBarKind(status, monitored, progress)}
|
kind={getProgressBarKind(status, monitored, progress)}
|
||||||
showText={true}
|
showText={true}
|
||||||
text={`${bookFileCount} / ${bookCount}`}
|
text={`${bookCount} / ${totalBookCount}`}
|
||||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
||||||
width={125}
|
width={125}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,8 +7,18 @@ import { inputTypes } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
const nameOptions = [
|
const nameOptions = [
|
||||||
{ key: 'firstLast', value: translate('NameFirstLast') },
|
{
|
||||||
{ key: 'lastFirst', value: translate('NameLastFirst') }
|
key: 'firstLast',
|
||||||
|
get value() {
|
||||||
|
return translate('NameFirstLast');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lastFirst',
|
||||||
|
get value() {
|
||||||
|
return translate('NameLastFirst');
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
class AuthorIndexTableOptions extends Component {
|
class AuthorIndexTableOptions extends Component {
|
||||||
|
|||||||
@@ -6,4 +6,5 @@
|
|||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
width: 20px !important;
|
width: 20px !important;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class MonitoringOptionsModalContent extends Component {
|
|||||||
const {
|
const {
|
||||||
isSaving,
|
isSaving,
|
||||||
saveError
|
saveError
|
||||||
} = prevProps;
|
} = this.props;
|
||||||
|
|
||||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
@@ -89,9 +89,9 @@ class BookEditorFooter extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const monitoredOptions = [
|
const monitoredOptions = [
|
||||||
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||||
{ key: 'monitored', value: 'Monitored' },
|
{ key: 'monitored', value: translate('Monitored') },
|
||||||
{ key: 'unmonitored', value: 'Unmonitored' }
|
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
import BookInteractiveSearchModalContent from './BookInteractiveSearchModalContent';
|
import BookInteractiveSearchModalContent from './BookInteractiveSearchModalContent';
|
||||||
|
|
||||||
function BookInteractiveSearchModal(props) {
|
function BookInteractiveSearchModal(props) {
|
||||||
@@ -14,6 +15,7 @@ function BookInteractiveSearchModal(props) {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
closeOnBackgroundClick={false}
|
closeOnBackgroundClick={false}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class BookshelfFooter extends Component {
|
|||||||
const {
|
const {
|
||||||
isSaving,
|
isSaving,
|
||||||
saveError
|
saveError
|
||||||
} = prevProps;
|
} = this.props;
|
||||||
|
|
||||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -88,9 +88,9 @@ class BookshelfFooter extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const monitoredOptions = [
|
const monitoredOptions = [
|
||||||
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||||
{ key: 'monitored', value: 'Monitored' },
|
{ key: 'monitored', value: translate('Monitored') },
|
||||||
{ key: 'unmonitored', value: 'Unmonitored' }
|
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||||
];
|
];
|
||||||
|
|
||||||
const noChanges = monitored === NO_CHANGE &&
|
const noChanges = monitored === NO_CHANGE &&
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import React from 'react';
|
|||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import styles from './Alert.css';
|
import styles from './Alert.css';
|
||||||
|
|
||||||
function Alert({ className, kind, children, ...otherProps }) {
|
function Alert(props) {
|
||||||
|
const { className, kind, children, ...otherProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -19,8 +21,8 @@ function Alert({ className, kind, children, ...otherProps }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Alert.propTypes = {
|
Alert.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string,
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
children: PropTypes.node.isRequired
|
children: PropTypes.node.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,9 @@
|
|||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
font-size: 21px;
|
font-size: 21px;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
color: #909293;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
frontend/src/Components/FieldSet.css.d.ts
vendored
1
frontend/src/Components/FieldSet.css.d.ts
vendored
@@ -3,6 +3,7 @@
|
|||||||
interface CssExports {
|
interface CssExports {
|
||||||
'fieldSet': string;
|
'fieldSet': string;
|
||||||
'legend': string;
|
'legend': string;
|
||||||
|
'small': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
import styles from './FieldSet.css';
|
import styles from './FieldSet.css';
|
||||||
|
|
||||||
class FieldSet extends Component {
|
class FieldSet extends Component {
|
||||||
@@ -9,13 +11,14 @@ class FieldSet extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
size,
|
||||||
legend,
|
legend,
|
||||||
children
|
children
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset className={styles.fieldSet}>
|
<fieldset className={styles.fieldSet}>
|
||||||
<legend className={styles.legend}>
|
<legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
|
||||||
{legend}
|
{legend}
|
||||||
</legend>
|
</legend>
|
||||||
{children}
|
{children}
|
||||||
@@ -26,8 +29,13 @@ class FieldSet extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FieldSet.propTypes = {
|
FieldSet.propTypes = {
|
||||||
|
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||||
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||||
children: PropTypes.node
|
children: PropTypes.node
|
||||||
};
|
};
|
||||||
|
|
||||||
|
FieldSet.defaultProps = {
|
||||||
|
size: sizes.MEDIUM
|
||||||
|
};
|
||||||
|
|
||||||
export default FieldSet;
|
export default FieldSet;
|
||||||
|
|||||||
@@ -206,11 +206,13 @@ class FilterBuilderRow extends Component {
|
|||||||
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
|
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
|
||||||
|
|
||||||
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
|
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
|
||||||
|
const { name, label } = availablePropFilter;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: availablePropFilter.name,
|
key: name,
|
||||||
value: availablePropFilter.label
|
value: typeof label === 'function' ? label() : label
|
||||||
};
|
};
|
||||||
});
|
}).sort((a, b) => a.value.localeCompare(b.value));
|
||||||
|
|
||||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
.tag {
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
&.isLastTag {
|
&.isLastTag {
|
||||||
.or {
|
.or {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import styles from './FilterBuilderRowValueTag.css';
|
|||||||
|
|
||||||
function FilterBuilderRowValueTag(props) {
|
function FilterBuilderRowValueTag(props) {
|
||||||
return (
|
return (
|
||||||
<span
|
<div
|
||||||
className={styles.tag}
|
className={styles.tag}
|
||||||
>
|
>
|
||||||
<TagInputTag
|
<TagInputTag
|
||||||
@@ -15,12 +15,13 @@ function FilterBuilderRowValueTag(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!props.isLastTag &&
|
props.isLastTag ?
|
||||||
<span className={styles.or}>
|
null :
|
||||||
|
<div className={styles.or}>
|
||||||
or
|
or
|
||||||
</span>
|
</div>
|
||||||
}
|
}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
|
max-height: 100%;
|
||||||
width: 350px !important;
|
width: 350px !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
// import translate from 'Utilities/String/translate';
|
// import translate from 'Utilities/String/translate';
|
||||||
import AutoCompleteInput from './AutoCompleteInput';
|
import AutoCompleteInput from './AutoCompleteInput';
|
||||||
@@ -26,6 +26,7 @@ import PathInputConnector from './PathInputConnector';
|
|||||||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
||||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||||
import TagInputConnector from './TagInputConnector';
|
import TagInputConnector from './TagInputConnector';
|
||||||
|
import TagSelectInputConnector from './TagSelectInputConnector';
|
||||||
import TextArea from './TextArea';
|
import TextArea from './TextArea';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import TextTagInputConnector from './TextTagInputConnector';
|
import TextTagInputConnector from './TextTagInputConnector';
|
||||||
@@ -103,6 +104,9 @@ function getComponent(type) {
|
|||||||
case inputTypes.TEXT_TAG:
|
case inputTypes.TEXT_TAG:
|
||||||
return TextTagInputConnector;
|
return TextTagInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.TAG_SELECT:
|
||||||
|
return TagSelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.UMASK:
|
case inputTypes.UMASK:
|
||||||
return UMaskInput;
|
return UMaskInput;
|
||||||
|
|
||||||
@@ -266,16 +270,27 @@ FormInputGroup.propTypes = {
|
|||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
containerClassName: PropTypes.string.isRequired,
|
containerClassName: PropTypes.string.isRequired,
|
||||||
inputClassName: PropTypes.string,
|
inputClassName: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.any,
|
||||||
|
values: PropTypes.arrayOf(PropTypes.any),
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
|
min: PropTypes.number,
|
||||||
|
max: PropTypes.number,
|
||||||
unit: PropTypes.string,
|
unit: PropTypes.string,
|
||||||
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
helpTexts: PropTypes.arrayOf(PropTypes.string),
|
helpTexts: PropTypes.arrayOf(PropTypes.string),
|
||||||
helpTextWarning: PropTypes.string,
|
helpTextWarning: PropTypes.string,
|
||||||
helpLink: PropTypes.string,
|
helpLink: PropTypes.string,
|
||||||
|
autoFocus: PropTypes.bool,
|
||||||
|
includeNoChange: PropTypes.bool,
|
||||||
|
includeNoChangeDisabled: PropTypes.bool,
|
||||||
|
selectedValueOptions: PropTypes.object,
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object)
|
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
FormInputGroup.defaultProps = {
|
FormInputGroup.defaultProps = {
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ import React from 'react';
|
|||||||
import { sizes } from 'Helpers/Props';
|
import { sizes } from 'Helpers/Props';
|
||||||
import styles from './FormLabel.css';
|
import styles from './FormLabel.css';
|
||||||
|
|
||||||
function FormLabel({
|
function FormLabel(props) {
|
||||||
children,
|
const {
|
||||||
className,
|
children,
|
||||||
errorClassName,
|
className,
|
||||||
size,
|
errorClassName,
|
||||||
name,
|
size,
|
||||||
hasError,
|
name,
|
||||||
isAdvanced,
|
hasError,
|
||||||
...otherProps
|
isAdvanced,
|
||||||
}) {
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
@@ -31,13 +33,13 @@ function FormLabel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
FormLabel.propTypes = {
|
FormLabel.propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
errorClassName: PropTypes.string,
|
errorClassName: PropTypes.string,
|
||||||
size: PropTypes.oneOf(sizes.all),
|
size: PropTypes.oneOf(sizes.all),
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
hasError: PropTypes.bool,
|
hasError: PropTypes.bool,
|
||||||
isAdvanced: PropTypes.bool.isRequired
|
isAdvanced: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
FormLabel.defaultProps = {
|
FormLabel.defaultProps = {
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ import { createSelector } from 'reselect';
|
|||||||
import { metadataProfileNames } from 'Helpers/Props';
|
import { metadataProfileNames } from 'Helpers/Props';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.metadataProfiles', sortByName),
|
createSortedSectionSelector('settings.metadataProfiles', sortByName),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
|
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
(state, { includeNone }) => includeNone,
|
(state, { includeNone }) => includeNone,
|
||||||
(metadataProfiles, includeNoChange, includeMixed, includeNone) => {
|
(metadataProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed, includeNone) => {
|
||||||
|
|
||||||
const profiles = metadataProfiles.items.filter((item) => item.name !== metadataProfileNames.NONE);
|
const profiles = metadataProfiles.items.filter((item) => item.name !== metadataProfileNames.NONE);
|
||||||
const noneProfile = metadataProfiles.items.find((item) => item.name === metadataProfileNames.NONE);
|
const noneProfile = metadataProfiles.items.find((item) => item.name === metadataProfileNames.NONE);
|
||||||
@@ -36,8 +38,8 @@ function createMapStateToProps() {
|
|||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: 'No Change',
|
value: translate('NoChange'),
|
||||||
disabled: true
|
disabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +70,8 @@ class MetadataProfileSelectInputConnector extends Component {
|
|||||||
values
|
values
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!value || !_.some(values, (option) => parseInt(option.key) === value)) {
|
if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
|
||||||
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key)));
|
const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
|
||||||
|
|
||||||
if (firstValue) {
|
if (firstValue) {
|
||||||
this.onChange({ name, value: firstValue.key });
|
this.onChange({ name, value: firstValue.key });
|
||||||
@@ -81,7 +83,7 @@ class MetadataProfileSelectInputConnector extends Component {
|
|||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
onChange = ({ name, value }) => {
|
||||||
this.props.onChange({ name, value: parseInt(value) });
|
this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) });
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -107,7 +109,8 @@ MetadataProfileSelectInputConnector.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
MetadataProfileSelectInputConnector.defaultProps = {
|
MetadataProfileSelectInputConnector.defaultProps = {
|
||||||
includeNoChange: false
|
includeNoChange: false,
|
||||||
|
includeNone: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MetadataProfileSelectInputConnector);
|
export default connect(createMapStateToProps)(MetadataProfileSelectInputConnector);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import monitorOptions from 'Utilities/Author/monitorOptions';
|
import monitorOptions from 'Utilities/Author/monitorOptions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
function MonitorBooksSelectInput(props) {
|
function MonitorBooksSelectInput(props) {
|
||||||
@@ -16,7 +17,7 @@ function MonitorBooksSelectInput(props) {
|
|||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: 'No Change',
|
value: translate('NoChange'),
|
||||||
disabled: true
|
disabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import monitorNewItemsOptions from 'Utilities/Author/monitorNewItemsOptions';
|
import monitorNewItemsOptions from 'Utilities/Author/monitorNewItemsOptions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
function MonitorNewItemsSelectInput(props) {
|
function MonitorNewItemsSelectInput(props) {
|
||||||
@@ -15,7 +16,7 @@ function MonitorNewItemsSelectInput(props) {
|
|||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: 'No Change',
|
value: translate('NoChange'),
|
||||||
disabled: true
|
disabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function parseValue(props, value) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (value == null || value === '') {
|
if (value == null || value === '') {
|
||||||
return min;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newValue = isFloat ? parseFloat(value) : parseInt(value);
|
let newValue = isFloat ? parseFloat(value) : parseInt(value);
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||||||
return inputTypes.SELECT;
|
return inputTypes.SELECT;
|
||||||
case 'tag':
|
case 'tag':
|
||||||
return inputTypes.TEXT_TAG;
|
return inputTypes.TEXT_TAG;
|
||||||
|
case 'tagSelect':
|
||||||
|
return inputTypes.TAG_SELECT;
|
||||||
case 'textbox':
|
case 'textbox':
|
||||||
return inputTypes.TEXT;
|
return inputTypes.TEXT;
|
||||||
case 'oAuth':
|
case 'oAuth':
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
|
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
(qualityProfiles, includeNoChange, includeMixed) => {
|
(qualityProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed) => {
|
||||||
const values = _.map(qualityProfiles.items, (qualityProfile) => {
|
const values = _.map(qualityProfiles.items, (qualityProfile) => {
|
||||||
return {
|
return {
|
||||||
key: qualityProfile.id,
|
key: qualityProfile.id,
|
||||||
@@ -23,8 +25,8 @@ function createMapStateToProps() {
|
|||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: 'No Change',
|
value: translate('NoChange'),
|
||||||
disabled: true
|
disabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +57,8 @@ class QualityProfileSelectInputConnector extends Component {
|
|||||||
values
|
values
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!value || !_.some(values, (option) => parseInt(option.key) === value)) {
|
if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
|
||||||
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key)));
|
const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
|
||||||
|
|
||||||
if (firstValue) {
|
if (firstValue) {
|
||||||
this.onChange({ name, value: firstValue.key });
|
this.onChange({ name, value: firstValue.key });
|
||||||
@@ -68,7 +70,7 @@ class QualityProfileSelectInputConnector extends Component {
|
|||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
onChange = ({ name, value }) => {
|
||||||
this.props.onChange({ name, value: parseInt(value) });
|
this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) });
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class RootFolderSelectInput extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
value,
|
includeNoChange,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -71,7 +71,6 @@ class RootFolderSelectInput extends Component {
|
|||||||
<div>
|
<div>
|
||||||
<EnhancedSelectInput
|
<EnhancedSelectInput
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
value={value || ''}
|
|
||||||
selectedValueComponent={RootFolderSelectInputSelectedValue}
|
selectedValueComponent={RootFolderSelectInputSelectedValue}
|
||||||
optionComponent={RootFolderSelectInputOption}
|
optionComponent={RootFolderSelectInputOption}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
@@ -93,7 +92,12 @@ RootFolderSelectInput.propTypes = {
|
|||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
|
includeNoChange: PropTypes.bool.isRequired,
|
||||||
onChange: PropTypes.func.isRequired
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RootFolderSelectInput.defaultProps = {
|
||||||
|
includeNoChange: false
|
||||||
|
};
|
||||||
|
|
||||||
export default RootFolderSelectInput;
|
export default RootFolderSelectInput;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import RootFolderSelectInput from './RootFolderSelectInput';
|
import RootFolderSelectInput from './RootFolderSelectInput';
|
||||||
|
|
||||||
const ADD_NEW_KEY = 'addNew';
|
const ADD_NEW_KEY = 'addNew';
|
||||||
@@ -12,7 +13,8 @@ function createMapStateToProps() {
|
|||||||
(state, { value }) => value,
|
(state, { value }) => value,
|
||||||
(state, { includeMissingValue }) => includeMissingValue,
|
(state, { includeMissingValue }) => includeMissingValue,
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(rootFolders, value, includeMissingValue, includeNoChange) => {
|
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||||
|
(rootFolders, value, includeMissingValue, includeNoChange, includeNoChangeDisabled = true) => {
|
||||||
const values = rootFolders.items.map((rootFolder) => {
|
const values = rootFolders.items.map((rootFolder) => {
|
||||||
return {
|
return {
|
||||||
key: rootFolder.path,
|
key: rootFolder.path,
|
||||||
@@ -27,8 +29,8 @@ function createMapStateToProps() {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: '',
|
value: '',
|
||||||
name: 'No Change',
|
name: translate('NoChange'),
|
||||||
isDisabled: true,
|
isDisabled: includeNoChangeDisabled,
|
||||||
isMissing: false
|
isMissing: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class SelectInput extends Component {
|
|||||||
value={key}
|
value={key}
|
||||||
{...otherOptionProps}
|
{...otherOptionProps}
|
||||||
>
|
>
|
||||||
{optionValue}
|
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
disabledClassName: PropTypes.string,
|
disabledClassName: PropTypes.string,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired,
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
hasError: PropTypes.bool,
|
hasError: PropTypes.bool,
|
||||||
|
|||||||
@@ -75,6 +75,18 @@ class TagInput extends Component {
|
|||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
onTagEdit = ({ value, ...otherProps }) => {
|
||||||
|
const currentValue = this.state.value;
|
||||||
|
|
||||||
|
if (currentValue && this.props.onTagReplace) {
|
||||||
|
this.props.onTagReplace(otherProps, { name: currentValue });
|
||||||
|
} else {
|
||||||
|
this.props.onTagDelete(otherProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ value });
|
||||||
|
};
|
||||||
|
|
||||||
onInputContainerPress = () => {
|
onInputContainerPress = () => {
|
||||||
this._autosuggestRef.input.focus();
|
this._autosuggestRef.input.focus();
|
||||||
};
|
};
|
||||||
@@ -188,6 +200,7 @@ class TagInput extends Component {
|
|||||||
const {
|
const {
|
||||||
tags,
|
tags,
|
||||||
kind,
|
kind,
|
||||||
|
canEdit,
|
||||||
tagComponent,
|
tagComponent,
|
||||||
onTagDelete
|
onTagDelete
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -199,8 +212,10 @@ class TagInput extends Component {
|
|||||||
kind={kind}
|
kind={kind}
|
||||||
inputProps={inputProps}
|
inputProps={inputProps}
|
||||||
isFocused={this.state.isFocused}
|
isFocused={this.state.isFocused}
|
||||||
|
canEdit={canEdit}
|
||||||
tagComponent={tagComponent}
|
tagComponent={tagComponent}
|
||||||
onTagDelete={onTagDelete}
|
onTagDelete={onTagDelete}
|
||||||
|
onTagEdit={this.onTagEdit}
|
||||||
onInputContainerPress={this.onInputContainerPress}
|
onInputContainerPress={this.onInputContainerPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -225,7 +240,7 @@ class TagInput extends Component {
|
|||||||
<AutoSuggestInput
|
<AutoSuggestInput
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
forwardedRef={this._setAutosuggestRef}
|
forwardedRef={this._setAutosuggestRef}
|
||||||
className={styles.internalInput}
|
className={className}
|
||||||
inputContainerClassName={classNames(
|
inputContainerClassName={classNames(
|
||||||
inputContainerClassName,
|
inputContainerClassName,
|
||||||
isFocused && styles.isFocused,
|
isFocused && styles.isFocused,
|
||||||
@@ -262,11 +277,13 @@ TagInput.propTypes = {
|
|||||||
placeholder: PropTypes.string.isRequired,
|
placeholder: PropTypes.string.isRequired,
|
||||||
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
minQueryLength: PropTypes.number.isRequired,
|
minQueryLength: PropTypes.number.isRequired,
|
||||||
|
canEdit: PropTypes.bool,
|
||||||
hasError: PropTypes.bool,
|
hasError: PropTypes.bool,
|
||||||
hasWarning: PropTypes.bool,
|
hasWarning: PropTypes.bool,
|
||||||
tagComponent: PropTypes.elementType.isRequired,
|
tagComponent: PropTypes.elementType.isRequired,
|
||||||
onTagAdd: PropTypes.func.isRequired,
|
onTagAdd: PropTypes.func.isRequired,
|
||||||
onTagDelete: PropTypes.func.isRequired
|
onTagDelete: PropTypes.func.isRequired,
|
||||||
|
onTagReplace: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
TagInput.defaultProps = {
|
TagInput.defaultProps = {
|
||||||
@@ -277,6 +294,7 @@ TagInput.defaultProps = {
|
|||||||
placeholder: '',
|
placeholder: '',
|
||||||
delimiters: ['Tab', 'Enter', ' ', ','],
|
delimiters: ['Tab', 'Enter', ' ', ','],
|
||||||
minQueryLength: 1,
|
minQueryLength: 1,
|
||||||
|
canEdit: false,
|
||||||
tagComponent: TagInputTag
|
tagComponent: TagInputTag
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ class TagInputConnector extends Component {
|
|||||||
<TagInput
|
<TagInput
|
||||||
onTagAdd={this.onTagAdd}
|
onTagAdd={this.onTagAdd}
|
||||||
onTagDelete={this.onTagDelete}
|
onTagDelete={this.onTagDelete}
|
||||||
|
onTagReplace={this.onTagReplace}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ class TagInputInput extends Component {
|
|||||||
tags,
|
tags,
|
||||||
inputProps,
|
inputProps,
|
||||||
kind,
|
kind,
|
||||||
|
canEdit,
|
||||||
tagComponent: TagComponent,
|
tagComponent: TagComponent,
|
||||||
onTagDelete
|
onTagDelete,
|
||||||
|
onTagEdit
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,8 +48,10 @@ class TagInputInput extends Component {
|
|||||||
index={index}
|
index={index}
|
||||||
tag={tag}
|
tag={tag}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
|
canEdit={canEdit}
|
||||||
isLastTag={index === tags.length - 1}
|
isLastTag={index === tags.length - 1}
|
||||||
onDelete={onTagDelete}
|
onDelete={onTagDelete}
|
||||||
|
onEdit={onTagEdit}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -66,8 +70,10 @@ TagInputInput.propTypes = {
|
|||||||
inputProps: PropTypes.object.isRequired,
|
inputProps: PropTypes.object.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
isFocused: PropTypes.bool.isRequired,
|
isFocused: PropTypes.bool.isRequired,
|
||||||
|
canEdit: PropTypes.bool.isRequired,
|
||||||
tagComponent: PropTypes.elementType.isRequired,
|
tagComponent: PropTypes.elementType.isRequired,
|
||||||
onTagDelete: PropTypes.func.isRequired,
|
onTagDelete: PropTypes.func.isRequired,
|
||||||
|
onTagEdit: PropTypes.func.isRequired,
|
||||||
onInputContainerPress: PropTypes.func.isRequired
|
onInputContainerPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,34 @@
|
|||||||
.tag {
|
.tag {
|
||||||
composes: link from '~Components/Link/Link.css';
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 100%;
|
||||||
height: 31px;
|
height: 31px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkWithEdit {
|
||||||
|
max-width: calc(100% - 9px - 4px - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editContainer {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 4px;
|
||||||
|
padding-left: 2px;
|
||||||
|
border-left: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editButton {
|
||||||
|
composes: button from '~Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
width: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'editButton': string;
|
||||||
|
'editContainer': string;
|
||||||
|
'label': string;
|
||||||
|
'link': string;
|
||||||
|
'linkWithEdit': string;
|
||||||
'tag': string;
|
'tag': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||||
import styles from './TagInputTag.css';
|
import styles from './TagInputTag.css';
|
||||||
|
|
||||||
@@ -24,24 +25,61 @@ class TagInputTag extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onEdit = () => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
tag,
|
||||||
|
onEdit
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onEdit({
|
||||||
|
index,
|
||||||
|
id: tag.id,
|
||||||
|
value: tag.name
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
tag,
|
tag,
|
||||||
kind
|
kind,
|
||||||
|
canEdit
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<div
|
||||||
className={styles.tag}
|
className={styles.tag}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onPress={this.onDelete}
|
|
||||||
>
|
>
|
||||||
<Label kind={kind}>
|
<Label
|
||||||
{tag.name}
|
className={styles.label}
|
||||||
|
kind={kind}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className={canEdit ? styles.linkWithEdit : styles.link}
|
||||||
|
tabIndex={-1}
|
||||||
|
onPress={this.onDelete}
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{
|
||||||
|
canEdit ?
|
||||||
|
<div className={styles.editContainer}>
|
||||||
|
<IconButton
|
||||||
|
className={styles.editButton}
|
||||||
|
name={icons.EDIT}
|
||||||
|
size={9}
|
||||||
|
onPress={this.onEdit}
|
||||||
|
/>
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Label>
|
</Label>
|
||||||
</Link>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +88,9 @@ TagInputTag.propTypes = {
|
|||||||
index: PropTypes.number.isRequired,
|
index: PropTypes.number.isRequired,
|
||||||
tag: PropTypes.shape(tagShape),
|
tag: PropTypes.shape(tagShape),
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
onDelete: PropTypes.func.isRequired
|
canEdit: PropTypes.bool.isRequired,
|
||||||
|
onDelete: PropTypes.func.isRequired,
|
||||||
|
onEdit: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagInputTag;
|
export default TagInputTag;
|
||||||
|
|||||||
102
frontend/src/Components/Form/TagSelectInputConnector.js
Normal file
102
frontend/src/Components/Form/TagSelectInputConnector.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import TagInput from './TagInput';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { value }) => value,
|
||||||
|
(state, { values }) => values,
|
||||||
|
(tags, tagList) => {
|
||||||
|
const sortedTags = _.sortBy(tagList, 'value');
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags: tags.reduce((acc, tag) => {
|
||||||
|
const matchingTag = _.find(tagList, { key: tag });
|
||||||
|
|
||||||
|
if (matchingTag) {
|
||||||
|
acc.push({
|
||||||
|
id: tag,
|
||||||
|
name: matchingTag.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []),
|
||||||
|
|
||||||
|
tagList: sortedTags.map(({ key: id, value: name }) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
allTags: sortedTags
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TagSelectInputConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onTagAdd = (tag) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
allTags
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const existingTag =_.some(allTags, { key: tag.id });
|
||||||
|
|
||||||
|
const newValue = value.slice();
|
||||||
|
|
||||||
|
if (existingTag) {
|
||||||
|
newValue.push(tag.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onChange({ name, value: newValue });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTagDelete = ({ index }) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = value.slice();
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
|
||||||
|
this.props.onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<TagInput
|
||||||
|
onTagAdd={this.onTagAdd}
|
||||||
|
onTagDelete={this.onTagDelete}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagSelectInputConnector.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
allTags: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(TagSelectInputConnector);
|
||||||
@@ -71,6 +71,20 @@ class TextTagInputConnector extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onTagReplace = (tagToReplace, newTag) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
valueArray,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = [...valueArray];
|
||||||
|
newValue.splice(tagToReplace.index, 1);
|
||||||
|
newValue.push(newTag.name.trim());
|
||||||
|
|
||||||
|
onChange({ name, value: newValue });
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -80,6 +94,7 @@ class TextTagInputConnector extends Component {
|
|||||||
tagList={[]}
|
tagList={[]}
|
||||||
onTagAdd={this.onTagAdd}
|
onTagAdd={this.onTagAdd}
|
||||||
onTagDelete={this.onTagDelete}
|
onTagDelete={this.onTagDelete}
|
||||||
|
onTagReplace={this.onTagReplace}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Icon extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={containerClassName}
|
className={containerClassName}
|
||||||
title={title}
|
title={typeof title === 'function' ? title() : title}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
@@ -58,7 +58,7 @@ Icon.propTypes = {
|
|||||||
name: PropTypes.object.isRequired,
|
name: PropTypes.object.isRequired,
|
||||||
kind: PropTypes.string.isRequired,
|
kind: PropTypes.string.isRequired,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
title: PropTypes.string,
|
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||||
isSpinning: PropTypes.bool.isRequired,
|
isSpinning: PropTypes.bool.isRequired,
|
||||||
fixedWidth: PropTypes.bool.isRequired
|
fixedWidth: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ function Label(props) {
|
|||||||
|
|
||||||
Label.propTypes = {
|
Label.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
|
title: PropTypes.string,
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||||
outline: PropTypes.bool.isRequired,
|
outline: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -39,11 +39,13 @@ function IconButton(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconButton.propTypes = {
|
IconButton.propTypes = {
|
||||||
|
...Link.propTypes,
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
iconClassName: PropTypes.string,
|
iconClassName: PropTypes.string,
|
||||||
kind: PropTypes.string,
|
kind: PropTypes.string,
|
||||||
name: PropTypes.object.isRequired,
|
name: PropTypes.object.isRequired,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
|
title: PropTypes.string,
|
||||||
isSpinning: PropTypes.bool,
|
isSpinning: PropTypes.bool,
|
||||||
isDisabled: PropTypes.bool
|
isDisabled: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
|
||||||
import styles from './Link.css';
|
|
||||||
|
|
||||||
class Link extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onClick = (event) => {
|
|
||||||
const {
|
|
||||||
isDisabled,
|
|
||||||
onPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!isDisabled && onPress) {
|
|
||||||
onPress(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
component,
|
|
||||||
to,
|
|
||||||
target,
|
|
||||||
isDisabled,
|
|
||||||
noRouter,
|
|
||||||
onPress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const linkProps = { target };
|
|
||||||
let el = component;
|
|
||||||
|
|
||||||
if (to && typeof to === 'string') {
|
|
||||||
if ((/\w+?:\/\//).test(to)) {
|
|
||||||
el = 'a';
|
|
||||||
linkProps.href = to;
|
|
||||||
linkProps.target = target || '_blank';
|
|
||||||
linkProps.rel = 'noreferrer';
|
|
||||||
} else if (noRouter) {
|
|
||||||
el = 'a';
|
|
||||||
linkProps.href = to;
|
|
||||||
linkProps.target = target || '_self';
|
|
||||||
} else {
|
|
||||||
el = RouterLink;
|
|
||||||
linkProps.to = `${window.Readarr.urlBase}/${to.replace(/^\//, '')}`;
|
|
||||||
linkProps.target = target;
|
|
||||||
}
|
|
||||||
} else if (to && typeof to === 'object') {
|
|
||||||
el = RouterLink;
|
|
||||||
linkProps.target = target;
|
|
||||||
if (to.pathname.startsWith(`${window.Readarr.urlBase}/`)) {
|
|
||||||
linkProps.to = to;
|
|
||||||
} else {
|
|
||||||
const pathname = `${window.Readarr.urlBase}/${to.pathname.replace(/^\//, '')}`;
|
|
||||||
linkProps.to = {
|
|
||||||
...to,
|
|
||||||
pathname
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el === 'button' || el === 'input') {
|
|
||||||
linkProps.type = otherProps.type || 'button';
|
|
||||||
linkProps.disabled = isDisabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
linkProps.className = classNames(
|
|
||||||
className,
|
|
||||||
styles.link,
|
|
||||||
to && styles.to,
|
|
||||||
isDisabled && 'isDisabled'
|
|
||||||
);
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
...otherProps,
|
|
||||||
...linkProps
|
|
||||||
};
|
|
||||||
|
|
||||||
props.onClick = this.onClick;
|
|
||||||
|
|
||||||
return (
|
|
||||||
React.createElement(el, props)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Link.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
|
||||||
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
|
||||||
target: PropTypes.string,
|
|
||||||
isDisabled: PropTypes.bool,
|
|
||||||
noRouter: PropTypes.bool,
|
|
||||||
onPress: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
Link.defaultProps = {
|
|
||||||
component: 'button',
|
|
||||||
noRouter: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Link;
|
|
||||||
96
frontend/src/Components/Link/Link.tsx
Normal file
96
frontend/src/Components/Link/Link.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {
|
||||||
|
ComponentClass,
|
||||||
|
FunctionComponent,
|
||||||
|
SyntheticEvent,
|
||||||
|
useCallback,
|
||||||
|
} from 'react';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import styles from './Link.css';
|
||||||
|
|
||||||
|
interface ReactRouterLinkProps {
|
||||||
|
to?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
|
||||||
|
className?: string;
|
||||||
|
component?:
|
||||||
|
| string
|
||||||
|
| FunctionComponent<LinkProps>
|
||||||
|
| ComponentClass<LinkProps, unknown>;
|
||||||
|
to?: string;
|
||||||
|
target?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
noRouter?: boolean;
|
||||||
|
onPress?(event: SyntheticEvent): void;
|
||||||
|
}
|
||||||
|
function Link(props: LinkProps) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
component = 'button',
|
||||||
|
to,
|
||||||
|
target,
|
||||||
|
type,
|
||||||
|
isDisabled,
|
||||||
|
noRouter = false,
|
||||||
|
onPress,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const onClick = useCallback(
|
||||||
|
(event: SyntheticEvent) => {
|
||||||
|
if (!isDisabled && onPress) {
|
||||||
|
onPress(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isDisabled, onPress]
|
||||||
|
);
|
||||||
|
|
||||||
|
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
|
||||||
|
target,
|
||||||
|
};
|
||||||
|
let el = component;
|
||||||
|
|
||||||
|
if (to) {
|
||||||
|
if (/\w+?:\/\//.test(to)) {
|
||||||
|
el = 'a';
|
||||||
|
linkProps.href = to;
|
||||||
|
linkProps.target = target || '_blank';
|
||||||
|
linkProps.rel = 'noreferrer';
|
||||||
|
} else if (noRouter) {
|
||||||
|
el = 'a';
|
||||||
|
linkProps.href = to;
|
||||||
|
linkProps.target = target || '_self';
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
el = RouterLink;
|
||||||
|
linkProps.to = `${window.Readarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||||
|
linkProps.target = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el === 'button' || el === 'input') {
|
||||||
|
linkProps.type = type || 'button';
|
||||||
|
linkProps.disabled = isDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
linkProps.className = classNames(
|
||||||
|
className,
|
||||||
|
styles.link,
|
||||||
|
to && styles.to,
|
||||||
|
isDisabled && 'isDisabled'
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementProps = {
|
||||||
|
...otherProps,
|
||||||
|
type,
|
||||||
|
...linkProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
elementProps.onClick = onClick;
|
||||||
|
|
||||||
|
return React.createElement(el, elementProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Link;
|
||||||
@@ -42,6 +42,7 @@ function SpinnerButton(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SpinnerButton.propTypes = {
|
SpinnerButton.propTypes = {
|
||||||
|
...Button.Props,
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
isSpinning: PropTypes.bool.isRequired,
|
isSpinning: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
|
|||||||
@@ -13,24 +13,51 @@ class InlineMarkdown extends Component {
|
|||||||
data
|
data
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// For now only replace links
|
// For now only replace links or code blocks (not both)
|
||||||
const markdownBlocks = [];
|
const markdownBlocks = [];
|
||||||
if (data) {
|
if (data) {
|
||||||
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
|
const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
|
||||||
|
|
||||||
let endIndex = 0;
|
let endIndex = 0;
|
||||||
let match = null;
|
let match = null;
|
||||||
while ((match = regex.exec(data)) !== null) {
|
|
||||||
|
while ((match = linkRegex.exec(data)) !== null) {
|
||||||
if (match.index > endIndex) {
|
if (match.index > endIndex) {
|
||||||
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
|
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
|
||||||
endIndex = match.index + match[0].length;
|
endIndex = match.index + match[0].length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endIndex !== data.length) {
|
if (endIndex !== data.length && markdownBlocks.length > 0) {
|
||||||
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
|
||||||
|
|
||||||
|
endIndex = 0;
|
||||||
|
match = null;
|
||||||
|
let matchedCode = false;
|
||||||
|
|
||||||
|
while ((match = codeRegex.exec(data)) !== null) {
|
||||||
|
matchedCode = true;
|
||||||
|
|
||||||
|
if (match.index > endIndex) {
|
||||||
|
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
markdownBlocks.push(<code key={`code-${match.index}`}>{match[0].substring(1, match[0].length - 1)}</code>);
|
||||||
|
endIndex = match.index + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
|
||||||
|
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markdownBlocks.length === 0) {
|
||||||
|
markdownBlocks.push(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span className={className}>{markdownBlocks}</span>;
|
return <span className={className}>{markdownBlocks}</span>;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class FilterMenuContent extends Component {
|
|||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
onPress={onFilterSelect}
|
onPress={onFilterSelect}
|
||||||
>
|
>
|
||||||
{filter.label}
|
{typeof filter.label === 'function' ? filter.label() : filter.label}
|
||||||
</FilterMenuItem>
|
</FilterMenuItem>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ function ErrorPage(props) {
|
|||||||
const {
|
const {
|
||||||
version,
|
version,
|
||||||
isLocalStorageSupported,
|
isLocalStorageSupported,
|
||||||
|
translationsError,
|
||||||
authorError,
|
authorError,
|
||||||
customFiltersError,
|
customFiltersError,
|
||||||
tagsError,
|
tagsError,
|
||||||
@@ -20,6 +21,8 @@ function ErrorPage(props) {
|
|||||||
|
|
||||||
if (!isLocalStorageSupported) {
|
if (!isLocalStorageSupported) {
|
||||||
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
|
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
|
||||||
|
} else if (translationsError) {
|
||||||
|
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
|
||||||
} else if (authorError) {
|
} else if (authorError) {
|
||||||
errorMessage = getErrorMessage(authorError, 'Failed to load author from API');
|
errorMessage = getErrorMessage(authorError, 'Failed to load author from API');
|
||||||
} else if (customFiltersError) {
|
} else if (customFiltersError) {
|
||||||
@@ -52,6 +55,7 @@ function ErrorPage(props) {
|
|||||||
ErrorPage.propTypes = {
|
ErrorPage.propTypes = {
|
||||||
version: PropTypes.string.isRequired,
|
version: PropTypes.string.isRequired,
|
||||||
isLocalStorageSupported: PropTypes.bool.isRequired,
|
isLocalStorageSupported: PropTypes.bool.isRequired,
|
||||||
|
translationsError: PropTypes.object,
|
||||||
authorError: PropTypes.object,
|
authorError: PropTypes.object,
|
||||||
customFiltersError: PropTypes.object,
|
customFiltersError: PropTypes.object,
|
||||||
tagsError: PropTypes.object,
|
tagsError: PropTypes.object,
|
||||||
|
|||||||
@@ -53,10 +53,7 @@ class PageHeader extends Component {
|
|||||||
<div className={styles.logoContainer}>
|
<div className={styles.logoContainer}>
|
||||||
<Link
|
<Link
|
||||||
className={styles.logoLink}
|
className={styles.logoLink}
|
||||||
to={{
|
to={'/'}
|
||||||
pathname: '/',
|
|
||||||
state: { restoreScrollPosition: true }
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||||
import { fetchAuthor } from 'Store/Actions/authorActions';
|
import { fetchAuthor } from 'Store/Actions/authorActions';
|
||||||
import { fetchBooks } from 'Store/Actions/bookActions';
|
import { fetchBooks } from 'Store/Actions/bookActions';
|
||||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||||
@@ -52,6 +52,7 @@ const selectIsPopulated = createSelector(
|
|||||||
(state) => state.settings.metadataProfiles.isPopulated,
|
(state) => state.settings.metadataProfiles.isPopulated,
|
||||||
(state) => state.settings.importLists.isPopulated,
|
(state) => state.settings.importLists.isPopulated,
|
||||||
(state) => state.system.status.isPopulated,
|
(state) => state.system.status.isPopulated,
|
||||||
|
(state) => state.app.translations.isPopulated,
|
||||||
(
|
(
|
||||||
customFiltersIsPopulated,
|
customFiltersIsPopulated,
|
||||||
tagsIsPopulated,
|
tagsIsPopulated,
|
||||||
@@ -60,7 +61,8 @@ const selectIsPopulated = createSelector(
|
|||||||
qualityProfilesIsPopulated,
|
qualityProfilesIsPopulated,
|
||||||
metadataProfilesIsPopulated,
|
metadataProfilesIsPopulated,
|
||||||
importListsIsPopulated,
|
importListsIsPopulated,
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated,
|
||||||
|
translationsIsPopulated
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
customFiltersIsPopulated &&
|
customFiltersIsPopulated &&
|
||||||
@@ -70,7 +72,8 @@ const selectIsPopulated = createSelector(
|
|||||||
qualityProfilesIsPopulated &&
|
qualityProfilesIsPopulated &&
|
||||||
metadataProfilesIsPopulated &&
|
metadataProfilesIsPopulated &&
|
||||||
importListsIsPopulated &&
|
importListsIsPopulated &&
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated &&
|
||||||
|
translationsIsPopulated
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -84,6 +87,7 @@ const selectErrors = createSelector(
|
|||||||
(state) => state.settings.metadataProfiles.error,
|
(state) => state.settings.metadataProfiles.error,
|
||||||
(state) => state.settings.importLists.error,
|
(state) => state.settings.importLists.error,
|
||||||
(state) => state.system.status.error,
|
(state) => state.system.status.error,
|
||||||
|
(state) => state.app.translations.error,
|
||||||
(
|
(
|
||||||
customFiltersError,
|
customFiltersError,
|
||||||
tagsError,
|
tagsError,
|
||||||
@@ -92,7 +96,8 @@ const selectErrors = createSelector(
|
|||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
metadataProfilesError,
|
metadataProfilesError,
|
||||||
importListsError,
|
importListsError,
|
||||||
systemStatusError
|
systemStatusError,
|
||||||
|
translationsError
|
||||||
) => {
|
) => {
|
||||||
const hasError = !!(
|
const hasError = !!(
|
||||||
customFiltersError ||
|
customFiltersError ||
|
||||||
@@ -102,7 +107,8 @@ const selectErrors = createSelector(
|
|||||||
qualityProfilesError ||
|
qualityProfilesError ||
|
||||||
metadataProfilesError ||
|
metadataProfilesError ||
|
||||||
importListsError ||
|
importListsError ||
|
||||||
systemStatusError
|
systemStatusError ||
|
||||||
|
translationsError
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -114,7 +120,8 @@ const selectErrors = createSelector(
|
|||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
metadataProfilesError,
|
metadataProfilesError,
|
||||||
importListsError,
|
importListsError,
|
||||||
systemStatusError
|
systemStatusError,
|
||||||
|
translationsError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -176,6 +183,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatchFetchStatus() {
|
dispatchFetchStatus() {
|
||||||
dispatch(fetchStatus());
|
dispatch(fetchStatus());
|
||||||
},
|
},
|
||||||
|
dispatchFetchTranslations() {
|
||||||
|
dispatch(fetchTranslations());
|
||||||
|
},
|
||||||
onResize(dimensions) {
|
onResize(dimensions) {
|
||||||
dispatch(saveDimensions(dimensions));
|
dispatch(saveDimensions(dimensions));
|
||||||
},
|
},
|
||||||
@@ -210,6 +220,7 @@ class PageConnector extends Component {
|
|||||||
this.props.dispatchFetchImportLists();
|
this.props.dispatchFetchImportLists();
|
||||||
this.props.dispatchFetchUISettings();
|
this.props.dispatchFetchUISettings();
|
||||||
this.props.dispatchFetchStatus();
|
this.props.dispatchFetchStatus();
|
||||||
|
this.props.dispatchFetchTranslations();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +247,7 @@ class PageConnector extends Component {
|
|||||||
dispatchFetchImportLists,
|
dispatchFetchImportLists,
|
||||||
dispatchFetchUISettings,
|
dispatchFetchUISettings,
|
||||||
dispatchFetchStatus,
|
dispatchFetchStatus,
|
||||||
|
dispatchFetchTranslations,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -277,6 +289,7 @@ PageConnector.propTypes = {
|
|||||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchTranslations: PropTypes.func.isRequired,
|
||||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,28 +21,28 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
|
|||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
iconName: icons.AUTHOR_CONTINUING,
|
iconName: icons.AUTHOR_CONTINUING,
|
||||||
title: 'Library',
|
title: () => translate('Library'),
|
||||||
to: '/',
|
to: '/',
|
||||||
alias: '/authors',
|
alias: '/authors',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Authors',
|
title: () => translate('Authors'),
|
||||||
to: '/authors'
|
to: '/authors'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Books',
|
title: () => translate('Books'),
|
||||||
to: '/books'
|
to: '/books'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Add New',
|
title: () => translate('AddNew'),
|
||||||
to: '/add/search'
|
to: '/add/search'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Bookshelf',
|
title: () => translate('Bookshelf'),
|
||||||
to: '/shelf'
|
to: '/shelf'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Unmapped Files',
|
title: () => translate('UnmappedFiles'),
|
||||||
to: '/unmapped'
|
to: '/unmapped'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -50,26 +50,26 @@ const links = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.CALENDAR,
|
iconName: icons.CALENDAR,
|
||||||
title: 'Calendar',
|
title: () => translate('Calendar'),
|
||||||
to: '/calendar'
|
to: '/calendar'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.ACTIVITY,
|
iconName: icons.ACTIVITY,
|
||||||
title: 'Activity',
|
title: () => translate('Activity'),
|
||||||
to: '/activity/queue',
|
to: '/activity/queue',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Queue',
|
title: () => translate('Queue'),
|
||||||
to: '/activity/queue',
|
to: '/activity/queue',
|
||||||
statusComponent: QueueStatusConnector
|
statusComponent: QueueStatusConnector
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'History',
|
title: () => translate('History'),
|
||||||
to: '/activity/history'
|
to: '/activity/history'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Blocklist',
|
title: () => translate('Blocklist'),
|
||||||
to: '/activity/blocklist'
|
to: '/activity/blocklist'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -77,15 +77,15 @@ const links = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.WARNING,
|
iconName: icons.WARNING,
|
||||||
title: 'Wanted',
|
title: () => translate('Wanted'),
|
||||||
to: '/wanted/missing',
|
to: '/wanted/missing',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Missing',
|
title: () => translate('Missing'),
|
||||||
to: '/wanted/missing'
|
to: '/wanted/missing'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Cutoff Unmet',
|
title: () => translate('CutoffUnmet'),
|
||||||
to: '/wanted/cutoffunmet'
|
to: '/wanted/cutoffunmet'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -93,55 +93,55 @@ const links = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.SETTINGS,
|
iconName: icons.SETTINGS,
|
||||||
title: 'Settings',
|
title: () => translate('Settings'),
|
||||||
to: '/settings',
|
to: '/settings',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Media Management',
|
title: () => translate('MediaManagement'),
|
||||||
to: '/settings/mediamanagement'
|
to: '/settings/mediamanagement'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Profiles',
|
title: () => translate('Profiles'),
|
||||||
to: '/settings/profiles'
|
to: '/settings/profiles'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Quality',
|
title: () => translate('Quality'),
|
||||||
to: '/settings/quality'
|
to: '/settings/quality'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: translate('CustomFormats'),
|
title: () => translate('CustomFormats'),
|
||||||
to: '/settings/customformats'
|
to: '/settings/customformats'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: translate('Indexers'),
|
title: () => translate('Indexers'),
|
||||||
to: '/settings/indexers'
|
to: '/settings/indexers'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Download Clients',
|
title: () => translate('DownloadClients'),
|
||||||
to: '/settings/downloadclients'
|
to: '/settings/downloadclients'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Import Lists',
|
title: () => translate('ImportLists'),
|
||||||
to: '/settings/importlists'
|
to: '/settings/importlists'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Connect',
|
title: () => translate('Connect'),
|
||||||
to: '/settings/connect'
|
to: '/settings/connect'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Metadata',
|
title: () => translate('Metadata'),
|
||||||
to: '/settings/metadata'
|
to: '/settings/metadata'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tags',
|
title: () => translate('Tags'),
|
||||||
to: '/settings/tags'
|
to: '/settings/tags'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'General',
|
title: () => translate('General'),
|
||||||
to: '/settings/general'
|
to: '/settings/general'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'UI',
|
title: () => translate('Ui'),
|
||||||
to: '/settings/ui'
|
to: '/settings/ui'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -149,32 +149,32 @@ const links = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.SYSTEM,
|
iconName: icons.SYSTEM,
|
||||||
title: 'System',
|
title: () => translate('System'),
|
||||||
to: '/system/status',
|
to: '/system/status',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Status',
|
title: () => translate('Status'),
|
||||||
to: '/system/status',
|
to: '/system/status',
|
||||||
statusComponent: HealthStatusConnector
|
statusComponent: HealthStatusConnector
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tasks',
|
title: () => translate('Tasks'),
|
||||||
to: '/system/tasks'
|
to: '/system/tasks'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Backup',
|
title: () => translate('Backup'),
|
||||||
to: '/system/backup'
|
to: '/system/backup'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Updates',
|
title: () => translate('Updates'),
|
||||||
to: '/system/updates'
|
to: '/system/updates'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Events',
|
title: () => translate('Events'),
|
||||||
to: '/system/events'
|
to: '/system/events'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Log Files',
|
title: () => translate('LogFiles'),
|
||||||
to: '/system/logs/files'
|
to: '/system/logs/files'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<span className={isChildItem ? styles.noIcon : null}>
|
<span className={isChildItem ? styles.noIcon : null}>
|
||||||
{title}
|
{typeof title === 'function' ? title() : title}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
|
|||||||
|
|
||||||
PageSidebarItem.propTypes = {
|
PageSidebarItem.propTypes = {
|
||||||
iconName: PropTypes.object,
|
iconName: PropTypes.object,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||||
to: PropTypes.string.isRequired,
|
to: PropTypes.string.isRequired,
|
||||||
isActive: PropTypes.bool,
|
isActive: PropTypes.bool,
|
||||||
isActiveParent: PropTypes.bool,
|
isActiveParent: PropTypes.bool,
|
||||||
|
|||||||
14
frontend/src/Components/Table/Column.ts
Normal file
14
frontend/src/Components/Table/Column.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type PropertyFunction<T> = () => T;
|
||||||
|
|
||||||
|
interface Column {
|
||||||
|
name: string;
|
||||||
|
label: string | PropertyFunction<string> | React.ReactNode;
|
||||||
|
columnLabel?: string;
|
||||||
|
isSortable?: boolean;
|
||||||
|
isVisible: boolean;
|
||||||
|
isModifiable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Column;
|
||||||
@@ -52,6 +52,7 @@ function Table(props) {
|
|||||||
scrollDirections.HORIZONTAL :
|
scrollDirections.HORIZONTAL :
|
||||||
scrollDirections.NONE
|
scrollDirections.NONE
|
||||||
}
|
}
|
||||||
|
autoFocus={false}
|
||||||
>
|
>
|
||||||
<table className={className}>
|
<table className={className}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -106,7 +107,7 @@ function Table(props) {
|
|||||||
{...getTableHeaderCellProps(otherProps)}
|
{...getTableHeaderCellProps(otherProps)}
|
||||||
{...column}
|
{...column}
|
||||||
>
|
>
|
||||||
{column.label}
|
{typeof column.label === 'function' ? column.label() : column.label}
|
||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -120,6 +121,7 @@ function Table(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Table.propTypes = {
|
Table.propTypes = {
|
||||||
|
...TableHeaderCell.props,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
horizontalScroll: PropTypes.bool.isRequired,
|
horizontalScroll: PropTypes.bool.isRequired,
|
||||||
selectAll: PropTypes.bool.isRequired,
|
selectAll: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
|
|||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
name,
|
name,
|
||||||
|
label,
|
||||||
columnLabel,
|
columnLabel,
|
||||||
isSortable,
|
isSortable,
|
||||||
isVisible,
|
isVisible,
|
||||||
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
|
|||||||
{...otherProps}
|
{...otherProps}
|
||||||
component="th"
|
component="th"
|
||||||
className={className}
|
className={className}
|
||||||
title={columnLabel}
|
label={typeof label === 'function' ? label() : label}
|
||||||
|
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
|
|||||||
TableHeaderCell.propTypes = {
|
TableHeaderCell.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
columnLabel: PropTypes.string,
|
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
|
||||||
|
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||||
isSortable: PropTypes.bool,
|
isSortable: PropTypes.bool,
|
||||||
isVisible: PropTypes.bool,
|
isVisible: PropTypes.bool,
|
||||||
isModifiable: PropTypes.bool,
|
isModifiable: PropTypes.bool,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
|
|||||||
isDisabled={isModifiable === false}
|
isDisabled={isModifiable === false}
|
||||||
onChange={onVisibleChange}
|
onChange={onVisibleChange}
|
||||||
/>
|
/>
|
||||||
{label}
|
{typeof label === 'function' ? label() : label}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
|
|||||||
|
|
||||||
TableOptionsColumn.propTypes = {
|
TableOptionsColumn.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||||
isVisible: PropTypes.bool.isRequired,
|
isVisible: PropTypes.bool.isRequired,
|
||||||
isModifiable: PropTypes.bool.isRequired,
|
isModifiable: PropTypes.bool.isRequired,
|
||||||
index: PropTypes.number.isRequired,
|
index: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
|
|||||||
|
|
||||||
<TableOptionsColumn
|
<TableOptionsColumn
|
||||||
name={name}
|
name={name}
|
||||||
label={label}
|
label={typeof label === 'function' ? label() : label}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
isModifiable={isModifiable}
|
isModifiable={isModifiable}
|
||||||
index={index}
|
index={index}
|
||||||
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
|
|||||||
|
|
||||||
TableOptionsColumnDragSource.propTypes = {
|
TableOptionsColumnDragSource.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||||
isVisible: PropTypes.bool.isRequired,
|
isVisible: PropTypes.bool.isRequired,
|
||||||
isModifiable: PropTypes.bool.isRequired,
|
isModifiable: PropTypes.bool.isRequired,
|
||||||
index: PropTypes.number.isRequired,
|
index: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { tooltipPositions } from 'Helpers/Props';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
import styles from './Popover.css';
|
import styles from './Popover.css';
|
||||||
|
|
||||||
@@ -30,8 +31,13 @@ function Popover(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Popover.propTypes = {
|
Popover.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
bodyClassName: PropTypes.string,
|
||||||
|
anchor: PropTypes.node.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired
|
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||||
|
position: PropTypes.oneOf(tooltipPositions.all),
|
||||||
|
canFlip: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Popover;
|
export default Popover;
|
||||||
|
|||||||
11
frontend/src/Helpers/Hooks/usePrevious.tsx
Normal file
11
frontend/src/Helpers/Hooks/usePrevious.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export default function usePrevious<T>(value: T): T | undefined {
|
||||||
|
const ref = useRef<T>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
113
frontend/src/Helpers/Hooks/useSelectState.tsx
Normal file
113
frontend/src/Helpers/Hooks/useSelectState.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { useReducer } from 'react';
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import areAllSelected from 'Utilities/Table/areAllSelected';
|
||||||
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
|
||||||
|
export type SelectedState = Record<number, boolean>;
|
||||||
|
|
||||||
|
export interface SelectState {
|
||||||
|
selectedState: SelectedState;
|
||||||
|
lastToggled: number | null;
|
||||||
|
allSelected: boolean;
|
||||||
|
allUnselected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectAction =
|
||||||
|
| { type: 'reset' }
|
||||||
|
| { type: 'selectAll'; items: ModelBase[] }
|
||||||
|
| { type: 'unselectAll'; items: ModelBase[] }
|
||||||
|
| {
|
||||||
|
type: 'toggleSelected';
|
||||||
|
id: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
shiftKey: boolean;
|
||||||
|
items: ModelBase[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'removeItem';
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'updateItems';
|
||||||
|
items: ModelBase[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Dispatch = (action: SelectAction) => void;
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
selectedState: {},
|
||||||
|
lastToggled: null,
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: true,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSelectedState(items: ModelBase[], existingState: SelectedState) {
|
||||||
|
return items.reduce((acc: SelectedState, item) => {
|
||||||
|
const id = item.id;
|
||||||
|
|
||||||
|
acc[id] = existingState[id] ?? false;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectReducer(state: SelectState, action: SelectAction): SelectState {
|
||||||
|
const { selectedState } = state;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case 'reset': {
|
||||||
|
return cloneDeep(initialState);
|
||||||
|
}
|
||||||
|
case 'selectAll': {
|
||||||
|
return {
|
||||||
|
...selectAll(selectedState, true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'unselectAll': {
|
||||||
|
return {
|
||||||
|
...selectAll(selectedState, false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'toggleSelected': {
|
||||||
|
const result = {
|
||||||
|
...toggleSelected(
|
||||||
|
state,
|
||||||
|
action.items,
|
||||||
|
action.id,
|
||||||
|
action.isSelected,
|
||||||
|
action.shiftKey
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
case 'updateItems': {
|
||||||
|
const nextSelectedState = getSelectedState(action.items, selectedState);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...areAllSelected(nextSelectedState),
|
||||||
|
selectedState: nextSelectedState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unhandled action type: ${action.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useSelectState(): [SelectState, Dispatch] {
|
||||||
|
const selectedState = getSelectedState([], {});
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(selectReducer, {
|
||||||
|
selectedState,
|
||||||
|
lastToggled: null,
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [state, dispatch];
|
||||||
|
}
|
||||||
6
frontend/src/Helpers/Props/SortDirection.ts
Normal file
6
frontend/src/Helpers/Props/SortDirection.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
enum SortDirection {
|
||||||
|
Ascending = 'ascending',
|
||||||
|
Descending = 'descending',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SortDirection;
|
||||||
8
frontend/src/Helpers/Props/TooltipPosition.ts
Normal file
8
frontend/src/Helpers/Props/TooltipPosition.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
enum TooltipPosition {
|
||||||
|
Top = 'top',
|
||||||
|
Right = 'right',
|
||||||
|
Bottom = 'bottom',
|
||||||
|
Left = 'left',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TooltipPosition;
|
||||||
@@ -68,6 +68,7 @@ import {
|
|||||||
faInfoCircle as fasInfoCircle,
|
faInfoCircle as fasInfoCircle,
|
||||||
faLaptop as fasLaptop,
|
faLaptop as fasLaptop,
|
||||||
faLevelUpAlt as fasLevelUpAlt,
|
faLevelUpAlt as fasLevelUpAlt,
|
||||||
|
faListCheck as fasListCheck,
|
||||||
faLongArrowAltRight as fasLongArrowAltRight,
|
faLongArrowAltRight as fasLongArrowAltRight,
|
||||||
faMedkit as fasMedkit,
|
faMedkit as fasMedkit,
|
||||||
faMinus as fasMinus,
|
faMinus as fasMinus,
|
||||||
@@ -166,6 +167,7 @@ export const INFO = fasInfoCircle;
|
|||||||
export const INTERACTIVE = fasUser;
|
export const INTERACTIVE = fasUser;
|
||||||
export const KEYBOARD = farKeyboard;
|
export const KEYBOARD = farKeyboard;
|
||||||
export const LOGOUT = fasSignOutAlt;
|
export const LOGOUT = fasSignOutAlt;
|
||||||
|
export const MANAGE = fasListCheck;
|
||||||
export const MEDIA_INFO = farFileInvoice;
|
export const MEDIA_INFO = farFileInvoice;
|
||||||
export const MISSING = fasExclamationTriangle;
|
export const MISSING = fasExclamationTriangle;
|
||||||
export const MONITORED = fasBookmark;
|
export const MONITORED = fasBookmark;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const TAG = 'tag';
|
|||||||
export const TEXT = 'text';
|
export const TEXT = 'text';
|
||||||
export const TEXT_AREA = 'textArea';
|
export const TEXT_AREA = 'textArea';
|
||||||
export const TEXT_TAG = 'textTag';
|
export const TEXT_TAG = 'textTag';
|
||||||
|
export const TAG_SELECT = 'tagSelect';
|
||||||
export const UMASK = 'umask';
|
export const UMASK = 'umask';
|
||||||
|
|
||||||
export const all = [
|
export const all = [
|
||||||
@@ -49,5 +50,6 @@ export const all = [
|
|||||||
TEXT,
|
TEXT,
|
||||||
TEXT_AREA,
|
TEXT_AREA,
|
||||||
TEXT_TAG,
|
TEXT_TAG,
|
||||||
|
TAG_SELECT,
|
||||||
UMASK
|
UMASK
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const columns = [
|
|||||||
name: 'customFormats',
|
name: 'customFormats',
|
||||||
label: React.createElement(Icon, {
|
label: React.createElement(Icon, {
|
||||||
name: icons.INTERACTIVE,
|
name: icons.INTERACTIVE,
|
||||||
title: translate('CustomFormat')
|
title: () => translate('CustomFormat')
|
||||||
}),
|
}),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
@@ -91,9 +91,9 @@ const filterExistingFilesOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const importModeOptions = [
|
const importModeOptions = [
|
||||||
{ key: 'chooseImportMode', value: translate('ChooseImportMethod'), disabled: true },
|
{ key: 'chooseImportMode', value: () => translate('ChooseImportMethod'), disabled: true },
|
||||||
{ key: 'move', value: translate('MoveFiles') },
|
{ key: 'move', value: () => translate('MoveFiles') },
|
||||||
{ key: 'copy', value: translate('HardlinkCopyFiles') }
|
{ key: 'copy', value: () => translate('HardlinkCopyFiles') }
|
||||||
];
|
];
|
||||||
|
|
||||||
const SELECT = 'select';
|
const SELECT = 'select';
|
||||||
|
|||||||
@@ -258,7 +258,9 @@ class InteractiveImportRow extends Component {
|
|||||||
>
|
>
|
||||||
{
|
{
|
||||||
showReleaseGroupPlaceholder ?
|
showReleaseGroupPlaceholder ?
|
||||||
<InteractiveImportRowCellPlaceholder /> :
|
<InteractiveImportRowCellPlaceholder
|
||||||
|
isOptional={true}
|
||||||
|
/> :
|
||||||
releaseGroup
|
releaseGroup
|
||||||
}
|
}
|
||||||
</TableRowCellButton>
|
</TableRowCellButton>
|
||||||
|
|||||||
@@ -5,3 +5,7 @@
|
|||||||
height: 25px;
|
height: 25px;
|
||||||
border: 2px dashed var(--dangerColor);
|
border: 2px dashed var(--dangerColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.optional {
|
||||||
|
border: 2px dashed var(--gray);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'optional': string;
|
||||||
'placeholder': string;
|
'placeholder': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styles from './InteractiveImportRowCellPlaceholder.css';
|
|
||||||
|
|
||||||
function InteractiveImportRowCellPlaceholder() {
|
|
||||||
return (
|
|
||||||
<span className={styles.placeholder} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InteractiveImportRowCellPlaceholder;
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './InteractiveImportRowCellPlaceholder.css';
|
||||||
|
|
||||||
|
interface InteractiveImportRowCellPlaceholderProps {
|
||||||
|
isOptional?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InteractiveImportRowCellPlaceholder(
|
||||||
|
props: InteractiveImportRowCellPlaceholderProps
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
styles.placeholder,
|
||||||
|
props.isOptional && styles.optional
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InteractiveImportRowCellPlaceholder;
|
||||||
@@ -56,7 +56,7 @@ const columns = [
|
|||||||
name: 'customFormatScore',
|
name: 'customFormatScore',
|
||||||
label: React.createElement(Icon, {
|
label: React.createElement(Icon, {
|
||||||
name: icons.SCORE,
|
name: icons.SCORE,
|
||||||
title: translate('CustomFormatScore')
|
title: () => translate('CustomFormatScore')
|
||||||
}),
|
}),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import Peers from './Peers';
|
import Peers from './Peers';
|
||||||
import styles from './InteractiveSearchRow.css';
|
import styles from './InteractiveSearchRow.css';
|
||||||
@@ -32,6 +32,18 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
|||||||
return icons.DOWNLOAD;
|
return icons.DOWNLOAD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDownloadKind(isGrabbed, grabError, downloadAllowed) {
|
||||||
|
if (isGrabbed) {
|
||||||
|
return kinds.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grabError || !downloadAllowed) {
|
||||||
|
return kinds.DANGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return kinds.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||||
if (isGrabbing) {
|
if (isGrabbing) {
|
||||||
return '';
|
return '';
|
||||||
@@ -172,7 +184,7 @@ class InteractiveSearchRow extends Component {
|
|||||||
<TableRowCell className={styles.customFormatScore}>
|
<TableRowCell className={styles.customFormatScore}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
anchor={
|
anchor={
|
||||||
formatPreferredWordScore(customFormatScore, customFormats.length)
|
formatCustomFormatScore(customFormatScore, customFormats.length)
|
||||||
}
|
}
|
||||||
tooltip={<BookFormats formats={customFormats} />}
|
tooltip={<BookFormats formats={customFormats} />}
|
||||||
position={tooltipPositions.BOTTOM}
|
position={tooltipPositions.BOTTOM}
|
||||||
@@ -212,7 +224,7 @@ class InteractiveSearchRow extends Component {
|
|||||||
{
|
{
|
||||||
<SpinnerIconButton
|
<SpinnerIconButton
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
kind={grabError || !downloadAllowed ? kinds.DANGER : kinds.DEFAULT}
|
kind={getDownloadKind(isGrabbed, grabError, downloadAllowed)}
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
isSpinning={isGrabbing}
|
isSpinning={isGrabbing}
|
||||||
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { icons } from 'Helpers/Props';
|
|||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
|
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
|
||||||
|
import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal';
|
||||||
import DownloadClientOptionsConnector from './Options/DownloadClientOptionsConnector';
|
import DownloadClientOptionsConnector from './Options/DownloadClientOptionsConnector';
|
||||||
import RemotePathMappingsConnector from './RemotePathMappings/RemotePathMappingsConnector';
|
import RemotePathMappingsConnector from './RemotePathMappings/RemotePathMappingsConnector';
|
||||||
|
|
||||||
@@ -23,7 +24,8 @@ class DownloadClientSettings extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
hasPendingChanges: false
|
hasPendingChanges: false,
|
||||||
|
isManageDownloadClientsOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +40,14 @@ class DownloadClientSettings extends Component {
|
|||||||
this.setState(payload);
|
this.setState(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onManageDownloadClientsPress = () => {
|
||||||
|
this.setState({ isManageDownloadClientsOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onManageDownloadClientsModalClose = () => {
|
||||||
|
this.setState({ isManageDownloadClientsOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
onSavePress = () => {
|
onSavePress = () => {
|
||||||
if (this._saveCallback) {
|
if (this._saveCallback) {
|
||||||
this._saveCallback();
|
this._saveCallback();
|
||||||
@@ -55,7 +65,8 @@ class DownloadClientSettings extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
isSaving,
|
isSaving,
|
||||||
hasPendingChanges
|
hasPendingChanges,
|
||||||
|
isManageDownloadClientsOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -73,6 +84,12 @@ class DownloadClientSettings extends Component {
|
|||||||
isSpinning={isTestingAll}
|
isSpinning={isTestingAll}
|
||||||
onPress={dispatchTestAllDownloadClients}
|
onPress={dispatchTestAllDownloadClients}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('ManageClients')}
|
||||||
|
iconName={icons.MANAGE}
|
||||||
|
onPress={this.onManageDownloadClientsPress}
|
||||||
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
@@ -87,6 +104,11 @@ class DownloadClientSettings extends Component {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<RemotePathMappingsConnector />
|
<RemotePathMappingsConnector />
|
||||||
|
|
||||||
|
<ManageDownloadClientsModal
|
||||||
|
isOpen={isManageDownloadClientsOpen}
|
||||||
|
onModalClose={this.onManageDownloadClientsModalClose}
|
||||||
|
/>
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import Card from 'Components/Card';
|
import Card from 'Components/Card';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TagList from 'Components/TagList';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import EditDownloadClientModalConnector from './EditDownloadClientModalConnector';
|
import EditDownloadClientModalConnector from './EditDownloadClientModalConnector';
|
||||||
@@ -56,7 +57,9 @@ class DownloadClient extends Component {
|
|||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
enable,
|
enable,
|
||||||
priority
|
priority,
|
||||||
|
tags,
|
||||||
|
tagList
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -94,6 +97,11 @@ class DownloadClient extends Component {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TagList
|
||||||
|
tags={tags}
|
||||||
|
tagList={tagList}
|
||||||
|
/>
|
||||||
|
|
||||||
<EditDownloadClientModalConnector
|
<EditDownloadClientModalConnector
|
||||||
id={id}
|
id={id}
|
||||||
isOpen={this.state.isEditDownloadClientModalOpen}
|
isOpen={this.state.isEditDownloadClientModalOpen}
|
||||||
@@ -120,6 +128,8 @@ DownloadClient.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
enable: PropTypes.bool.isRequired,
|
enable: PropTypes.bool.isRequired,
|
||||||
priority: PropTypes.number.isRequired,
|
priority: PropTypes.number.isRequired,
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class DownloadClients extends Component {
|
|||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
onConfirmDeleteDownloadClient,
|
onConfirmDeleteDownloadClient,
|
||||||
|
tagList,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ class DownloadClients extends Component {
|
|||||||
<DownloadClient
|
<DownloadClient
|
||||||
key={item.id}
|
key={item.id}
|
||||||
{...item}
|
{...item}
|
||||||
|
tagList={tagList}
|
||||||
onConfirmDeleteDownloadClient={onConfirmDeleteDownloadClient}
|
onConfirmDeleteDownloadClient={onConfirmDeleteDownloadClient}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -109,6 +111,7 @@ DownloadClients.propTypes = {
|
|||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,20 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
|
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import DownloadClients from './DownloadClients';
|
import DownloadClients from './DownloadClients';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.downloadClients', sortByName),
|
createSortedSectionSelector('settings.downloadClients', sortByName),
|
||||||
(downloadClients) => downloadClients
|
createTagsSelector(),
|
||||||
|
(downloadClients, tagList) => {
|
||||||
|
return {
|
||||||
|
...downloadClients,
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
@@ -13,7 +14,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditDownloadClientModalContent.css';
|
import styles from './EditDownloadClientModalContent.css';
|
||||||
|
|
||||||
@@ -45,8 +46,12 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
implementationName,
|
implementationName,
|
||||||
name,
|
name,
|
||||||
enable,
|
enable,
|
||||||
|
protocol,
|
||||||
priority,
|
priority,
|
||||||
|
removeCompletedDownloads,
|
||||||
|
removeFailedDownloads,
|
||||||
fields,
|
fields,
|
||||||
|
tags,
|
||||||
message
|
message
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
@@ -142,6 +147,49 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
helpText={translate('DownloadClientTagHelpText')}
|
||||||
|
{...tags}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FieldSet
|
||||||
|
size={sizes.SMALL}
|
||||||
|
legend={translate('CompletedDownloadHandling')}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RemoveCompleted')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="removeCompletedDownloads"
|
||||||
|
helpText={translate('RemoveCompletedDownloadsHelpText')}
|
||||||
|
{...removeCompletedDownloads}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
protocol.value !== 'torrent' &&
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RemoveFailed')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="removeFailedDownloads"
|
||||||
|
helpText={translate('RemoveFailedDownloadsHelpText')}
|
||||||
|
{...removeFailedDownloads}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
}
|
||||||
|
</FieldSet>
|
||||||
</Form>
|
</Form>
|
||||||
}
|
}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ManageDownloadClientsEditModalContent from './ManageDownloadClientsEditModalContent';
|
||||||
|
|
||||||
|
interface ManageDownloadClientsEditModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
downloadClientIds: number[];
|
||||||
|
onSavePress(payload: object): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageDownloadClientsEditModal(
|
||||||
|
props: ManageDownloadClientsEditModalProps
|
||||||
|
) {
|
||||||
|
const { isOpen, downloadClientIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ManageDownloadClientsEditModalContent
|
||||||
|
downloadClientIds={downloadClientIds}
|
||||||
|
onSavePress={onSavePress}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageDownloadClientsEditModal;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.modalFooter {
|
||||||
|
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointExtraSmall) {
|
||||||
|
.modalFooter {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'modalFooter': string;
|
||||||
|
'selected': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './ManageDownloadClientsEditModalContent.css';
|
||||||
|
|
||||||
|
interface SavePayload {
|
||||||
|
enable?: boolean;
|
||||||
|
removeCompletedDownloads?: boolean;
|
||||||
|
removeFailedDownloads?: boolean;
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ManageDownloadClientsEditModalContentProps {
|
||||||
|
downloadClientIds: number[];
|
||||||
|
onSavePress(payload: object): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
|
const enableOptions = [
|
||||||
|
{
|
||||||
|
key: NO_CHANGE,
|
||||||
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Enabled');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'disabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Disabled');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function ManageDownloadClientsEditModalContent(
|
||||||
|
props: ManageDownloadClientsEditModalContentProps
|
||||||
|
) {
|
||||||
|
const { downloadClientIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
|
const [enable, setEnable] = useState(NO_CHANGE);
|
||||||
|
const [removeCompletedDownloads, setRemoveCompletedDownloads] =
|
||||||
|
useState(NO_CHANGE);
|
||||||
|
const [removeFailedDownloads, setRemoveFailedDownloads] = useState(NO_CHANGE);
|
||||||
|
const [priority, setPriority] = useState<null | string | number>(null);
|
||||||
|
|
||||||
|
const save = useCallback(() => {
|
||||||
|
let hasChanges = false;
|
||||||
|
const payload: SavePayload = {};
|
||||||
|
|
||||||
|
if (enable !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.enable = enable === 'enabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeCompletedDownloads !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.removeCompletedDownloads = removeCompletedDownloads === 'enabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeFailedDownloads !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.removeFailedDownloads = removeFailedDownloads === 'enabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priority !== null) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.priority = priority as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
onSavePress(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}, [
|
||||||
|
enable,
|
||||||
|
priority,
|
||||||
|
removeCompletedDownloads,
|
||||||
|
removeFailedDownloads,
|
||||||
|
onSavePress,
|
||||||
|
onModalClose,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onInputChange = useCallback(
|
||||||
|
({ name, value }: { name: string; value: string }) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'enable':
|
||||||
|
setEnable(value);
|
||||||
|
break;
|
||||||
|
case 'priority':
|
||||||
|
setPriority(value);
|
||||||
|
break;
|
||||||
|
case 'removeCompletedDownloads':
|
||||||
|
setRemoveCompletedDownloads(value);
|
||||||
|
break;
|
||||||
|
case 'removeFailedDownloads':
|
||||||
|
setRemoveFailedDownloads(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(
|
||||||
|
`EditDownloadClientsModalContent Unknown Input: '${name}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedCount = downloadClientIds.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('EditSelectedDownloadClients')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Enabled')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="enable"
|
||||||
|
value={enable}
|
||||||
|
values={enableOptions}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Priority')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="priority"
|
||||||
|
value={priority}
|
||||||
|
min={1}
|
||||||
|
max={50}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RemoveCompletedDownloads')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="removeCompletedDownloads"
|
||||||
|
value={removeCompletedDownloads}
|
||||||
|
values={enableOptions}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RemoveFailedDownloads')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="removeFailedDownloads"
|
||||||
|
value={removeFailedDownloads}
|
||||||
|
values={enableOptions}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<div className={styles.selected}>
|
||||||
|
{translate('CountDownloadClientsSelected', [selectedCount])}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button onPress={save}>{translate('ApplyChanges')}</Button>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageDownloadClientsEditModalContent;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ManageDownloadClientsModalContent from './ManageDownloadClientsModalContent';
|
||||||
|
|
||||||
|
interface ManageDownloadClientsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageDownloadClientsModal(props: ManageDownloadClientsModalProps) {
|
||||||
|
const { isOpen, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ManageDownloadClientsModalContent onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageDownloadClientsModal;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.leftButtons,
|
||||||
|
.rightButtons {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 50%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightButtons {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user