mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-16 21:16:24 -04:00
Compare commits
124 Commits
sonarr-pul
...
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
|
1. Fork Readarr
|
||||||
2. Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories)
|
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`
|
3. Grab the submodules `git submodule init && git submodule update`
|
||||||
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
|
4. Install the required Node Packages `yarn install`
|
||||||
5. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `netcoreapp31`
|
5. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
|
||||||
6. Debug the project in Visual Studio
|
6. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `netcoreapp31`
|
||||||
7. Open http://localhost:8787
|
7. Debug the project in Visual Studio
|
||||||
|
8. Open http://localhost:8787
|
||||||
|
|
||||||
### Contributing Code ###
|
### 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)
|
- 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://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://hub.docker.com/r/hotio/readarr)
|
[](https://hub.docker.com/r/hotio/readarr)
|
||||||
|

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