mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-07 13:40:02 -05:00
Compare commits
124 Commits
redirect-r
...
test-rebas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0952d4a0c | ||
|
|
579f1eae2d | ||
|
|
091ebb5cee | ||
|
|
d3a96fac12 | ||
|
|
da0b60dd96 | ||
|
|
3bfdd4aafa | ||
|
|
da2bc75eab | ||
|
|
1d922f3ce6 | ||
|
|
b3bdaeeffa | ||
|
|
604c354284 | ||
|
|
ec0fc6f3e1 | ||
|
|
2d28359627 | ||
|
|
45d0f4bdb7 | ||
|
|
ba1f2a5a7b | ||
|
|
0ef8f2b2f7 | ||
|
|
91d6fe90cd | ||
|
|
baa728f2c1 | ||
|
|
d20f9ea269 | ||
|
|
db809f579c | ||
|
|
4970ed5323 | ||
|
|
fa653bf546 | ||
|
|
f496d96907 | ||
|
|
cfc52d12ea | ||
|
|
40f156c16e | ||
|
|
a322d9ed6d | ||
|
|
604666f961 | ||
|
|
851ff948af | ||
|
|
b6f052f39c | ||
|
|
e6236823cf | ||
|
|
32c759d32b | ||
|
|
993d5818a4 | ||
|
|
2b93aaa9b2 | ||
|
|
4ec873bf7a | ||
|
|
b574048733 | ||
|
|
5c4dfc5e7b | ||
|
|
5545ae94ac | ||
|
|
15e0b8dd4d | ||
|
|
ac4a82abf4 | ||
|
|
cb07186970 | ||
|
|
e5409dbff6 | ||
|
|
59d6a77b74 | ||
|
|
db0151c39b | ||
|
|
4ac646685c | ||
|
|
d01d112bfe | ||
|
|
77ef282916 | ||
|
|
e5f483eadc | ||
|
|
8e78bf71a1 | ||
|
|
0c581c6146 | ||
|
|
cabec3c625 | ||
|
|
a0ef1ebaad | ||
|
|
5abfee1bf8 | ||
|
|
f46d5534f4 | ||
|
|
35225c799b | ||
|
|
89584666ff | ||
|
|
a776336b8c | ||
|
|
39a0cb3f43 | ||
|
|
2abee1970f | ||
|
|
cdf648670d | ||
|
|
810a02090b | ||
|
|
4282b84fb6 | ||
|
|
42acea55fd | ||
|
|
c7e7d48ca3 | ||
|
|
b1fbdebba9 | ||
|
|
b4bfd7d07f | ||
|
|
470ba66d4a | ||
|
|
400fa4a960 | ||
|
|
e9de0420dd | ||
|
|
56a623bad9 | ||
|
|
26bf0c0542 | ||
|
|
33de5d2049 | ||
|
|
1e3b775837 | ||
|
|
efa77c1823 | ||
|
|
3428ee3ca2 | ||
|
|
27c9337071 | ||
|
|
1fbaefc054 | ||
|
|
b1cfa90a9f | ||
|
|
3e01c3089c | ||
|
|
2e80c86e23 | ||
|
|
31837958c1 | ||
|
|
a22297c197 | ||
|
|
6e595bfad3 | ||
|
|
8ffcc8a711 | ||
|
|
4b3db6f596 | ||
|
|
778085544c | ||
|
|
81a20ab0e5 | ||
|
|
770176e127 | ||
|
|
2f56814f58 | ||
|
|
4247605dc9 | ||
|
|
3d4c8df6f7 | ||
|
|
a5e69528a9 | ||
|
|
074bcb4ebf | ||
|
|
2bd4965763 | ||
|
|
fdef79a9dc | ||
|
|
1423ebdb07 | ||
|
|
06d3e47232 | ||
|
|
255c19bb83 | ||
|
|
4104efa9a0 | ||
|
|
5f1b928cd9 | ||
|
|
49b085bac6 | ||
|
|
5a4034ec6a | ||
|
|
ed1175ed86 | ||
|
|
31287eb0d7 | ||
|
|
664905b4f2 | ||
|
|
a4223e8dbf | ||
|
|
3af8c2f504 | ||
|
|
67086060b1 | ||
|
|
099fd3166c | ||
|
|
43363cbd08 | ||
|
|
7d0eeca9a9 | ||
|
|
aa7d289a48 | ||
|
|
c0f8d92c6e | ||
|
|
2732ea43ad | ||
|
|
bc059c3f32 | ||
|
|
172f8e4b31 | ||
|
|
2e9087d4c3 | ||
|
|
d004f95297 | ||
|
|
566d84e22d | ||
|
|
8dd3c14132 | ||
|
|
acb3470988 | ||
|
|
953d8a92c2 | ||
|
|
129245cc5b | ||
|
|
4ecd7eaf08 | ||
|
|
698eedb51a | ||
|
|
a94623d95b |
@@ -19,11 +19,12 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.serva
|
||||
|
||||
1. Fork Readarr
|
||||
2. Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories)
|
||||
3. Install the required Node Packages `yarn install`
|
||||
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
|
||||
5. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `netcoreapp31`
|
||||
6. Debug the project in Visual Studio
|
||||
7. Open http://localhost:8787
|
||||
3. Grab the submodules `git submodule init && git submodule update`
|
||||
4. Install the required Node Packages `yarn install`
|
||||
5. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
|
||||
6. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `netcoreapp31`
|
||||
7. Debug the project in Visual Studio
|
||||
8. Open http://localhost:8787
|
||||
|
||||
### Contributing Code ###
|
||||
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Readarr/Readarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://hub.docker.com/r/hotio/readarr)
|
||||

|
||||
[](#backers) [](#sponsors)
|
||||
|
||||
### Readarr is in early stages of development, alpha/beta binary builds are not yet available. Use of any test builds isn't recommend, and may have detrimental effects on your library.
|
||||
|
||||
@@ -697,17 +697,6 @@ stages:
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh ${OSNAME} Automation Test
|
||||
displayName: Run Automation Tests
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy Screenshot to: $(Build.ArtifactStagingDirectory)'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)'
|
||||
Contents: |
|
||||
**/*_test_screenshot.png
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
|
||||
- publish: $(Build.ArtifactStagingDirectory)/screenshots
|
||||
artifact: '$(osName)AutomationScreenshots'
|
||||
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
|
||||
displayName: Publish Screenshot Bundle
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
@@ -10,11 +11,83 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import BlacklistRowConnector from './BlacklistRowConnector';
|
||||
|
||||
class Blacklist extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmRemoveModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
if (hasDifferentItems(prevProps.items, items)) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveSelectedConfirmed = () => {
|
||||
this.props.onRemoveSelected(this.getSelectedIds());
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -28,6 +101,7 @@ class Blacklist extends Component {
|
||||
items,
|
||||
columns,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlacklistExecuting,
|
||||
onClearBlacklistPress,
|
||||
...otherProps
|
||||
@@ -36,10 +110,27 @@ class Blacklist extends Component {
|
||||
const isAllPopulated = isPopulated && isAuthorPopulated;
|
||||
const isAnyFetching = isFetching || isAuthorFetching;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title="Blacklist">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Remove Selected"
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={!selectedIds.length}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Clear"
|
||||
iconName={icons.CLEAR}
|
||||
@@ -61,7 +152,7 @@ class Blacklist extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isAnyFetching && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -83,8 +174,12 @@ class Blacklist extends Component {
|
||||
isAllPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
@@ -92,8 +187,10 @@ class Blacklist extends Component {
|
||||
return (
|
||||
<BlacklistRowConnector
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -108,7 +205,17 @@ class Blacklist extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Remove Selected"
|
||||
message={'Are you sure you want to remove the selected items from the blacklist?'}
|
||||
confirmLabel="Remove Selected"
|
||||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -123,7 +230,9 @@ Blacklist.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
onRemoveSelected: PropTypes.func.isRequired,
|
||||
onClearBlacklistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -92,6 +92,10 @@ class BlacklistConnector extends Component {
|
||||
this.props.gotoBlacklistPage({ page });
|
||||
}
|
||||
|
||||
onRemoveSelected = (ids) => {
|
||||
this.props.removeBlacklistItems({ ids });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlacklistSort({ sortKey });
|
||||
}
|
||||
@@ -119,6 +123,7 @@ class BlacklistConnector extends Component {
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onRemoveSelected={this.onRemoveSelected}
|
||||
onSortPress={this.onSortPress}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onClearBlacklistPress={this.onClearBlacklistPress}
|
||||
@@ -138,6 +143,7 @@ BlacklistConnector.propTypes = {
|
||||
gotoBlacklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||
removeBlacklistItems: PropTypes.func.isRequired,
|
||||
setBlacklistSort: PropTypes.func.isRequired,
|
||||
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlacklist: PropTypes.func.isRequired,
|
||||
|
||||
@@ -5,6 +5,7 @@ import BookQuality from 'Book/BookQuality';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||
@@ -39,6 +40,7 @@ class BlacklistRow extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
author,
|
||||
sourceTitle,
|
||||
quality,
|
||||
@@ -46,7 +48,9 @@ class BlacklistRow extends Component {
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onRemovePress
|
||||
} = this.props;
|
||||
|
||||
@@ -56,6 +60,12 @@ class BlacklistRow extends Component {
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
@@ -167,7 +177,9 @@ BlacklistRow.propTypes = {
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
|
||||
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import BlacklistRow from './BlacklistRow';
|
||||
|
||||
@@ -18,7 +18,7 @@ function createMapStateToProps() {
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemovePress() {
|
||||
dispatch(removeFromBlacklist({ id: props.id }));
|
||||
dispatch(removeBlacklistItem({ id: props.id }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
@@ -98,7 +98,7 @@ class History extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetchingAny && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -149,7 +149,7 @@ class History extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
@@ -13,6 +13,7 @@ import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
@@ -36,34 +37,28 @@ class Queue extends Component {
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isPendingSelected: false,
|
||||
isConfirmRemoveModalOpen: false
|
||||
isConfirmRemoveModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
// Don't update when fetching has completed if items have changed,
|
||||
// before books start fetching or when books start fetching.
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
isFetching,
|
||||
isBooksFetching
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
this.props.isFetching &&
|
||||
nextProps.isPopulated &&
|
||||
hasDifferentItems(this.props.items, nextProps.items) &&
|
||||
nextProps.items.some((e) => e.bookId)
|
||||
(!isBooksFetching && prevProps.isBooksFetching) ||
|
||||
(!isFetching && prevProps.isFetching) ||
|
||||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.bookId))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.props.isBooksFetching && nextProps.isBooksFetching) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState((state) => {
|
||||
return removeOldSelectedState(state, prevProps.items);
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -200,7 +195,7 @@ class Queue extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isRefreshing && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -257,7 +252,7 @@ class Queue extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 auto;
|
||||
@@ -95,11 +96,9 @@
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.authorNavigationButtons {
|
||||
.artistNavigationButtons {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
@@ -31,7 +31,6 @@ import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
||||
@@ -295,7 +294,7 @@ class AuthorDetails extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector innerClassName={styles.innerContentBody}>
|
||||
<PageContentBody innerClassName={styles.innerContentBody}>
|
||||
<div className={styles.header}>
|
||||
<div
|
||||
className={styles.backdrop}
|
||||
@@ -412,7 +411,7 @@ class AuthorDetails extends Component {
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk)
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
@@ -518,7 +517,7 @@ class AuthorDetails extends Component {
|
||||
<div className={styles.overview}>
|
||||
<TextTruncate
|
||||
line={Math.floor(125 / (defaultFontSize * lineHeight))}
|
||||
text={stripHtml(overview)}
|
||||
text={overview}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -686,7 +685,7 @@ class AuthorDetails extends Component {
|
||||
showImportMode={false}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createSelector } from 'reselect';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import AuthorDetailsConnector from './AuthorDetailsConnector';
|
||||
import styles from './AuthorDetails.css';
|
||||
@@ -74,9 +74,9 @@ class AuthorDetailsPageConnector extends Component {
|
||||
if (isFetching && !isPopulated) {
|
||||
return (
|
||||
<PageContent title='loading'>
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<LoadingIndicator />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@ import NoAuthor from 'Author/NoAuthor';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { align, sortDirections } from 'Helpers/Props';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
@@ -20,46 +23,6 @@ import AuthorEditorFooter from './AuthorEditorFooter';
|
||||
import AuthorEditorRowConnector from './AuthorEditorRowConnector';
|
||||
import OrganizeAuthorModal from './Organize/OrganizeAuthorModal';
|
||||
|
||||
function getColumns(showMetadataProfile) {
|
||||
return [
|
||||
{
|
||||
name: 'status',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sortName',
|
||||
label: 'Name',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
label: 'Quality Profile',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'metadataProfileId',
|
||||
label: 'Metadata Profile',
|
||||
isSortable: true,
|
||||
isVisible: showMetadataProfile
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
class AuthorEditor extends Component {
|
||||
|
||||
//
|
||||
@@ -74,8 +37,7 @@ class AuthorEditor extends Component {
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isOrganizingAuthorModalOpen: false,
|
||||
isRetaggingAuthorModalOpen: false,
|
||||
columns: getColumns(props.showMetadataProfile)
|
||||
isRetaggingAuthorModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,6 +117,7 @@ class AuthorEditor extends Component {
|
||||
error,
|
||||
totalItems,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
@@ -166,7 +129,7 @@ class AuthorEditor extends Component {
|
||||
deleteError,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
showMetadataProfile,
|
||||
onTableOptionChange,
|
||||
onSortPress,
|
||||
onFilterSelect
|
||||
} = this.props;
|
||||
@@ -174,8 +137,7 @@ class AuthorEditor extends Component {
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
columns
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const selectedAuthorIds = this.getSelectedIds();
|
||||
@@ -185,6 +147,18 @@ class AuthorEditor extends Component {
|
||||
<PageToolbar>
|
||||
<PageToolbarSection />
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
@@ -196,7 +170,7 @@ class AuthorEditor extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -243,7 +217,7 @@ class AuthorEditor extends Component {
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoAuthor totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<AuthorEditorFooter
|
||||
authorIds={selectedAuthorIds}
|
||||
@@ -254,7 +228,8 @@ class AuthorEditor extends Component {
|
||||
deleteError={deleteError}
|
||||
isOrganizingAuthor={isOrganizingAuthor}
|
||||
isRetaggingAuthor={isRetaggingAuthor}
|
||||
showMetadataProfile={showMetadataProfile}
|
||||
columns={columns}
|
||||
showMetadataProfile={columns.find((column) => column.name === 'metadataProfileId').isVisible}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
|
||||
onRetagAuthorPress={this.onRetagAuthorPress}
|
||||
@@ -283,6 +258,7 @@ AuthorEditor.propTypes = {
|
||||
error: PropTypes.object,
|
||||
totalItems: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
@@ -294,7 +270,7 @@ AuthorEditor.propTypes = {
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingAuthor: PropTypes.bool.isRequired,
|
||||
isRetaggingAuthor: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort } from 'Store/Actions/authorEditorActions';
|
||||
import { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort, setAuthorEditorTableOption } from 'Store/Actions/authorEditorActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/settingsActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
@@ -12,15 +12,13 @@ import AuthorEditor from './AuthorEditor';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.metadataProfiles,
|
||||
createClientSideCollectionSelector('authors', 'authorEditor'),
|
||||
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
|
||||
(metadataProfiles, author, isOrganizingAuthor, isRetaggingAuthor) => {
|
||||
(author, isOrganizingAuthor, isRetaggingAuthor) => {
|
||||
return {
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
showMetadataProfile: metadataProfiles.items.length > 1,
|
||||
...author
|
||||
};
|
||||
}
|
||||
@@ -30,6 +28,7 @@ function createMapStateToProps() {
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetAuthorEditorSort: setAuthorEditorSort,
|
||||
dispatchSetAuthorEditorFilter: setAuthorEditorFilter,
|
||||
dispatchSetAuthorEditorTableOption: setAuthorEditorTableOption,
|
||||
dispatchSaveAuthorEditor: saveAuthorEditor,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
@@ -55,6 +54,10 @@ class AuthorEditorConnector extends Component {
|
||||
this.props.dispatchSetAuthorEditorFilter({ selectedFilterKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.dispatchSetAuthorEditorTableOption(payload);
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveAuthorEditor(payload);
|
||||
}
|
||||
@@ -76,6 +79,7 @@ class AuthorEditorConnector extends Component {
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -84,6 +88,7 @@ class AuthorEditorConnector extends Component {
|
||||
AuthorEditorConnector.propTypes = {
|
||||
dispatchSetAuthorEditorSort: PropTypes.func.isRequired,
|
||||
dispatchSetAuthorEditorFilter: PropTypes.func.isRequired,
|
||||
dispatchSetAuthorEditorTableOption: PropTypes.func.isRequired,
|
||||
dispatchSaveAuthorEditor: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||
|
||||
@@ -138,7 +138,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDeleting,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
showMetadataProfile,
|
||||
columns,
|
||||
onOrganizeAuthorPress,
|
||||
onRetagAuthorPress
|
||||
} = this.props;
|
||||
@@ -147,6 +147,7 @@ class AuthorEditorFooter extends Component {
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
bookFolder,
|
||||
rootFolderPath,
|
||||
savingTags,
|
||||
isTagsModalOpen,
|
||||
@@ -161,6 +162,12 @@ class AuthorEditorFooter extends Component {
|
||||
{ key: 'unmonitored', value: 'Unmonitored' }
|
||||
];
|
||||
|
||||
const bookFolderOptions = [
|
||||
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
||||
{ key: 'yes', value: 'Yes' },
|
||||
{ key: 'no', value: 'No' }
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
@@ -178,56 +185,110 @@ class AuthorEditorFooter extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Quality Profile"
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
showMetadataProfile &&
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Metadata Profile"
|
||||
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
|
||||
/>
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
<MetadataProfileSelectInputConnector
|
||||
name="metadataProfileId"
|
||||
value={metadataProfileId}
|
||||
includeNoChange={true}
|
||||
includeNone={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Quality Profile"
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'metadataProfileId') {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Metadata Profile"
|
||||
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<MetadataProfileSelectInputConnector
|
||||
name="metadataProfileId"
|
||||
value={metadataProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'bookFolder') {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Book Folder"
|
||||
isSaving={isSaving && bookFolder !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
name="bookFolder"
|
||||
value={bookFolder}
|
||||
values={bookFolderOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Root Folder"
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Root Folder"
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<AuthorEditorFooterLabel
|
||||
@@ -315,6 +376,7 @@ AuthorEditorFooter.propTypes = {
|
||||
isOrganizingAuthor: PropTypes.bool.isRequired,
|
||||
isRetaggingAuthor: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired,
|
||||
onOrganizeAuthorPress: PropTypes.func.isRequired,
|
||||
onRetagAuthorPress: PropTypes.func.isRequired
|
||||
|
||||
5
frontend/src/Author/Editor/AuthorEditorRow.css
Normal file
5
frontend/src/Author/Editor/AuthorEditorRow.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.bookFolder {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||
import AuthorStatusCell from 'Author/Index/Table/AuthorStatusCell';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import styles from './AuthorEditorRow.css';
|
||||
|
||||
class AuthorEditorRow extends Component {
|
||||
|
||||
@@ -25,16 +27,20 @@ class AuthorEditorRow extends Component {
|
||||
const {
|
||||
id,
|
||||
status,
|
||||
titleSlug,
|
||||
foreignAuthorId,
|
||||
authorName,
|
||||
authorType,
|
||||
bookFolder,
|
||||
monitored,
|
||||
metadataProfile,
|
||||
qualityProfile,
|
||||
path,
|
||||
statistics,
|
||||
tags,
|
||||
columns,
|
||||
isSaving,
|
||||
isSelected,
|
||||
onAuthorMonitoredPress,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
@@ -46,39 +52,105 @@ class AuthorEditorRow extends Component {
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<AuthorStatusCell
|
||||
authorType={authorType}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
/>
|
||||
|
||||
<TableRowCell>
|
||||
<AuthorNameLink
|
||||
titleSlug={titleSlug}
|
||||
authorName={authorName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{qualityProfile.name}
|
||||
</TableRowCell>
|
||||
|
||||
{
|
||||
_.find(columns, { name: 'metadataProfileId' }).isVisible &&
|
||||
<TableRowCell>
|
||||
{metadataProfile.name}
|
||||
</TableRowCell>
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<AuthorStatusCell
|
||||
key={name}
|
||||
authorType={authorType}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
isSaving={isSaving}
|
||||
onMonitoredPress={onAuthorMonitoredPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sortName') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.title}
|
||||
>
|
||||
<AuthorNameLink
|
||||
foreignAuthorId={foreignAuthorId}
|
||||
authorName={authorName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{qualityProfile.name}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'metadataProfileId') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{metadataProfile.name}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'bookFolder') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.bookFolder}
|
||||
>
|
||||
<CheckInput
|
||||
name="bookFolder"
|
||||
value={bookFolder}
|
||||
isDisabled={true}
|
||||
onChange={this.onBookFolderChange}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{path}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sizeOnDisk') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{formatBytes(statistics.sizeOnDisk)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'tags') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<TagListConnector
|
||||
tags={tags}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<TableRowCell>
|
||||
{path}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<TagListConnector
|
||||
tags={tags}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -87,16 +159,21 @@ class AuthorEditorRow extends Component {
|
||||
AuthorEditorRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
foreignAuthorId: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
authorType: PropTypes.string,
|
||||
bookFolder: PropTypes.string,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
metadataProfile: PropTypes.object.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onAuthorMonitoredPress: PropTypes.func.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ function OrganizeAuthorModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
<Alert>
|
||||
Tip: To preview a rename... select "Cancel" then click any author name and use the
|
||||
Tip: To preview a rename, select "Cancel", then select any artist name and use the
|
||||
<Icon
|
||||
className={styles.renameIcon}
|
||||
name={icons.ORGANIZE}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
.sourceTitle {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.details,
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
@@ -94,7 +94,7 @@ class AuthorHistoryRow extends Component {
|
||||
{book.title}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<TableRowCell className={styles.sourceTitle}>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
||||
import NoAuthor from 'Author/NoAuthor';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
@@ -312,7 +312,7 @@ class AuthorIndex extends Component {
|
||||
</PageToolbar>
|
||||
|
||||
<div className={styles.pageContentBodyWrapper}>
|
||||
<PageContentBodyConnector
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
className={styles.contentBody}
|
||||
innerClassName={styles[`${view}InnerContentBody`]}
|
||||
@@ -351,7 +351,7 @@ class AuthorIndex extends Component {
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoAuthor totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
isLoaded && !!jumpBarItems.order.length &&
|
||||
|
||||
@@ -8,8 +8,7 @@ import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createAuthorClientSideCollectionItemsSelector
|
||||
from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
|
||||
import createAuthorClientSideCollectionItemsSelector from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import AuthorIndex from './AuthorIndex';
|
||||
|
||||
@@ -11,7 +11,6 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
import AuthorIndexOverviewInfo from './AuthorIndexOverviewInfo';
|
||||
import styles from './AuthorIndexOverview.css';
|
||||
|
||||
@@ -205,7 +204,7 @@ class AuthorIndexOverview extends Component {
|
||||
>
|
||||
<TextTruncate
|
||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||
text={stripHtml(overview)}
|
||||
text={overview}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
$hoverScale: 1.05;
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
transition: all 200ms ease-in;
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class AuthorIndexPoster extends Component {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer}>
|
||||
<Label className={styles.controls}>
|
||||
|
||||
@@ -105,6 +105,7 @@ class AuthorIndexPosters extends Component {
|
||||
|
||||
this._isInitialized = false;
|
||||
this._grid = null;
|
||||
this._padding = props.isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@@ -112,7 +113,9 @@ class AuthorIndexPosters extends Component {
|
||||
items,
|
||||
sortKey,
|
||||
posterOptions,
|
||||
jumpToCharacter
|
||||
jumpToCharacter,
|
||||
scrollTop,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -124,7 +127,7 @@ class AuthorIndexPosters extends Component {
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
prevProps.posterOptions !== posterOptions) {
|
||||
this.calculateGrid();
|
||||
this.calculateGrid(width, isSmallScreen);
|
||||
}
|
||||
|
||||
if (this._grid &&
|
||||
@@ -149,6 +152,10 @@ class AuthorIndexPosters extends Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0) {
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -164,10 +171,9 @@ class AuthorIndexPosters extends Component {
|
||||
posterOptions
|
||||
} = this.props;
|
||||
|
||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen);
|
||||
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
|
||||
const posterWidth = columnWidth - padding;
|
||||
const posterWidth = columnWidth - this._padding * 2;
|
||||
const posterHeight = calculatePosterHeight(posterWidth);
|
||||
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions);
|
||||
|
||||
@@ -214,7 +220,10 @@ class AuthorIndexPosters extends Component {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={style}
|
||||
style={{
|
||||
...style,
|
||||
padding: this._padding
|
||||
}}
|
||||
>
|
||||
<AuthorIndexItemConnector
|
||||
key={author.id}
|
||||
@@ -229,6 +238,7 @@ class AuthorIndexPosters extends Component {
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
style={style}
|
||||
authorId={author.id}
|
||||
qualityProfileId={author.qualityProfileId}
|
||||
metadataProfileId={author.metadataProfileId}
|
||||
@@ -249,9 +259,9 @@ class AuthorIndexPosters extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
scroller,
|
||||
items,
|
||||
isSmallScreen,
|
||||
scroller
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -310,6 +320,7 @@ AuthorIndexPosters.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
posterOptions: PropTypes.object.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 auto;
|
||||
@@ -83,6 +84,8 @@
|
||||
}
|
||||
|
||||
.bookNavigationButtons {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import IconButton from 'Components/Link/IconButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
@@ -29,7 +29,6 @@ import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
import BookDetailsLinks from './BookDetailsLinks';
|
||||
import styles from './BookDetails.css';
|
||||
|
||||
@@ -197,7 +196,7 @@ class BookDetails extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector innerClassName={styles.innerContentBody}>
|
||||
<PageContentBody innerClassName={styles.innerContentBody}>
|
||||
<div className={styles.header}>
|
||||
<div
|
||||
className={styles.backdrop}
|
||||
@@ -361,7 +360,7 @@ class BookDetails extends Component {
|
||||
<div className={styles.overview}>
|
||||
<TextTruncate
|
||||
line={Math.floor(125 / (defaultFontSize * lineHeight))}
|
||||
text={stripHtml(overview)}
|
||||
text={overview}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -467,7 +466,7 @@ class BookDetails extends Component {
|
||||
onModalClose={this.onDeleteBookModalClose}
|
||||
/>
|
||||
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createSelector } from 'reselect';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
|
||||
import BookDetailsConnector from './BookDetailsConnector';
|
||||
|
||||
@@ -103,9 +103,9 @@ class BookDetailsPageConnector extends Component {
|
||||
(!isFetching && !isPopulated)) {
|
||||
return (
|
||||
<PageContent title='loading'>
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<LoadingIndicator />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ function BookFileEditorRow(props) {
|
||||
id,
|
||||
path,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = props;
|
||||
@@ -28,6 +29,7 @@ function BookFileEditorRow(props) {
|
||||
<TableRowCell>
|
||||
<BookQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
@@ -38,6 +40,7 @@ BookFileEditorRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import NoAuthor from 'Author/NoAuthor';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
@@ -363,7 +363,7 @@ class Bookshelf extends Component {
|
||||
</PageToolbar>
|
||||
|
||||
<div className={styles.pageContentBodyWrapper}>
|
||||
<PageContentBodyConnector
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
className={styles.contentBody}
|
||||
innerClassName={styles.innerContentBody}
|
||||
@@ -414,7 +414,7 @@ class Bookshelf extends Component {
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoAuthor totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
isPopulated && !!jumpBarItems.order.length &&
|
||||
|
||||
@@ -5,7 +5,7 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Measure from 'Components/Measure';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
@@ -130,7 +130,7 @@ class CalendarPage extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector
|
||||
<PageContentBody
|
||||
className={styles.calendarPageBody}
|
||||
innerClassName={styles.calendarInnerPageBody}
|
||||
>
|
||||
@@ -171,7 +171,7 @@ class CalendarPage extends Component {
|
||||
hasAuthor && !!authorError &&
|
||||
<LegendConnector />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<CalendarLinkModal
|
||||
isOpen={isCalendarLinkModalOpen}
|
||||
|
||||
@@ -2,13 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearOptions, fetchOptions } from 'Store/Actions/providerOptionActions';
|
||||
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
|
||||
import DeviceInput from './DeviceInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
(state) => state.providerOptions,
|
||||
(state) => state.providerOptions.devices || defaultState,
|
||||
(value, devices) => {
|
||||
|
||||
return {
|
||||
@@ -51,7 +51,7 @@ class DeviceInputConnector extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this.props.dispatchClearOptions();
|
||||
this.props.dispatchClearOptions({ section: 'devices' });
|
||||
}
|
||||
|
||||
//
|
||||
@@ -65,6 +65,7 @@ class DeviceInputConnector extends Component {
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchOptions({
|
||||
section: 'devices',
|
||||
action: 'getDevices',
|
||||
provider,
|
||||
providerData
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
.enhancedSelect {
|
||||
composes: input from '~Components/Form/Input.css';
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
}
|
||||
|
||||
.editableContainer {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
||||
color: $black;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
@@ -33,6 +26,16 @@
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.dropdownArrowContainerEditable {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding-right: 17px;
|
||||
width: 30%;
|
||||
height: 35px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dropdownArrowContainerDisabled {
|
||||
composes: dropdownArrowContainer;
|
||||
|
||||
@@ -76,3 +79,8 @@
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
margin: 5px -5px 5px 0;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Measure from 'Components/Measure';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
@@ -16,6 +17,7 @@ import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './EnhancedSelectInput.css';
|
||||
|
||||
function isArrowKey(keyCode) {
|
||||
@@ -58,11 +60,30 @@ function getSelectedIndex(props) {
|
||||
values
|
||||
} = props;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return values.findIndex((v) => {
|
||||
return value.size && v.key === value[0];
|
||||
});
|
||||
}
|
||||
|
||||
return values.findIndex((v) => {
|
||||
return v.key === value;
|
||||
});
|
||||
}
|
||||
|
||||
function isSelectedItem(index, props) {
|
||||
const {
|
||||
value,
|
||||
values
|
||||
} = props;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.includes(values[index].key);
|
||||
}
|
||||
|
||||
return values[index].key === value;
|
||||
}
|
||||
|
||||
function getKey(selectedIndex, values) {
|
||||
return values[selectedIndex].key;
|
||||
}
|
||||
@@ -92,7 +113,7 @@ class EnhancedSelectInput extends Component {
|
||||
this._scheduleUpdate();
|
||||
}
|
||||
|
||||
if (prevProps.value !== this.props.value) {
|
||||
if (!Array.isArray(this.props.value) && prevProps.value !== this.props.value) {
|
||||
this.setState({
|
||||
selectedIndex: getSelectedIndex(this.props)
|
||||
});
|
||||
@@ -134,7 +155,7 @@ class EnhancedSelectInput extends Component {
|
||||
const button = document.getElementById(this._buttonId);
|
||||
const options = document.getElementById(this._optionsId);
|
||||
|
||||
if (!button || this.state.isMobile) {
|
||||
if (!button || !event.target.isConnected || this.state.isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -149,11 +170,21 @@ class EnhancedSelectInput extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
if (this.state.isOpen) {
|
||||
this._removeListener();
|
||||
this.setState({ isOpen: false });
|
||||
}
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
||||
const origIndex = getSelectedIndex(this.props);
|
||||
if (origIndex !== this.state.selectedIndex) {
|
||||
this.setState({ selectedIndex: origIndex });
|
||||
if (!this.props.isEditable) {
|
||||
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
||||
const origIndex = getSelectedIndex(this.props);
|
||||
|
||||
if (origIndex !== this.state.selectedIndex) {
|
||||
this.setState({ selectedIndex: origIndex });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +208,7 @@ class EnhancedSelectInput extends Component {
|
||||
}
|
||||
|
||||
if (
|
||||
selectedIndex == null ||
|
||||
selectedIndex == null || selectedIndex === -1 ||
|
||||
getSelectedOption(selectedIndex, values).isDisabled
|
||||
) {
|
||||
if (keyCode === keyCodes.UP_ARROW) {
|
||||
@@ -231,16 +262,35 @@ class EnhancedSelectInput extends Component {
|
||||
this._addListener();
|
||||
}
|
||||
|
||||
if (!this.state.isOpen && this.props.onOpen) {
|
||||
this.props.onOpen();
|
||||
}
|
||||
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
|
||||
onSelect = (value) => {
|
||||
this.setState({ isOpen: false });
|
||||
if (Array.isArray(this.props.value)) {
|
||||
let newValue = null;
|
||||
const index = this.props.value.indexOf(value);
|
||||
if (index === -1) {
|
||||
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
|
||||
} else {
|
||||
newValue = [...this.props.value];
|
||||
newValue.splice(index, 1);
|
||||
}
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: newValue
|
||||
});
|
||||
} else {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value
|
||||
});
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
@@ -258,13 +308,19 @@ class EnhancedSelectInput extends Component {
|
||||
const {
|
||||
className,
|
||||
disabledClassName,
|
||||
name,
|
||||
value,
|
||||
values,
|
||||
isDisabled,
|
||||
isEditable,
|
||||
isFetching,
|
||||
hasError,
|
||||
hasWarning,
|
||||
valueOptions,
|
||||
selectedValueOptions,
|
||||
selectedValueComponent: SelectedValueComponent,
|
||||
optionComponent: OptionComponent
|
||||
optionComponent: OptionComponent,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -274,6 +330,7 @@ class EnhancedSelectInput extends Component {
|
||||
isMobile
|
||||
} = this.state;
|
||||
|
||||
const isMultiSelect = Array.isArray(value);
|
||||
const selectedOption = getSelectedOption(selectedIndex, values);
|
||||
|
||||
return (
|
||||
@@ -289,37 +346,94 @@ class EnhancedSelectInput extends Component {
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
isDisabled && disabledClassName
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<SelectedValueComponent
|
||||
{...selectedValueOptions}
|
||||
{...selectedOption}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{selectedOption ? selectedOption.value : null}
|
||||
</SelectedValueComponent>
|
||||
{
|
||||
isEditable ?
|
||||
<div
|
||||
className={styles.editableContainer}
|
||||
>
|
||||
<TextInput
|
||||
className={className}
|
||||
name={name}
|
||||
value={value}
|
||||
readOnly={isDisabled}
|
||||
hasError={hasError}
|
||||
hasWarning={hasWarning}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.dropdownArrowContainerEditable,
|
||||
isDisabled ?
|
||||
styles.dropdownArrowContainerDisabled :
|
||||
styles.dropdownArrowContainer)
|
||||
}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
|
||||
<div
|
||||
className={isDisabled ?
|
||||
styles.dropdownArrowContainerDisabled :
|
||||
styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
{
|
||||
!isFetching &&
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
}
|
||||
</Link>
|
||||
</div> :
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
isDisabled && disabledClassName
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<SelectedValueComponent
|
||||
value={value}
|
||||
values={values}
|
||||
{...selectedValueOptions}
|
||||
{...selectedOption}
|
||||
isDisabled={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
>
|
||||
{selectedOption ? selectedOption.value : null}
|
||||
</SelectedValueComponent>
|
||||
|
||||
<div
|
||||
className={isDisabled ?
|
||||
styles.dropdownArrowContainerDisabled :
|
||||
styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching &&
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
</Measure>
|
||||
</div>
|
||||
)}
|
||||
@@ -358,11 +472,18 @@ class EnhancedSelectInput extends Component {
|
||||
>
|
||||
{
|
||||
values.map((v, index) => {
|
||||
const hasParent = v.parentKey !== undefined;
|
||||
const depth = hasParent ? 1 : 0;
|
||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
||||
return (
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
id={v.key}
|
||||
isSelected={index === selectedIndex}
|
||||
depth={depth}
|
||||
isSelected={isSelectedItem(index, this.props)}
|
||||
isDisabled={parentSelected}
|
||||
isMultiSelect={isMultiSelect}
|
||||
{...valueOptions}
|
||||
{...v}
|
||||
isMobile={false}
|
||||
onSelect={this.onSelect}
|
||||
@@ -399,11 +520,18 @@ class EnhancedSelectInput extends Component {
|
||||
<Scroller className={styles.optionsModalScroller}>
|
||||
{
|
||||
values.map((v, index) => {
|
||||
const hasParent = v.parentKey !== undefined;
|
||||
const depth = hasParent ? 1 : 0;
|
||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
||||
return (
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
id={v.key}
|
||||
isSelected={index === selectedIndex}
|
||||
depth={depth}
|
||||
isSelected={isSelectedItem(index, this.props)}
|
||||
isMultiSelect={isMultiSelect}
|
||||
isDisabled={parentSelected}
|
||||
{...valueOptions}
|
||||
{...v}
|
||||
isMobile={true}
|
||||
onSelect={this.onSelect}
|
||||
@@ -426,14 +554,18 @@ EnhancedSelectInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
disabledClassName: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isEditable: PropTypes.bool.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
valueOptions: PropTypes.object.isRequired,
|
||||
selectedValueOptions: PropTypes.object.isRequired,
|
||||
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
optionComponent: PropTypes.elementType,
|
||||
onOpen: PropTypes.func,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -441,6 +573,9 @@ EnhancedSelectInput.defaultProps = {
|
||||
className: styles.enhancedSelect,
|
||||
disabledClassName: styles.isDisabled,
|
||||
isDisabled: false,
|
||||
isFetching: false,
|
||||
isEditable: false,
|
||||
valueOptions: {},
|
||||
selectedValueOptions: {},
|
||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||
optionComponent: HintedSelectInputOption
|
||||
|
||||
159
frontend/src/Components/Form/EnhancedSelectInputConnector.js
Normal file
159
frontend/src/Components/Form/EnhancedSelectInputConnector.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const importantFieldNames = [
|
||||
'baseUrl',
|
||||
'apiPath',
|
||||
'apiKey'
|
||||
];
|
||||
|
||||
function getProviderDataKey(providerData) {
|
||||
if (!providerData || !providerData.fields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fields = providerData.fields
|
||||
.filter((f) => importantFieldNames.includes(f.name))
|
||||
.map((f) => f.value);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
function getSelectOptions(items) {
|
||||
if (!items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return items.map((option) => {
|
||||
return {
|
||||
key: option.value,
|
||||
value: option.name,
|
||||
hint: option.hint,
|
||||
parentKey: option.parentValue
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { selectOptionsProviderAction }) => state.providerOptions[selectOptionsProviderAction] || defaultState,
|
||||
(options) => {
|
||||
if (options) {
|
||||
return {
|
||||
isFetching: options.isFetching,
|
||||
values: getSelectOptions(options.items)
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchOptions: fetchOptions,
|
||||
dispatchClearOptions: clearOptions
|
||||
};
|
||||
|
||||
class EnhancedSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
refetchRequired: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this._populate();
|
||||
}
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
const prevKey = getProviderDataKey(prevProps.providerData);
|
||||
const nextKey = getProviderDataKey(this.props.providerData);
|
||||
|
||||
if (!_.isEqual(prevKey, nextKey)) {
|
||||
this.setState({ refetchRequired: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOpen = () => {
|
||||
if (this.state.refetchRequired) {
|
||||
this._populate();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_populate() {
|
||||
const {
|
||||
provider,
|
||||
providerData,
|
||||
selectOptionsProviderAction,
|
||||
dispatchFetchOptions
|
||||
} = this.props;
|
||||
|
||||
if (selectOptionsProviderAction) {
|
||||
this.setState({ refetchRequired: false });
|
||||
dispatchFetchOptions({
|
||||
section: selectOptionsProviderAction,
|
||||
action: selectOptionsProviderAction,
|
||||
provider,
|
||||
providerData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
const {
|
||||
selectOptionsProviderAction,
|
||||
dispatchClearOptions
|
||||
} = this.props;
|
||||
|
||||
if (selectOptionsProviderAction) {
|
||||
dispatchClearOptions({ section: selectOptionsProviderAction });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onOpen={this.onOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EnhancedSelectInputConnector.propTypes = {
|
||||
provider: PropTypes.string.isRequired,
|
||||
providerData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectOptionsProviderAction: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
dispatchFetchOptions: PropTypes.func.isRequired,
|
||||
dispatchClearOptions: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EnhancedSelectInputConnector);
|
||||
@@ -11,6 +11,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.optionCheck {
|
||||
composes: container from '~./CheckInput.css';
|
||||
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
.optionCheckInput {
|
||||
composes: input from '~./CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.isSelected {
|
||||
background-color: #e2e2e2;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import CheckInput from './CheckInput';
|
||||
import styles from './EnhancedSelectInputOption.css';
|
||||
|
||||
class EnhancedSelectInputOption extends Component {
|
||||
@@ -20,15 +21,22 @@ class EnhancedSelectInputOption extends Component {
|
||||
onSelect(id);
|
||||
}
|
||||
|
||||
onCheckPress = () => {
|
||||
// CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation.
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
id,
|
||||
depth,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isHidden,
|
||||
isMultiSelect,
|
||||
isMobile,
|
||||
children
|
||||
} = this.props;
|
||||
@@ -37,8 +45,8 @@ class EnhancedSelectInputOption extends Component {
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
isSelected && styles.isSelected,
|
||||
isDisabled && styles.isDisabled,
|
||||
isSelected && !isMultiSelect && styles.isSelected,
|
||||
isDisabled && !isMultiSelect && styles.isDisabled,
|
||||
isHidden && styles.isHidden,
|
||||
isMobile && styles.isMobile
|
||||
)}
|
||||
@@ -46,6 +54,24 @@ class EnhancedSelectInputOption extends Component {
|
||||
isDisabled={isDisabled}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
|
||||
{
|
||||
depth !== 0 &&
|
||||
<div style={{ width: `${depth * 20}px` }} />
|
||||
}
|
||||
|
||||
{
|
||||
isMultiSelect &&
|
||||
<CheckInput
|
||||
className={styles.optionCheckInput}
|
||||
containerClassName={styles.optionCheck}
|
||||
name={`select-${id}`}
|
||||
value={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
onChange={this.onCheckPress}
|
||||
/>
|
||||
}
|
||||
|
||||
{children}
|
||||
|
||||
{
|
||||
@@ -63,10 +89,12 @@ class EnhancedSelectInputOption extends Component {
|
||||
|
||||
EnhancedSelectInputOption.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isHidden: PropTypes.bool.isRequired,
|
||||
isMultiSelect: PropTypes.bool.isRequired,
|
||||
isMobile: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
onSelect: PropTypes.func.isRequired
|
||||
@@ -74,8 +102,10 @@ EnhancedSelectInputOption.propTypes = {
|
||||
|
||||
EnhancedSelectInputOption.defaultProps = {
|
||||
className: styles.option,
|
||||
depth: 0,
|
||||
isDisabled: false,
|
||||
isHidden: false
|
||||
isHidden: false,
|
||||
isMultiSelect: false
|
||||
};
|
||||
|
||||
export default EnhancedSelectInputOption;
|
||||
|
||||
@@ -9,7 +9,9 @@ import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
import DeviceInputConnector from './DeviceInputConnector';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||
import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
|
||||
import MonitorBooksSelectInput from './MonitorBooksSelectInput';
|
||||
@@ -23,6 +25,7 @@ import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
||||
import TagInputConnector from './TagInputConnector';
|
||||
import TextInput from './TextInput';
|
||||
import TextTagInputConnector from './TextTagInputConnector';
|
||||
import UMaskInput from './UMaskInput';
|
||||
import styles from './FormInputGroup.css';
|
||||
|
||||
function getComponent(type) {
|
||||
@@ -69,12 +72,18 @@ function getComponent(type) {
|
||||
case inputTypes.BOOK_EDITION_SELECT:
|
||||
return BookEditionSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_SELECT:
|
||||
return IndexerSelectInputConnector;
|
||||
|
||||
case inputTypes.ROOT_FOLDER_SELECT:
|
||||
return RootFolderSelectInputConnector;
|
||||
|
||||
case inputTypes.SELECT:
|
||||
return EnhancedSelectInput;
|
||||
|
||||
case inputTypes.DYNAMIC_SELECT:
|
||||
return EnhancedSelectInputConnector;
|
||||
|
||||
case inputTypes.SERIES_TYPE_SELECT:
|
||||
return SeriesTypeSelectInput;
|
||||
|
||||
@@ -84,6 +93,9 @@ function getComponent(type) {
|
||||
case inputTypes.TEXT_TAG:
|
||||
return TextTagInputConnector;
|
||||
|
||||
case inputTypes.UMASK:
|
||||
return UMaskInput;
|
||||
|
||||
default:
|
||||
return TextInput;
|
||||
}
|
||||
@@ -191,7 +203,7 @@ function FormInputGroup(props) {
|
||||
}
|
||||
|
||||
{
|
||||
!checkInput && helpTextWarning &&
|
||||
(!checkInput || helpText) && helpTextWarning &&
|
||||
<FormInputHelpText
|
||||
text={helpTextWarning}
|
||||
isWarning={true}
|
||||
@@ -214,7 +226,7 @@ function FormInputGroup(props) {
|
||||
key={index}
|
||||
text={error.message}
|
||||
link={error.link}
|
||||
linkTooltip={error.detailedMessage}
|
||||
tooltip={error.detailedMessage}
|
||||
isError={true}
|
||||
isCheckInput={checkInput}
|
||||
/>
|
||||
@@ -229,7 +241,7 @@ function FormInputGroup(props) {
|
||||
key={index}
|
||||
text={warning.message}
|
||||
link={warning.link}
|
||||
linkTooltip={warning.detailedMessage}
|
||||
tooltip={warning.detailedMessage}
|
||||
isWarning={true}
|
||||
isCheckInput={checkInput}
|
||||
/>
|
||||
|
||||
@@ -37,3 +37,7 @@
|
||||
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ function FormInputHelpText(props) {
|
||||
className,
|
||||
text,
|
||||
link,
|
||||
linkTooltip,
|
||||
tooltip,
|
||||
isError,
|
||||
isWarning,
|
||||
isCheckInput
|
||||
@@ -28,16 +28,27 @@ function FormInputHelpText(props) {
|
||||
{text}
|
||||
|
||||
{
|
||||
!!link &&
|
||||
link ?
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={link}
|
||||
title={linkTooltip}
|
||||
title={tooltip}
|
||||
>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
/>
|
||||
</Link>
|
||||
</Link> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!link && tooltip ?
|
||||
<Icon
|
||||
containerClassName={styles.details}
|
||||
name={icons.INFO}
|
||||
title={tooltip}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
@@ -47,7 +58,7 @@ FormInputHelpText.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
link: PropTypes.string,
|
||||
linkTooltip: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
isError: PropTypes.bool,
|
||||
isWarning: PropTypes.bool,
|
||||
isCheckInput: PropTypes.bool
|
||||
|
||||
@@ -6,14 +6,25 @@ import styles from './HintedSelectInputOption.css';
|
||||
|
||||
function HintedSelectInputOption(props) {
|
||||
const {
|
||||
id,
|
||||
value,
|
||||
hint,
|
||||
depth,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isMultiSelect,
|
||||
isMobile,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputOption
|
||||
id={id}
|
||||
depth={depth}
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
isHidden={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
isMobile={isMobile}
|
||||
{...otherProps}
|
||||
>
|
||||
@@ -36,9 +47,20 @@ function HintedSelectInputOption(props) {
|
||||
}
|
||||
|
||||
HintedSelectInputOption.propTypes = {
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
hint: PropTypes.node,
|
||||
depth: PropTypes.number,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isMultiSelect: PropTypes.bool.isRequired,
|
||||
isMobile: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
HintedSelectInputOption.defaultProps = {
|
||||
isDisabled: false,
|
||||
isHidden: false,
|
||||
isMultiSelect: false
|
||||
};
|
||||
|
||||
export default HintedSelectInputOption;
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
|
||||
import styles from './HintedSelectInputSelectedValue.css';
|
||||
|
||||
function HintedSelectInputSelectedValue(props) {
|
||||
const {
|
||||
value,
|
||||
values,
|
||||
hint,
|
||||
isMultiSelect,
|
||||
includeHint,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const valuesMap = isMultiSelect && _.keyBy(values, 'key');
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputSelectedValue
|
||||
className={styles.selectedValue}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.valueText}>
|
||||
{value}
|
||||
{
|
||||
isMultiSelect &&
|
||||
value.map((key, index) => {
|
||||
const v = valuesMap[key];
|
||||
return (
|
||||
<Label key={key}>
|
||||
{v ? v.value : key}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
!isMultiSelect && value
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -31,12 +51,15 @@ function HintedSelectInputSelectedValue(props) {
|
||||
}
|
||||
|
||||
HintedSelectInputSelectedValue.propTypes = {
|
||||
value: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hint: PropTypes.string,
|
||||
isMultiSelect: PropTypes.bool.isRequired,
|
||||
includeHint: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
HintedSelectInputSelectedValue.defaultProps = {
|
||||
isMultiSelect: false,
|
||||
includeHint: true
|
||||
};
|
||||
|
||||
|
||||
96
frontend/src/Components/Form/IndexerSelectInputConnector.js
Normal file
96
frontend/src/Components/Form/IndexerSelectInputConnector.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.indexers,
|
||||
(state, { includeAny }) => includeAny,
|
||||
(indexers, includeAny) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = indexers;
|
||||
|
||||
const values = _.map(items.sort(sortByName), (indexer) => {
|
||||
return {
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
};
|
||||
});
|
||||
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchIndexers: fetchIndexers
|
||||
};
|
||||
|
||||
class IndexerSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchIndexers();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
this.props.onChange({ name, value: parseInt(value) });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerSelectInputConnector.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeAny: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
IndexerSelectInputConnector.defaultProps = {
|
||||
includeAny: false
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);
|
||||
@@ -98,7 +98,9 @@ class KeyValueListInput extends Component {
|
||||
className,
|
||||
value,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder
|
||||
valuePlaceholder,
|
||||
hasError,
|
||||
hasWarning
|
||||
} = this.props;
|
||||
|
||||
const { isFocused } = this.state;
|
||||
@@ -106,7 +108,9 @@ class KeyValueListInput extends Component {
|
||||
return (
|
||||
<div className={classNames(
|
||||
className,
|
||||
isFocused && styles.isFocused
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning
|
||||
)}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
|
||||
function getType(type) {
|
||||
function getType({ type, selectOptionsProviderAction }) {
|
||||
switch (type) {
|
||||
case 'captcha':
|
||||
return inputTypes.CAPTCHA;
|
||||
@@ -25,6 +25,9 @@ function getType(type) {
|
||||
case 'filePath':
|
||||
return inputTypes.PATH;
|
||||
case 'select':
|
||||
if (selectOptionsProviderAction) {
|
||||
return inputTypes.DYNAMIC_SELECT;
|
||||
}
|
||||
return inputTypes.SELECT;
|
||||
case 'tag':
|
||||
return inputTypes.TEXT_TAG;
|
||||
@@ -45,7 +48,8 @@ function getSelectValues(selectOptions) {
|
||||
return _.reduce(selectOptions, (result, option) => {
|
||||
result.push({
|
||||
key: option.value,
|
||||
value: option.name
|
||||
value: option.name,
|
||||
hint: option.hint
|
||||
});
|
||||
|
||||
return result;
|
||||
@@ -86,7 +90,7 @@ function ProviderFieldFormGroup(props) {
|
||||
<FormLabel>{label}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={getType(type)}
|
||||
type={getType(props)}
|
||||
name={name}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
@@ -106,7 +110,8 @@ function ProviderFieldFormGroup(props) {
|
||||
|
||||
const selectOptionsShape = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.number.isRequired
|
||||
value: PropTypes.number.isRequired,
|
||||
hint: PropTypes.string
|
||||
};
|
||||
|
||||
ProviderFieldFormGroup.propTypes = {
|
||||
@@ -123,6 +128,7 @@ ProviderFieldFormGroup.propTypes = {
|
||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)),
|
||||
selectOptionsProviderAction: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.hasError {
|
||||
composes: hasError from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.hasWarning {
|
||||
composes: hasWarning from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.internalInput {
|
||||
flex: 1 1 0%;
|
||||
margin-left: 3px;
|
||||
|
||||
@@ -210,6 +210,8 @@ class TagInput extends Component {
|
||||
const {
|
||||
className,
|
||||
inputContainerClassName,
|
||||
hasError,
|
||||
hasWarning,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -226,7 +228,9 @@ class TagInput extends Component {
|
||||
className={styles.internalInput}
|
||||
inputContainerClassName={classNames(
|
||||
inputContainerClassName,
|
||||
isFocused && styles.isFocused
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning
|
||||
)}
|
||||
value={value}
|
||||
suggestions={suggestions}
|
||||
|
||||
53
frontend/src/Components/Form/UMaskInput.css
Normal file
53
frontend/src/Components/Form/UMaskInput.css
Normal file
@@ -0,0 +1,53 @@
|
||||
.inputWrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inputFolder {
|
||||
composes: input from '~Components/Form/Input.css';
|
||||
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.inputUnitWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputUnit {
|
||||
composes: inputUnit from '~Components/Form/FormInputGroup.css';
|
||||
|
||||
right: 40px;
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 5px;
|
||||
margin-left: 17px;
|
||||
line-height: 20px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
label {
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.unit {
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.readOnly {
|
||||
background-color: #eee;
|
||||
}
|
||||
133
frontend/src/Components/Form/UMaskInput.js
Normal file
133
frontend/src/Components/Form/UMaskInput.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import styles from './UMaskInput.css';
|
||||
|
||||
const umaskOptions = [
|
||||
{
|
||||
key: '755',
|
||||
value: '755 - Owner write, Everyone else read',
|
||||
hint: 'drwxr-xr-x'
|
||||
},
|
||||
{
|
||||
key: '775',
|
||||
value: '775 - Owner & Group write, Other read',
|
||||
hint: 'drwxrwxr-x'
|
||||
},
|
||||
{
|
||||
key: '770',
|
||||
value: '770 - Owner & Group write',
|
||||
hint: 'drwxrwx---'
|
||||
},
|
||||
{
|
||||
key: '750',
|
||||
value: '750 - Owner write, Group read',
|
||||
hint: 'drwxr-x---'
|
||||
},
|
||||
{
|
||||
key: '777',
|
||||
value: '777 - Everyone write',
|
||||
hint: 'drwxrwxrwx'
|
||||
}
|
||||
];
|
||||
|
||||
function formatPermissions(permissions) {
|
||||
|
||||
const hasSticky = permissions & 0o1000;
|
||||
const hasSetGID = permissions & 0o2000;
|
||||
const hasSetUID = permissions & 0o4000;
|
||||
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const bit = (permissions & (1 << i)) !== 0;
|
||||
let digit = bit ? 'xwr'[i % 3] : '-';
|
||||
if (i === 6 && hasSetUID) {
|
||||
digit = bit ? 's' : 'S';
|
||||
} else if (i === 3 && hasSetGID) {
|
||||
digit = bit ? 's' : 'S';
|
||||
} else if (i === 0 && hasSticky) {
|
||||
digit = bit ? 't' : 'T';
|
||||
}
|
||||
result = digit + result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
class UMaskInput extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const valueNum = parseInt(value, 8);
|
||||
const umaskNum = 0o777 & ~valueNum;
|
||||
const umask = umaskNum.toString(8).padStart(4, '0');
|
||||
const folderNum = 0o777 & ~umaskNum;
|
||||
const folder = folderNum.toString(8).padStart(3, '0');
|
||||
const fileNum = 0o666 & ~umaskNum;
|
||||
const file = fileNum.toString(8).padStart(3, '0');
|
||||
|
||||
const unit = formatPermissions(folderNum);
|
||||
|
||||
const values = umaskOptions.map((v) => {
|
||||
return { ...v, hint: <span className={styles.unit}>{v.hint}</span> };
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.inputWrapper}>
|
||||
<div className={styles.inputUnitWrapper}>
|
||||
<EnhancedSelectInput
|
||||
name={name}
|
||||
value={value}
|
||||
values={values}
|
||||
isEditable={true}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<div className={styles.inputUnit}>
|
||||
d{unit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
<label>UMask</label>
|
||||
<div className={styles.value}>{umask}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>Folder</label>
|
||||
<div className={styles.value}>{folder}</div>
|
||||
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>File</label>
|
||||
<div className={styles.value}>{file}</div>
|
||||
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UMaskInput.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func
|
||||
};
|
||||
|
||||
export default UMaskInput;
|
||||
@@ -1,12 +1,15 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import styles from './ToolbarMenuButton.css';
|
||||
|
||||
function ToolbarMenuButton(props) {
|
||||
const {
|
||||
iconName,
|
||||
indicator,
|
||||
text,
|
||||
...otherProps
|
||||
} = props;
|
||||
@@ -22,6 +25,21 @@ function ToolbarMenuButton(props) {
|
||||
size={21}
|
||||
/>
|
||||
|
||||
{
|
||||
indicator &&
|
||||
<span
|
||||
className={classNames(
|
||||
styles.indicatorContainer,
|
||||
'fa-layers fa-fw'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CIRCLE}
|
||||
size={9}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{text}
|
||||
@@ -34,7 +52,8 @@ function ToolbarMenuButton(props) {
|
||||
|
||||
ToolbarMenuButton.propTypes = {
|
||||
iconName: PropTypes.object.isRequired,
|
||||
text: PropTypes.string
|
||||
text: PropTypes.string,
|
||||
indicator: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default ToolbarMenuButton;
|
||||
|
||||
@@ -3,11 +3,20 @@ import React, { Component } from 'react';
|
||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
||||
class PageContentBody extends Component {
|
||||
|
||||
//
|
||||
// Lifecyle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._isMobile = isMobileUtil();
|
||||
}
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -26,13 +35,12 @@ class PageContentBody extends Component {
|
||||
const {
|
||||
className,
|
||||
innerClassName,
|
||||
isSmallScreen,
|
||||
children,
|
||||
dispatch,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller;
|
||||
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
|
||||
|
||||
return (
|
||||
<ScrollerComponent
|
||||
@@ -52,7 +60,6 @@ class PageContentBody extends Component {
|
||||
PageContentBody.propTypes = {
|
||||
className: PropTypes.string,
|
||||
innerClassName: PropTypes.string,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
onScroll: PropTypes.func,
|
||||
dispatch: PropTypes.func
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
||||
import PageContentBodyConnector from './PageContentBodyConnector';
|
||||
import PageContentBody from './PageContentBody';
|
||||
import styles from './PageContentError.css';
|
||||
|
||||
function PageContentError(props) {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<ErrorBoundaryError
|
||||
{...props}
|
||||
message='There was an error loading this page'
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ class SignalRConnector extends Component {
|
||||
|
||||
this.connection.on('receiveMessage', this.onReceiveMessage);
|
||||
|
||||
this.connection.start().then(this.onConnected);
|
||||
this.connection.start().then(this.onStart, this.onStartFail);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -286,7 +286,19 @@ class SignalRConnector extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConnected = () => {
|
||||
onStartFail = (error) => {
|
||||
console.error('[signalR] failed to connect');
|
||||
console.error(error);
|
||||
|
||||
this.props.dispatchSetAppValue({
|
||||
isConnected: false,
|
||||
isReconnecting: false,
|
||||
isDisconnected: false,
|
||||
isRestarting: false
|
||||
});
|
||||
}
|
||||
|
||||
onStart = () => {
|
||||
console.debug('[signalR] connected');
|
||||
|
||||
this.props.dispatchSetAppValue({
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
@@ -54,9 +54,9 @@ class Tooltip extends Component {
|
||||
} else if ((/^bottom/).test(data.placement)) {
|
||||
data.styles.maxHeight = windowHeight - bottom - 20;
|
||||
} else if ((/^right/).test(data.placement)) {
|
||||
data.styles.maxWidth = windowWidth - right - 30;
|
||||
data.styles.maxWidth = windowWidth - right - 35;
|
||||
} else {
|
||||
data.styles.maxWidth = left - 30;
|
||||
data.styles.maxWidth = left - 35;
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
@@ -12,12 +12,15 @@ export const PATH = 'path';
|
||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
|
||||
export const BOOK_EDITION_SELECT = 'bookEditionSelect';
|
||||
export const INDEXER_SELECT = 'indexerSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const SELECT = 'select';
|
||||
export const SERIES_TYPE_SELECT = 'authorTypeSelect';
|
||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||
export const TAG = 'tag';
|
||||
export const TEXT = 'text';
|
||||
export const TEXT_TAG = 'textTag';
|
||||
export const UMASK = 'umask';
|
||||
|
||||
export const all = [
|
||||
AUTO_COMPLETE,
|
||||
@@ -34,10 +37,13 @@ export const all = [
|
||||
QUALITY_PROFILE_SELECT,
|
||||
METADATA_PROFILE_SELECT,
|
||||
BOOK_EDITION_SELECT,
|
||||
INDEXER_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
SELECT,
|
||||
DYNAMIC_SELECT,
|
||||
SERIES_TYPE_SELECT,
|
||||
TAG,
|
||||
TEXT,
|
||||
TEXT_TAG
|
||||
TEXT_TAG,
|
||||
UMASK
|
||||
];
|
||||
|
||||
@@ -93,7 +93,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
recentFolders.map((recentFolder) => {
|
||||
recentFolders.slice(0).reverse().map((recentFolder) => {
|
||||
return (
|
||||
<RecentFolderRow
|
||||
key={recentFolder.folder}
|
||||
|
||||
@@ -55,6 +55,7 @@ const columns = [
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds, sortDirections, tooltipPositions } from 'Helpers/Props';
|
||||
import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
|
||||
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
@@ -272,6 +272,7 @@ class InteractiveImportRow extends Component {
|
||||
isOpen={isSelectBookModalOpen}
|
||||
ids={[id]}
|
||||
authorId={author && author.id}
|
||||
sortDirection={sortDirections.ASCENDING}
|
||||
onModalClose={this.onSelectBookModalClose}
|
||||
/>
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, sortDirections } from 'Helpers/Props';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||
import InteractiveSearchRow from './InteractiveSearchRow';
|
||||
import styles from './InteractiveSearch.css';
|
||||
|
||||
@@ -87,16 +90,33 @@ function InteractiveSearch(props) {
|
||||
error,
|
||||
totalReleasesCount,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
type,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onSortPress,
|
||||
onFilterSelect,
|
||||
onGrabPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.filterMenuContainer}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
buttonComponent={PageMenuButton}
|
||||
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
|
||||
filterModalConnectorComponentProps={{ type }}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
isFetching ? <LoadingIndicator /> : null
|
||||
}
|
||||
@@ -171,12 +191,16 @@ InteractiveSearch.propTypes = {
|
||||
error: PropTypes.object,
|
||||
totalReleasesCount: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import AddNewAuthorSearchResultConnector from './Author/AddNewAuthorSearchResultConnector';
|
||||
import AddNewBookSearchResultConnector from './Book/AddNewBookSearchResultConnector';
|
||||
import styles from './AddNewItem.css';
|
||||
@@ -87,7 +88,7 @@ class AddNewItem extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title="Add New Item">
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<Icon
|
||||
@@ -122,8 +123,13 @@ class AddNewItem extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Failed to load search results, please try again.</div>
|
||||
!isFetching && !!error ?
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
Failed to load search results, please try again.
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
{
|
||||
@@ -182,7 +188,7 @@ class AddNewItem extends Component {
|
||||
}
|
||||
|
||||
<div />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,10 +34,20 @@
|
||||
|
||||
.content {
|
||||
flex: 0 1 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nameRow {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
@@ -47,6 +57,14 @@
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 1 0 auto;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.mbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
@@ -69,3 +87,10 @@
|
||||
margin-top: 20px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.titleRow {
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import Link from 'Components/Link/Link';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
import AddNewAuthorModal from './AddNewAuthorModal';
|
||||
import styles from './AddNewAuthorSearchResult.css';
|
||||
|
||||
@@ -113,44 +112,49 @@ class AddNewAuthorSearchResult extends Component {
|
||||
}
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.name}>
|
||||
{authorName}
|
||||
<div className={styles.nameRow}>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{authorName}
|
||||
|
||||
{
|
||||
!name.contains(year) && year ?
|
||||
<span className={styles.year}>
|
||||
({year})
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
{
|
||||
!authorName.contains(year) && year ?
|
||||
<span className={styles.year}>
|
||||
({year})
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
{
|
||||
!!disambiguation &&
|
||||
<span className={styles.year}>({disambiguation})</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
!!disambiguation &&
|
||||
<span className={styles.year}>({disambiguation})</span>
|
||||
}
|
||||
<div className={styles.icons}>
|
||||
{
|
||||
isExistingAuthor ?
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title="Already in your library"
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isExistingAuthor ?
|
||||
<Link
|
||||
className={styles.mbLink}
|
||||
to={`https://goodreads.com/author/show/${foreignAuthorId}`}
|
||||
onPress={this.onMBLinkPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title="Already in your library"
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.mbLink}
|
||||
to={`https://goodreads.com/author/show/${foreignAuthorId}`}
|
||||
onPress={this.onMBLinkPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.mbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={28}
|
||||
/>
|
||||
</Link>
|
||||
className={styles.mbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={28}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -186,7 +190,7 @@ class AddNewAuthorSearchResult extends Component {
|
||||
<TextTruncate
|
||||
truncateText="…"
|
||||
line={Math.floor(height / (defaultFontSize * lineHeight))}
|
||||
text={stripHtml(overview)}
|
||||
text={overview}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
||||
import styles from './AddNewBookModalContent.css';
|
||||
|
||||
@@ -94,7 +93,7 @@ class AddNewBookModalContent extends Component {
|
||||
<TextTruncate
|
||||
truncateText="…"
|
||||
line={8}
|
||||
text={stripHtml(overview)}
|
||||
text={overview}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
|
||||
@@ -34,24 +34,37 @@
|
||||
|
||||
.content {
|
||||
flex: 0 1 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.name {
|
||||
.titleRow {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.authorName {
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.year {
|
||||
margin-left: 10px;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 1 0 auto;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.mbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
@@ -74,3 +87,10 @@
|
||||
margin-top: 20px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.titleRow {
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import Link from 'Components/Link/Link';
|
||||
import { icons, sizes } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
import AddNewBookModal from './AddNewBookModal';
|
||||
import styles from './AddNewBookSearchResult.css';
|
||||
|
||||
@@ -112,52 +111,42 @@ class AddNewBookSearchResult extends Component {
|
||||
}
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.name}>
|
||||
{title}
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
|
||||
{
|
||||
!!disambiguation &&
|
||||
<span className={styles.year}>({disambiguation})</span>
|
||||
}
|
||||
{
|
||||
!!disambiguation &&
|
||||
<span className={styles.year}>({disambiguation})</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
isExistingBook ?
|
||||
<div className={styles.icons}>
|
||||
{
|
||||
isExistingBook ?
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title="Already in your library"
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.mbLink}
|
||||
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
|
||||
onPress={this.onTVDBLinkPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={20}
|
||||
title="Book already in your library"
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.mbLink}
|
||||
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
|
||||
onPress={this.onMBLinkPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.mbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={28}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className={styles.authorName}> By: {author.authorName}</span>
|
||||
|
||||
{
|
||||
isExistingAuthor ?
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={15}
|
||||
title="Author already in your library"
|
||||
/> :
|
||||
null
|
||||
}
|
||||
className={styles.mbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={28}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -186,7 +175,7 @@ class AddNewBookSearchResult extends Component {
|
||||
<TextTruncate
|
||||
truncateText="…"
|
||||
line={Math.floor(height / (defaultFontSize * lineHeight))}
|
||||
text={stripHtml(overview)}
|
||||
text={overview}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -77,7 +77,7 @@ class DownloadClientSettings extends Component {
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<DownloadClientsConnector />
|
||||
|
||||
<DownloadClientOptionsConnector
|
||||
@@ -86,7 +86,7 @@ class DownloadClientSettings extends Component {
|
||||
/>
|
||||
|
||||
<RemotePathMappingsConnector />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ function BackupSettings(props) {
|
||||
type={inputTypes.NUMBER}
|
||||
name="backupRetention"
|
||||
unit="days"
|
||||
helpText="Automatic backups older than the retention will be cleaned up automatically"
|
||||
helpText="Automatic backups older than the retention period will be cleaned up automatically"
|
||||
onChange={onInputChange}
|
||||
{...backupRetention}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import Form from 'Components/Form/Form';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import AnalyticSettings from './AnalyticSettings';
|
||||
@@ -26,8 +26,7 @@ const requiresRestartKeys = [
|
||||
'sslCertPassword',
|
||||
'authenticationMethod',
|
||||
'username',
|
||||
'password',
|
||||
'apiKey'
|
||||
'password'
|
||||
];
|
||||
|
||||
class GeneralSettings extends Component {
|
||||
@@ -47,9 +46,15 @@ class GeneralSettings extends Component {
|
||||
const {
|
||||
settings,
|
||||
isSaving,
|
||||
saveError
|
||||
saveError,
|
||||
isResettingApiKey
|
||||
} = this.props;
|
||||
|
||||
if (!isResettingApiKey && prevProps.isResettingApiKey) {
|
||||
this.setState({ isRestartRequiredModalOpen: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSaving || saveError || !prevProps.isSaving) {
|
||||
return;
|
||||
}
|
||||
@@ -113,7 +118,7 @@ class GeneralSettings extends Component {
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -176,7 +181,7 @@ class GeneralSettings extends Component {
|
||||
/>
|
||||
</Form>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isRestartRequiredModalOpen}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -73,10 +73,10 @@ class ImportListSettings extends Component {
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<ImportListsConnector />
|
||||
<ImportListsExclusionsConnector />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -76,14 +76,14 @@ class IndexerSettings extends Component {
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<IndexersConnector />
|
||||
|
||||
<IndexerOptionsConnector
|
||||
onChildMounted={this.onChildMounted}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
width: 290px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
@@ -12,6 +17,12 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.enabled {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -2,8 +2,9 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import EditIndexerModalConnector from './EditIndexerModalConnector';
|
||||
import styles from './Indexer.css';
|
||||
|
||||
@@ -47,6 +48,15 @@ class Indexer extends Component {
|
||||
this.props.onConfirmDeleteIndexer(this.props.id);
|
||||
}
|
||||
|
||||
onCloneIndexerPress = () => {
|
||||
const {
|
||||
id,
|
||||
onCloneIndexerPress
|
||||
} = this.props;
|
||||
|
||||
onCloneIndexerPress(id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -69,8 +79,17 @@ class Indexer extends Component {
|
||||
overlayContent={true}
|
||||
onPress={this.onEditIndexerPress}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title="Clone Profile"
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneIndexerPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
@@ -144,6 +163,7 @@ Indexer.propTypes = {
|
||||
supportsRss: PropTypes.bool.isRequired,
|
||||
supportsSearch: PropTypes.bool.isRequired,
|
||||
showPriority: PropTypes.bool.isRequired,
|
||||
onCloneIndexerPress: PropTypes.func.isRequired,
|
||||
onConfirmDeleteIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ class Indexers extends Component {
|
||||
this.setState({ isAddIndexerModalOpen: true });
|
||||
}
|
||||
|
||||
onCloneIndexerPress = (id) => {
|
||||
this.props.dispatchCloneIndexer({ id });
|
||||
this.setState({ isEditIndexerModalOpen: true });
|
||||
}
|
||||
|
||||
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
|
||||
this.setState({
|
||||
isAddIndexerModalOpen: false,
|
||||
@@ -48,6 +53,7 @@ class Indexers extends Component {
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
dispatchCloneIndexer,
|
||||
onConfirmDeleteIndexer,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -73,6 +79,7 @@ class Indexers extends Component {
|
||||
key={item.id}
|
||||
{...item}
|
||||
showPriority={showPriority}
|
||||
onCloneIndexerPress={this.onCloneIndexerPress}
|
||||
onConfirmDeleteIndexer={onConfirmDeleteIndexer}
|
||||
/>
|
||||
);
|
||||
@@ -111,6 +118,7 @@ Indexers.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchCloneIndexer: PropTypes.func.isRequired,
|
||||
onConfirmDeleteIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import Indexers from './Indexers';
|
||||
@@ -15,8 +15,9 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchIndexers,
|
||||
deleteIndexer
|
||||
dispatchFetchIndexers: fetchIndexers,
|
||||
dispatchDeleteIndexer: deleteIndexer,
|
||||
dispatchCloneIndexer: cloneIndexer
|
||||
};
|
||||
|
||||
class IndexersConnector extends Component {
|
||||
@@ -25,14 +26,14 @@ class IndexersConnector extends Component {
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchIndexers();
|
||||
this.props.dispatchFetchIndexers();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteIndexer = (id) => {
|
||||
this.props.deleteIndexer({ id });
|
||||
this.props.dispatchDeleteIndexer({ id });
|
||||
}
|
||||
|
||||
//
|
||||
@@ -49,8 +50,9 @@ class IndexersConnector extends Component {
|
||||
}
|
||||
|
||||
IndexersConnector.propTypes = {
|
||||
fetchIndexers: PropTypes.func.isRequired,
|
||||
deleteIndexer: PropTypes.func.isRequired
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired,
|
||||
dispatchDeleteIndexer: PropTypes.func.isRequired,
|
||||
dispatchCloneIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector);
|
||||
|
||||
@@ -7,7 +7,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import RemotePathMappingsConnector from 'Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
@@ -63,7 +63,7 @@ class MediaManagement extends Component {
|
||||
onSavePress={onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<RootFoldersConnector />
|
||||
<RemotePathMappingsConnector />
|
||||
<NamingConnector />
|
||||
@@ -372,7 +372,7 @@ class MediaManagement extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="setPermissionsLinux"
|
||||
helpText="Should chmod/chown be run when files are imported/renamed?"
|
||||
helpText="Should chmod be run when files are imported/renamed?"
|
||||
helpTextWarning="If you're unsure what these settings do, do not alter them."
|
||||
onChange={onInputChange}
|
||||
{...settings.setPermissionsLinux}
|
||||
@@ -383,46 +383,15 @@ class MediaManagement extends Component {
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>File chmod mode</FormLabel>
|
||||
<FormLabel>chmod Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="fileChmod"
|
||||
helpText="Octal, applied to media files when imported/renamed by Readarr"
|
||||
type={inputTypes.UMASK}
|
||||
name="chmodFolder"
|
||||
helpText="Octal, applied during import/rename to media folders and files (without execute bits)"
|
||||
helpTextWarning="This only works if the user running Lidarr is the owner of the file. It's better to ensure the download client sets the permissions properly."
|
||||
onChange={onInputChange}
|
||||
{...settings.fileChmod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Folder chmod mode</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="folderChmod"
|
||||
helpText="Octal, applied to author/book folders created by Readarr"
|
||||
values={fileDateOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.folderChmod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>chown User</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="chownUser"
|
||||
helpText="Username or uid. Use uid for remote file systems."
|
||||
values={fileDateOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.chownUser}
|
||||
{...settings.chmodFolder}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -436,6 +405,7 @@ class MediaManagement extends Component {
|
||||
type={inputTypes.TEXT}
|
||||
name="chownGroup"
|
||||
helpText="Group name or gid. Use gid for remote file systems."
|
||||
helpTextWarning="This only works if the user running Readarr is the owner of the file. It's better to ensure the download client uses the same group as Readarr."
|
||||
values={fileDateOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.chownGroup}
|
||||
@@ -445,7 +415,7 @@ class MediaManagement extends Component {
|
||||
}
|
||||
</Form>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import MetadatasConnector from './Metadata/MetadatasConnector';
|
||||
import MetadataProviderConnector from './MetadataProvider/MetadataProviderConnector';
|
||||
@@ -54,13 +54,13 @@ class MetadataSettings extends Component {
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<MetadataProviderConnector
|
||||
onChildMounted={this.onChildMounted}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
<MetadatasConnector />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import NotificationsConnector from './Notifications/NotificationsConnector';
|
||||
|
||||
@@ -11,9 +11,9 @@ function NotificationSettings() {
|
||||
showSave={false}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<NotificationsConnector />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
60
frontend/src/Settings/Profiles/Metadata/PrimaryTypeItem.js
Normal file
60
frontend/src/Settings/Profiles/Metadata/PrimaryTypeItem.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import styles from './TypeItem.css';
|
||||
|
||||
class PrimaryTypeItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAllowedChange = ({ value }) => {
|
||||
const {
|
||||
albumTypeId,
|
||||
onMetadataPrimaryTypeItemAllowedChange
|
||||
} = this.props;
|
||||
|
||||
onMetadataPrimaryTypeItemAllowedChange(albumTypeId, value);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
allowed
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.metadataProfileItem
|
||||
)}
|
||||
>
|
||||
<label
|
||||
className={styles.albumTypeName}
|
||||
>
|
||||
<CheckInput
|
||||
containerClassName={styles.checkContainer}
|
||||
name={name}
|
||||
value={allowed}
|
||||
onChange={this.onAllowedChange}
|
||||
/>
|
||||
{name}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PrimaryTypeItem.propTypes = {
|
||||
albumTypeId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
sortIndex: PropTypes.number.isRequired,
|
||||
onMetadataPrimaryTypeItemAllowedChange: PropTypes.func
|
||||
};
|
||||
|
||||
export default PrimaryTypeItem;
|
||||
87
frontend/src/Settings/Profiles/Metadata/PrimaryTypeItems.js
Normal file
87
frontend/src/Settings/Profiles/Metadata/PrimaryTypeItems.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import PrimaryTypeItem from './PrimaryTypeItem';
|
||||
import styles from './TypeItems.css';
|
||||
|
||||
class PrimaryTypeItems extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
metadataProfileItems,
|
||||
errors,
|
||||
warnings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<FormLabel>Primary Types</FormLabel>
|
||||
<div>
|
||||
|
||||
{
|
||||
errors.map((error, index) => {
|
||||
return (
|
||||
<FormInputHelpText
|
||||
key={index}
|
||||
text={error.message}
|
||||
isError={true}
|
||||
isCheckInput={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
warnings.map((warning, index) => {
|
||||
return (
|
||||
<FormInputHelpText
|
||||
key={index}
|
||||
text={warning.message}
|
||||
isWarning={true}
|
||||
isCheckInput={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.albumTypes}>
|
||||
{
|
||||
metadataProfileItems.map(({ allowed, albumType }, index) => {
|
||||
return (
|
||||
<PrimaryTypeItem
|
||||
key={albumType.id}
|
||||
albumTypeId={albumType.id}
|
||||
name={albumType.name}
|
||||
allowed={allowed}
|
||||
sortIndex={index}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}).reverse()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PrimaryTypeItems.propTypes = {
|
||||
metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||
formLabel: PropTypes.string
|
||||
};
|
||||
|
||||
PrimaryTypeItems.defaultProps = {
|
||||
errors: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
export default PrimaryTypeItems;
|
||||
60
frontend/src/Settings/Profiles/Metadata/ReleaseStatusItem.js
Normal file
60
frontend/src/Settings/Profiles/Metadata/ReleaseStatusItem.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import styles from './TypeItem.css';
|
||||
|
||||
class ReleaseStatusItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAllowedChange = ({ value }) => {
|
||||
const {
|
||||
albumTypeId,
|
||||
onMetadataReleaseStatusItemAllowedChange
|
||||
} = this.props;
|
||||
|
||||
onMetadataReleaseStatusItemAllowedChange(albumTypeId, value);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
allowed
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.metadataProfileItem
|
||||
)}
|
||||
>
|
||||
<label
|
||||
className={styles.albumTypeName}
|
||||
>
|
||||
<CheckInput
|
||||
containerClassName={styles.checkContainer}
|
||||
name={name}
|
||||
value={allowed}
|
||||
onChange={this.onAllowedChange}
|
||||
/>
|
||||
{name}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReleaseStatusItem.propTypes = {
|
||||
albumTypeId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
sortIndex: PropTypes.number.isRequired,
|
||||
onMetadataReleaseStatusItemAllowedChange: PropTypes.func
|
||||
};
|
||||
|
||||
export default ReleaseStatusItem;
|
||||
@@ -0,0 +1,87 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ReleaseStatusItem from './ReleaseStatusItem';
|
||||
import styles from './TypeItems.css';
|
||||
|
||||
class ReleaseStatusItems extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
metadataProfileItems,
|
||||
errors,
|
||||
warnings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<FormLabel>Release Statuses</FormLabel>
|
||||
<div>
|
||||
|
||||
{
|
||||
errors.map((error, index) => {
|
||||
return (
|
||||
<FormInputHelpText
|
||||
key={index}
|
||||
text={error.message}
|
||||
isError={true}
|
||||
isCheckInput={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
warnings.map((warning, index) => {
|
||||
return (
|
||||
<FormInputHelpText
|
||||
key={index}
|
||||
text={warning.message}
|
||||
isWarning={true}
|
||||
isCheckInput={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.albumTypes}>
|
||||
{
|
||||
metadataProfileItems.map(({ allowed, releaseStatus }, index) => {
|
||||
return (
|
||||
<ReleaseStatusItem
|
||||
key={releaseStatus.id}
|
||||
albumTypeId={releaseStatus.id}
|
||||
name={releaseStatus.name}
|
||||
allowed={allowed}
|
||||
sortIndex={index}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReleaseStatusItems.propTypes = {
|
||||
metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||
formLabel: PropTypes.string
|
||||
};
|
||||
|
||||
ReleaseStatusItems.defaultProps = {
|
||||
errors: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
export default ReleaseStatusItems;
|
||||
60
frontend/src/Settings/Profiles/Metadata/SecondaryTypeItem.js
Normal file
60
frontend/src/Settings/Profiles/Metadata/SecondaryTypeItem.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import styles from './TypeItem.css';
|
||||
|
||||
class SecondaryTypeItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAllowedChange = ({ value }) => {
|
||||
const {
|
||||
albumTypeId,
|
||||
onMetadataSecondaryTypeItemAllowedChange
|
||||
} = this.props;
|
||||
|
||||
onMetadataSecondaryTypeItemAllowedChange(albumTypeId, value);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
allowed
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.metadataProfileItem
|
||||
)}
|
||||
>
|
||||
<label
|
||||
className={styles.albumTypeName}
|
||||
>
|
||||
<CheckInput
|
||||
containerClassName={styles.checkContainer}
|
||||
name={name}
|
||||
value={allowed}
|
||||
onChange={this.onAllowedChange}
|
||||
/>
|
||||
{name}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SecondaryTypeItem.propTypes = {
|
||||
albumTypeId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
sortIndex: PropTypes.number.isRequired,
|
||||
onMetadataSecondaryTypeItemAllowedChange: PropTypes.func
|
||||
};
|
||||
|
||||
export default SecondaryTypeItem;
|
||||
@@ -0,0 +1,87 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SecondaryTypeItem from './SecondaryTypeItem';
|
||||
import styles from './TypeItems.css';
|
||||
|
||||
class SecondaryTypeItems extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
metadataProfileItems,
|
||||
errors,
|
||||
warnings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<FormLabel>Secondary Types</FormLabel>
|
||||
<div>
|
||||
|
||||
{
|
||||
errors.map((error, index) => {
|
||||
return (
|
||||
<FormInputHelpText
|
||||
key={index}
|
||||
text={error.message}
|
||||
isError={true}
|
||||
isCheckInput={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
warnings.map((warning, index) => {
|
||||
return (
|
||||
<FormInputHelpText
|
||||
key={index}
|
||||
text={warning.message}
|
||||
isWarning={true}
|
||||
isCheckInput={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.albumTypes}>
|
||||
{
|
||||
metadataProfileItems.map(({ allowed, albumType }, index) => {
|
||||
return (
|
||||
<SecondaryTypeItem
|
||||
key={albumType.id}
|
||||
albumTypeId={albumType.id}
|
||||
name={albumType.name}
|
||||
allowed={allowed}
|
||||
sortIndex={index}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SecondaryTypeItems.propTypes = {
|
||||
metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||
formLabel: PropTypes.string
|
||||
};
|
||||
|
||||
SecondaryTypeItems.defaultProps = {
|
||||
errors: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
export default SecondaryTypeItems;
|
||||
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
|
||||
import MetadataProfilesConnector from './Metadata/MetadataProfilesConnector';
|
||||
@@ -24,14 +24,14 @@ class Profiles extends Component {
|
||||
showSave={false}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<QualityProfilesConnector />
|
||||
<MetadataProfilesConnector />
|
||||
<DelayProfilesConnector />
|
||||
<ReleaseProfilesConnector />
|
||||
</DndProvider>
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,11 +30,13 @@ function EditReleaseProfileModalContent(props) {
|
||||
|
||||
const {
|
||||
id,
|
||||
enabled,
|
||||
required,
|
||||
ignored,
|
||||
preferred,
|
||||
includePreferredWhenRenaming,
|
||||
tags
|
||||
tags,
|
||||
indexerId
|
||||
} = item;
|
||||
|
||||
return (
|
||||
@@ -45,6 +47,18 @@ function EditReleaseProfileModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Enable Profile</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enabled"
|
||||
helpText="Check to enable release profile"
|
||||
{...enabled}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Must Contain</FormLabel>
|
||||
|
||||
@@ -99,9 +113,23 @@ function EditReleaseProfileModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includePreferredWhenRenaming"
|
||||
helpText="Include in {Preferred Words} renaming format"
|
||||
helpText={indexerId.value === 0 ? 'Include in {Preferred Words} renaming format' : 'Only supported when Indexer is set to (All)'}
|
||||
{...includePreferredWhenRenaming}
|
||||
onChange={onInputChange}
|
||||
isDisabled={indexerId.value !== 0}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Indexer</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.INDEXER_SELECT}
|
||||
name="indexerId"
|
||||
helpText="Specify what indexer the profile applies to"
|
||||
{...indexerId}
|
||||
includeAny={true}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import EditReleaseProfileModalContent from './EditReleaseProfileModalContent';
|
||||
|
||||
const newReleaseProfile = {
|
||||
enabled: true,
|
||||
required: '',
|
||||
ignored: '',
|
||||
preferred: [],
|
||||
includePreferredWhenRenaming: false,
|
||||
tags: []
|
||||
tags: [],
|
||||
indexerId: 0
|
||||
};
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
@@ -55,11 +56,14 @@ class ReleaseProfile extends Component {
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
enabled,
|
||||
required,
|
||||
ignored,
|
||||
preferred,
|
||||
tags,
|
||||
tagList
|
||||
indexerId,
|
||||
tagList,
|
||||
indexerList
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -67,6 +71,8 @@ class ReleaseProfile extends Component {
|
||||
isDeleteReleaseProfileModalOpen
|
||||
} = this.state;
|
||||
|
||||
const indexer = indexerId !== 0 && _.find(indexerList, { id: indexerId });
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.releaseProfile}
|
||||
@@ -92,6 +98,23 @@ class ReleaseProfile extends Component {
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
preferred.map((item) => {
|
||||
const isPreferred = item.value >= 0;
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={item.key}
|
||||
kind={isPreferred ? kinds.DEFAULT : kinds.WARNING}
|
||||
>
|
||||
{item.key} {isPreferred && '+'}{item.value}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
split(ignored).map((item) => {
|
||||
@@ -111,28 +134,33 @@ class ReleaseProfile extends Component {
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
preferred.map((item) => {
|
||||
const isPreferred = item.value >= 0;
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={item.key}
|
||||
kind={isPreferred ? kinds.DEFAULT : kinds.WARNING}
|
||||
>
|
||||
{item.key} {isPreferred && '+'}{item.value}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{
|
||||
!enabled &&
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
Disabled
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
indexer &&
|
||||
<Label
|
||||
kind={kinds.INFO}
|
||||
outline={true}
|
||||
>
|
||||
{indexer.name}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditReleaseProfileModalConnector
|
||||
id={id}
|
||||
isOpen={isEditReleaseProfileModalOpen}
|
||||
@@ -156,18 +184,23 @@ class ReleaseProfile extends Component {
|
||||
|
||||
ReleaseProfile.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
required: PropTypes.string.isRequired,
|
||||
ignored: PropTypes.string.isRequired,
|
||||
preferred: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerId: PropTypes.number.isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ReleaseProfile.defaultProps = {
|
||||
enabled: true,
|
||||
required: '',
|
||||
ignored: '',
|
||||
preferred: []
|
||||
preferred: [],
|
||||
indexerId: 0
|
||||
};
|
||||
|
||||
export default ReleaseProfile;
|
||||
|
||||
@@ -40,6 +40,7 @@ class ReleaseProfiles extends Component {
|
||||
const {
|
||||
items,
|
||||
tagList,
|
||||
indexerList,
|
||||
onConfirmDeleteReleaseProfile,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -69,6 +70,7 @@ class ReleaseProfiles extends Component {
|
||||
<ReleaseProfile
|
||||
key={item.id}
|
||||
tagList={tagList}
|
||||
indexerList={indexerList}
|
||||
{...item}
|
||||
onConfirmDeleteReleaseProfile={onConfirmDeleteReleaseProfile}
|
||||
/>
|
||||
@@ -92,6 +94,7 @@ ReleaseProfiles.propTypes = {
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -2,24 +2,28 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteReleaseProfile, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||
import { deleteReleaseProfile, fetchIndexers, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import ReleaseProfiles from './ReleaseProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.releaseProfiles,
|
||||
(state) => state.settings.indexers,
|
||||
createTagsSelector(),
|
||||
(releaseProfiles, tagList) => {
|
||||
(releaseProfiles, indexers, tagList) => {
|
||||
return {
|
||||
...releaseProfiles,
|
||||
tagList
|
||||
tagList,
|
||||
isIndexersPopulated: indexers.isPopulated,
|
||||
indexerList: indexers.items
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchIndexers,
|
||||
fetchReleaseProfiles,
|
||||
deleteReleaseProfile
|
||||
};
|
||||
@@ -31,6 +35,9 @@ class ReleaseProfilesConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchReleaseProfiles();
|
||||
if (!this.props.isIndexersPopulated) {
|
||||
this.props.fetchIndexers();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -54,8 +61,10 @@ class ReleaseProfilesConnector extends Component {
|
||||
}
|
||||
|
||||
ReleaseProfilesConnector.propTypes = {
|
||||
isIndexersPopulated: PropTypes.bool.isRequired,
|
||||
fetchReleaseProfiles: PropTypes.func.isRequired,
|
||||
deleteReleaseProfile: PropTypes.func.isRequired
|
||||
deleteReleaseProfile: PropTypes.func.isRequired,
|
||||
fetchIndexers: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector';
|
||||
|
||||
@@ -54,12 +54,12 @@ class Quality extends Component {
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<QualityDefinitionsConnector
|
||||
onChildMounted={this.onChildMounted}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from './SettingsToolbarConnector';
|
||||
import styles from './Settings.css';
|
||||
|
||||
@@ -12,7 +12,7 @@ function Settings() {
|
||||
hasPendingChanges={false}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/mediamanagement"
|
||||
@@ -133,7 +133,7 @@ function Settings() {
|
||||
<div className={styles.summary}>
|
||||
Calendar, date and color impaired options
|
||||
</div>
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ function findMatchingItems(ids, items) {
|
||||
});
|
||||
}
|
||||
|
||||
function createMatchingAuthorSelector() {
|
||||
function createUnorderedMatchingAuthorSelector() {
|
||||
return createSelector(
|
||||
(state, { authorIds }) => authorIds,
|
||||
createAllAuthorSelector(),
|
||||
@@ -17,6 +17,26 @@ function createMatchingAuthorSelector() {
|
||||
);
|
||||
}
|
||||
|
||||
function createMatchingAuthorSelector() {
|
||||
return createSelector(
|
||||
createUnorderedMatchingAuthorSelector(),
|
||||
(authors) => {
|
||||
return authors.sort((authorA, authorB) => {
|
||||
const sortNameA = authorA.sortName;
|
||||
const sortNameB = authorB.sortName;
|
||||
|
||||
if (sortNameA > sortNameB) {
|
||||
return 1;
|
||||
} else if (sortNameA < sortNameB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMatchingDelayProfilesSelector() {
|
||||
return createSelector(
|
||||
(state, { delayProfileIds }) => delayProfileIds,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import TagsConnector from './TagsConnector';
|
||||
|
||||
@@ -11,9 +11,9 @@ function TagSettings() {
|
||||
showSave={false}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<TagsConnector />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
|
||||
@@ -65,7 +65,7 @@ class UISettings extends Component {
|
||||
onSavePress={onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
@@ -176,7 +176,7 @@ class UISettings extends Component {
|
||||
</FieldSet>
|
||||
</Form>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import createTestProviderHandler, { createCancelTestProviderHandler } from 'Stor
|
||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
|
||||
//
|
||||
// Variables
|
||||
@@ -21,6 +23,7 @@ const section = 'settings.indexers';
|
||||
export const FETCH_INDEXERS = 'settings/indexers/fetchIndexers';
|
||||
export const FETCH_INDEXER_SCHEMA = 'settings/indexers/fetchIndexerSchema';
|
||||
export const SELECT_INDEXER_SCHEMA = 'settings/indexers/selectIndexerSchema';
|
||||
export const CLONE_INDEXER = 'settings/indexers/cloneIndexer';
|
||||
export const SET_INDEXER_VALUE = 'settings/indexers/setIndexerValue';
|
||||
export const SET_INDEXER_FIELD_VALUE = 'settings/indexers/setIndexerFieldValue';
|
||||
export const SAVE_INDEXER = 'settings/indexers/saveIndexer';
|
||||
@@ -36,6 +39,7 @@ export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
||||
export const fetchIndexers = createThunk(FETCH_INDEXERS);
|
||||
export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA);
|
||||
export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA);
|
||||
export const cloneIndexer = createAction(CLONE_INDEXER);
|
||||
|
||||
export const saveIndexer = createThunk(SAVE_INDEXER);
|
||||
export const cancelSaveIndexer = createThunk(CANCEL_SAVE_INDEXER);
|
||||
@@ -113,6 +117,30 @@ export default {
|
||||
|
||||
return selectedSchema;
|
||||
});
|
||||
},
|
||||
|
||||
[CLONE_INDEXER]: function(state, { payload }) {
|
||||
const id = payload.id;
|
||||
const newState = getSectionState(state, section);
|
||||
const item = newState.items.find((i) => i.id === id);
|
||||
|
||||
// Use selectedSchema so `createProviderSettingsSelector` works properly
|
||||
const selectedSchema = { ...item };
|
||||
delete selectedSchema.id;
|
||||
delete selectedSchema.name;
|
||||
|
||||
selectedSchema.fields = selectedSchema.fields.map((field) => {
|
||||
return { ...field };
|
||||
});
|
||||
|
||||
newState.selectedSchema = selectedSchema;
|
||||
|
||||
// Set the name in pendingChanges
|
||||
newState.pendingChanges = {
|
||||
name: `${item.name} - Copy`
|
||||
};
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,9 @@ export const filterPredicates = {
|
||||
|
||||
sizeOnDisk: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
const sizeOnDisk = item.statistics ? item.statistics.sizeOnDisk : 0;
|
||||
const sizeOnDisk = item.statistics && item.statistics.sizeOnDisk ?
|
||||
item.statistics.sizeOnDisk :
|
||||
0;
|
||||
|
||||
return predicate(sizeOnDisk, filterValue);
|
||||
}
|
||||
@@ -133,6 +135,12 @@ export const sortPredicates = {
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
sizeOnDisk: function(item) {
|
||||
const { statistics = {} } = item;
|
||||
|
||||
return statistics.sizeOnDisk || 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { set, updateItem } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
||||
|
||||
//
|
||||
// Variables
|
||||
@@ -30,6 +31,58 @@ export const defaultState = {
|
||||
filters,
|
||||
filterPredicates,
|
||||
|
||||
columns: [
|
||||
{
|
||||
name: 'status',
|
||||
columnLabel: 'Status',
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
},
|
||||
{
|
||||
name: 'sortName',
|
||||
label: 'Name',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
label: 'Quality Profile',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'metadataProfileId',
|
||||
label: 'Metadata Profile',
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'albumFolder',
|
||||
label: 'Album Folder',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sizeOnDisk',
|
||||
label: 'Size on Disk',
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
}
|
||||
],
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'monitored',
|
||||
@@ -65,6 +118,12 @@ export const defaultState = {
|
||||
label: 'Root Folder Path',
|
||||
type: filterBuilderTypes.EXACT
|
||||
},
|
||||
{
|
||||
name: 'sizeOnDisk',
|
||||
label: 'Size on Disk',
|
||||
type: filterBuilderTypes.NUMBER,
|
||||
valueType: filterBuilderValueTypes.BYTES
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
@@ -90,6 +149,7 @@ export const SET_AUTHOR_EDITOR_SORT = 'authorEditor/setAuthorEditorSort';
|
||||
export const SET_AUTHOR_EDITOR_FILTER = 'authorEditor/setAuthorEditorFilter';
|
||||
export const SAVE_AUTHOR_EDITOR = 'authorEditor/saveAuthorEditor';
|
||||
export const BULK_DELETE_AUTHOR = 'authorEditor/bulkDeleteAuthor';
|
||||
export const SET_AUTHOR_EDITOR_TABLE_OPTION = 'authorEditor/setAuthorEditorTableOption';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
@@ -98,6 +158,7 @@ export const setAuthorEditorSort = createAction(SET_AUTHOR_EDITOR_SORT);
|
||||
export const setAuthorEditorFilter = createAction(SET_AUTHOR_EDITOR_FILTER);
|
||||
export const saveAuthorEditor = createThunk(SAVE_AUTHOR_EDITOR);
|
||||
export const bulkDeleteAuthor = createThunk(BULK_DELETE_AUTHOR);
|
||||
export const setAuthorEditorTableOption = createAction(SET_AUTHOR_EDITOR_TABLE_OPTION);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
@@ -181,6 +242,7 @@ export const actionHandlers = handleThunks({
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[SET_AUTHOR_EDITOR_TABLE_OPTION]: createSetTableOptionReducer(section),
|
||||
[SET_AUTHOR_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||
[SET_AUTHOR_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)
|
||||
|
||||
|
||||
@@ -180,13 +180,7 @@ export const defaultState = {
|
||||
bookCount: function(item) {
|
||||
const { statistics = {} } = item;
|
||||
|
||||
return statistics.bookCount;
|
||||
},
|
||||
|
||||
sizeOnDisk: function(item) {
|
||||
const { statistics = {} } = item;
|
||||
|
||||
return statistics.sizeOnDisk;
|
||||
return statistics.bookCount || 0;
|
||||
},
|
||||
|
||||
ratings: function(item) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user