Medium Support (Multi-disc Albums), Quality Grouping (#121)

* Multi Disc Stage 1 - Backend Work

* Quality Group Functionality

* Fixed: Only show wanted album types on ArtistDetail page

* Add Media Count Column to ArtistDetail Page

* Parser updates for multidisc cases, other usenet release title formats

* Search for Tracks by Medium Number in Addition to Title and TrackNumber

* Medium Renaming Token for Track Naming

* fixup Codacy and Comment Cleanup

* fixup remove comments
This commit is contained in:
Qstick 2017-11-15 21:24:33 -05:00 committed by GitHub
commit 21428cba6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
154 changed files with 2946 additions and 701 deletions

View file

@ -90,6 +90,15 @@ class NamingModal extends Component {
{ token: '{Album_CleanTitle}', example: 'Album_Title' }
];
const mediumTokens = [
{ token: '{medium:0}', example: '1' },
{ token: '{medium:00}', example: '01' }
];
const mediumFormatTokens = [
{ token: '{Medium Format}', example: 'CD' }
];
const trackTokens = [
{ token: '{track:0}', example: '1' },
{ token: '{track:00}', example: '01' }
@ -260,6 +269,48 @@ class NamingModal extends Component {
{
track &&
<div>
<FieldSet legend="Medium">
<div className={styles.groups}>
{
mediumTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
/>
);
}
)
}
</div>
</FieldSet>
<FieldSet legend="Medium Format">
<div className={styles.groups}>
{
mediumFormatTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
/>
);
}
)
}
</div>
</FieldSet>
<FieldSet legend="Track">
<div className={styles.groups}>
{

View file

@ -8,7 +8,7 @@ import LanguageProfileItem from './LanguageProfileItem';
import styles from './LanguageProfileItemDragPreview.css';
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
const formLabelWidth = parseInt(dimensions.formLabelWidth);
const formLabelLargeWidth = parseInt(dimensions.formLabelLargeWidth);
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
@ -40,7 +40,7 @@ class LanguageProfileItemDragPreview extends Component {
// list item and the preview is wider than the drag handle.
const { x, y } = currentOffset;
const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth;
const handleOffset = formGroupSmallWidth - formLabelLargeWidth - formLabelRightMarginWidth - dragHandleWidth;
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
const style = {

View file

@ -1,20 +1,56 @@
import PropTypes from 'prop-types';
import React from 'react';
import React, { Component } from 'react';
import { sizes } from 'Helpers/Props';
import Modal from 'Components/Modal/Modal';
import EditQualityProfileModalContentConnector from './EditQualityProfileModalContentConnector';
function EditQualityProfileModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditQualityProfileModalContentConnector
{...otherProps}
class EditQualityProfileModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
height: 'auto'
};
}
//
// Listeners
onContentHeightChange = (height) => {
if (this.state.height === 'auto' || height > this.state.height) {
this.setState({ height });
}
}
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
style={{ height: `${this.state.height}px` }}
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
onModalClose={onModalClose}
/>
</Modal>
);
>
<EditQualityProfileModalContentConnector
{...otherProps}
onContentHeightChange={this.onContentHeightChange}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
EditQualityProfileModal.propTypes = {

View file

@ -1,3 +1,18 @@
.formGroupsContainer {
display: flex;
flex-wrap: wrap;
}
.formGroupWrapper {
flex: 0 0 calc($formGroupSmallWidth - 100px);
}
.deleteButtonContainer {
margin-right: auto;
}
@media only screen and (max-width: $breakpointLarge) {
.formGroupsContainer {
display: block;
}
}

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes, kinds } from 'Helpers/Props';
import React, { Component } from 'react';
import Measure from 'react-measure';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -15,123 +17,223 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import QualityProfileItems from './QualityProfileItems';
import styles from './EditQualityProfileModalContent.css';
function EditQualityProfileModalContent(props) {
const {
isFetching,
error,
isSaving,
saveError,
qualities,
item,
isInUse,
onInputChange,
onCutoffChange,
onSavePress,
onModalClose,
onDeleteQualityProfilePress,
...otherProps
} = props;
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
const {
id,
name,
cutoff,
items
} = item;
class EditQualityProfileModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? 'Edit Quality Profile' : 'Add Quality Profile'}
</ModalHeader>
//
// Lifecycle
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
constructor(props, context) {
super(props, context);
{
!isFetching && !!error &&
<div>Unable to add a new quality profile, please try again.</div>
}
this.state = {
headerHeight: 0,
bodyHeight: 0,
footerHeight: 0
};
}
{
!isFetching && !error &&
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>Name</FormLabel>
componentDidUpdate(prevProps, prevState) {
const {
headerHeight,
bodyHeight,
footerHeight
} = this.state;
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
if (
headerHeight > 0 &&
bodyHeight > 0 &&
footerHeight > 0 &&
(
headerHeight !== prevState.headerHeight ||
bodyHeight !== prevState.bodyHeight ||
footerHeight !== prevState.footerHeight
)
) {
const padding = MODAL_BODY_PADDING * 2;
<FormGroup>
<FormLabel>Cutoff</FormLabel>
this.props.onContentHeightChange(
headerHeight + bodyHeight + footerHeight + padding
);
}
}
<FormInputGroup
type={inputTypes.SELECT}
name="cutoff"
{...cutoff}
value={cutoff ? cutoff.value.id : 0}
values={qualities}
helpText="Once this quality is reached Lidarr will no longer download episodes"
onChange={onCutoffChange}
/>
</FormGroup>
//
// Listeners
<QualityProfileItems
qualityProfileItems={items.value}
errors={items.errors}
warnings={items.warnings}
{...otherProps}
/>
onHeaderMeasure = ({ height }) => {
if (height > this.state.headerHeight) {
this.setState({ headerHeight: height });
}
}
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<div
className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a quality profile that is attached to a artist'}
>
<Button
kind={kinds.DANGER}
isDisabled={isInUse}
onPress={onDeleteQualityProfilePress}
>
Delete
</Button>
onBodyMeasure = ({ height }) => {
if (height > this.state.bodyHeight) {
this.setState({ bodyHeight: height });
}
}
onFooterMeasure = ({ height }) => {
if (height > this.state.footerHeight) {
this.setState({ footerHeight: height });
}
}
//
// Render
render() {
const {
editGroups,
isFetching,
error,
isSaving,
saveError,
qualities,
item,
isInUse,
onInputChange,
onCutoffChange,
onSavePress,
onModalClose,
onDeleteQualityProfilePress,
...otherProps
} = this.props;
const {
id,
name,
cutoff,
items
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<Measure
whitelist={['height']}
includeMargin={false}
onMeasure={this.onHeaderMeasure}
>
<ModalHeader>
{id ? 'Edit Quality Profile' : 'Add Quality Profile'}
</ModalHeader>
</Measure>
<ModalBody>
<Measure
whitelist={['height']}
onMeasure={this.onBodyMeasure}
>
<div>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to add a new quality profile, please try again.</div>
}
{
!isFetching && !error &&
<Form
{...otherProps}
>
<div className={styles.formGroupsContainer}>
<div className={styles.formGroupWrapper}>
<FormGroup size={sizes.EXTRA_SMALL}>
<FormLabel size={sizes.small}>
Name
</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup size={sizes.EXTRA_SMALL}>
<FormLabel size={sizes.small}>
Cutoff
</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="cutoff"
{...cutoff}
values={qualities}
helpText="Once this quality is reached Sonarr will no longer download episodes"
onChange={onCutoffChange}
/>
</FormGroup>
</div>
<div className={styles.formGroupWrapper}>
<QualityProfileItems
editGroups={editGroups}
qualityProfileItems={items.value}
errors={items.errors}
warnings={items.warnings}
{...otherProps}
/>
</div>
</div>
</Form>
}
</div>
}
</Measure>
</ModalBody>
<Button
onPress={onModalClose}
<Measure
whitelist={['height']}
includeMargin={false}
onMeasure={this.onFooterMeasure}
>
Cancel
</Button>
<ModalFooter>
{
id &&
<div
className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a quality profile that is attached to a series'}
>
<Button
kind={kinds.DANGER}
isDisabled={isInUse}
onPress={onDeleteQualityProfilePress}
>
Delete
</Button>
</div>
}
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
Save
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
<Button
onPress={onModalClose}
>
Cancel
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
Save
</SpinnerErrorButton>
</ModalFooter>
</Measure>
</ModalContent>
);
}
}
EditQualityProfileModalContent.propTypes = {
editGroups: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
@ -142,6 +244,7 @@ EditQualityProfileModalContent.propTypes = {
onInputChange: PropTypes.func.isRequired,
onCutoffChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onContentHeightChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteQualityProfilePress: PropTypes.func
};

View file

@ -8,6 +8,29 @@ import { fetchQualityProfileSchema, setQualityProfileValue, saveQualityProfile }
import connectSection from 'Store/connectSection';
import EditQualityProfileModalContent from './EditQualityProfileModalContent';
function getQualityItemGroupId(qualityProfile) {
// Get items with an `id` and filter out null/undefined values
const ids = _.filter(_.map(qualityProfile.items.value, 'id'), (i) => i != null);
return Math.max(1000, ...ids) + 1;
}
function parseIndex(index) {
const split = index.split('.');
if (split.length === 1) {
return [
null,
parseInt(split[0]) - 1
];
}
return [
parseInt(split[0]) - 1,
parseInt(split[1]) - 1
];
}
function createQualitiesSelector() {
return createSelector(
createProviderSettingsSelector(),
@ -17,12 +40,19 @@ function createQualitiesSelector() {
return [];
}
return _.reduceRight(items.value, (result, { allowed, quality }) => {
return _.reduceRight(items.value, (result, { allowed, id, name, quality }) => {
if (allowed) {
result.push({
key: quality.id,
value: quality.name
});
if (id) {
result.push({
key: id,
value: name
});
} else {
result.push({
key: quality.id,
value: quality.name
});
}
}
return result;
@ -61,8 +91,10 @@ class EditQualityProfileModalContentConnector extends Component {
super(props, context);
this.state = {
dragIndex: null,
dropIndex: null
dragQualityIndex: null,
dropQualityIndex: null,
dropPosition: null,
editGroups: true
};
}
@ -78,6 +110,33 @@ class EditQualityProfileModalContentConnector extends Component {
}
}
//
// Control
ensureCutoff = (qualityProfile) => {
const cutoff = qualityProfile.cutoff.value;
const cutoffItem = _.find(qualityProfile.items.value, (i) => {
if (!cutoff) {
return false;
}
return i.id === cutoff || (i.quality && i.quality.id === cutoff);
});
// If the cutoff isn't allowed anymore or there isn't a cutoff set one
if (!cutoff || !cutoffItem || !cutoffItem.allowed) {
const firstAllowed = _.find(qualityProfile.items.value, { allowed: true });
let cutoffId = null;
if (firstAllowed) {
cutoffId = firstAllowed.quality ? firstAllowed.quality.id : firstAllowed.id;
}
this.props.setQualityProfileValue({ name: 'cutoff', value: cutoffId });
}
}
//
// Listeners
@ -87,9 +146,17 @@ class EditQualityProfileModalContentConnector extends Component {
onCutoffChange = ({ name, value }) => {
const id = parseInt(value);
const item = _.find(this.props.item.items.value, (i) => i.quality.id === id);
const item = _.find(this.props.item.items.value, (i) => {
if (i.quality) {
return i.quality.id === id;
}
this.props.setQualityProfileValue({ name, value: item.quality });
return i.id === id;
});
const cutoffId = item.quality ? item.quality.id : item.id;
this.props.setQualityProfileValue({ name, value: cutoffId });
}
onSavePress = () => {
@ -98,58 +165,239 @@ class EditQualityProfileModalContentConnector extends Component {
onQualityProfileItemAllowedChange = (id, allowed) => {
const qualityProfile = _.cloneDeep(this.props.item);
const items = qualityProfile.items.value;
const item = _.find(qualityProfile.items.value, (i) => i.quality && i.quality.id === id);
const item = _.find(qualityProfile.items.value, (i) => i.quality.id === id);
item.allowed = allowed;
this.props.setQualityProfileValue({
name: 'items',
value: qualityProfile.items.value
value: items
});
const cutoff = qualityProfile.cutoff.value;
// If the cutoff isn't allowed anymore or there isn't a cutoff set one
if (!cutoff || !_.find(qualityProfile.items.value, (i) => i.quality.id === cutoff.id).allowed) {
const firstAllowed = _.find(qualityProfile.items.value, { allowed: true });
this.props.setQualityProfileValue({ name: 'cutoff', value: firstAllowed ? firstAllowed.quality : null });
}
this.ensureCutoff(qualityProfile);
}
onQualityProfileItemDragMove = (dragIndex, dropIndex) => {
if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) {
onItemGroupAllowedChange = (id, allowed) => {
const qualityProfile = _.cloneDeep(this.props.item);
const items = qualityProfile.items.value;
const item = _.find(qualityProfile.items.value, (i) => i.id === id);
item.allowed = allowed;
// Update each item in the group (for consistency only)
item.items.forEach((i) => {
i.allowed = allowed;
});
this.props.setQualityProfileValue({
name: 'items',
value: items
});
this.ensureCutoff(qualityProfile);
}
onItemGroupNameChange = (id, name) => {
const qualityProfile = _.cloneDeep(this.props.item);
const items = qualityProfile.items.value;
const group = _.find(items, (i) => i.id === id);
group.name = name;
this.props.setQualityProfileValue({
name: 'items',
value: items
});
}
onCreateGroupPress = (id) => {
const qualityProfile = _.cloneDeep(this.props.item);
const items = qualityProfile.items.value;
const item = _.find(items, (i) => i.quality && i.quality.id === id);
const index = items.indexOf(item);
const groupId = getQualityItemGroupId(qualityProfile);
const group = {
id: groupId,
name: item.quality.name,
allowed: item.allowed,
items: [
item
]
};
// Add the group in the same location the quality item was in.
items.splice(index, 1, group);
this.props.setQualityProfileValue({
name: 'items',
value: items
});
this.ensureCutoff(qualityProfile);
}
onDeleteGroupPress = (id) => {
const qualityProfile = _.cloneDeep(this.props.item);
const items = qualityProfile.items.value;
const group = _.find(items, (i) => i.id === id);
const index = items.indexOf(group);
// Add the items in the same location the group was in
items.splice(index, 1, ...group.items);
this.props.setQualityProfileValue({
name: 'items',
value: items
});
this.ensureCutoff(qualityProfile);
}
onQualityProfileItemDragMove = (options) => {
const {
dragQualityIndex,
dropQualityIndex,
dropPosition
} = options;
const [dragGroupIndex, dragItemIndex] = parseIndex(dragQualityIndex);
const [dropGroupIndex, dropItemIndex] = parseIndex(dropQualityIndex);
if (
(dropPosition === 'below' && dropItemIndex - 1 === dragItemIndex) ||
(dropPosition === 'above' && dropItemIndex + 1 === dragItemIndex)
) {
if (
this.state.dragQualityIndex != null &&
this.state.dropQualityIndex != null &&
this.state.dropPosition != null
) {
this.setState({
dragQualityIndex: null,
dropQualityIndex: null,
dropPosition: null
});
}
return;
}
let adjustedDropQualityIndex = dropQualityIndex;
// Correct dragging out of a group to the position above
if (
dropPosition === 'above' &&
dragGroupIndex !== dropGroupIndex &&
dropGroupIndex != null
) {
// Add 1 to the group index and 2 to the item index so it's inserted above in the correct group
adjustedDropQualityIndex = `${dropGroupIndex + 1}.${dropItemIndex + 2}`;
}
// Correct inserting above outside a group
if (
dropPosition === 'above' &&
dragGroupIndex !== dropGroupIndex &&
dropGroupIndex == null
) {
// Add 2 to the item index so it's entered in the correct place
adjustedDropQualityIndex = `${dropItemIndex + 2}`;
}
// Correct inserting below a quality within the same group (when moving a lower item)
if (
dropPosition === 'below' &&
dragGroupIndex === dropGroupIndex &&
dropGroupIndex != null &&
dragItemIndex < dropItemIndex
) {
// Add 1 to the group index leave the item index
adjustedDropQualityIndex = `${dropGroupIndex + 1}.${dropItemIndex}`;
}
// Correct inserting below a quality outside a group (when moving a lower item)
if (
dropPosition === 'below' &&
dragGroupIndex === dropGroupIndex &&
dropGroupIndex == null &&
dragItemIndex < dropItemIndex
) {
// Leave the item index so it's inserted below the item
adjustedDropQualityIndex = `${dropItemIndex}`;
}
if (
dragQualityIndex !== this.state.dragQualityIndex ||
adjustedDropQualityIndex !== this.state.dropQualityIndex ||
dropPosition !== this.state.dropPosition
) {
this.setState({
dragIndex,
dropIndex
dragQualityIndex,
dropQualityIndex: adjustedDropQualityIndex,
dropPosition
});
}
}
onQualityProfileItemDragEnd = ({ id }, didDrop) => {
onQualityProfileItemDragEnd = (didDrop) => {
const {
dragIndex,
dropIndex
dragQualityIndex,
dropQualityIndex
} = this.state;
if (didDrop && dropIndex !== null) {
if (didDrop && dropQualityIndex != null) {
const qualityProfile = _.cloneDeep(this.props.item);
const items = qualityProfile.items.value;
const [dragGroupIndex, dragItemIndex] = parseIndex(dragQualityIndex);
const [dropGroupIndex, dropItemIndex] = parseIndex(dropQualityIndex);
const items = qualityProfile.items.value.splice(dragIndex, 1);
qualityProfile.items.value.splice(dropIndex, 0, items[0]);
let item = null;
let dropGroup = null;
// Get the group before moving anything so we know the correct place to drop it.
if (dropGroupIndex != null) {
dropGroup = items[dropGroupIndex];
}
if (dragGroupIndex == null) {
item = items.splice(dragItemIndex, 1)[0];
} else {
const group = items[dragGroupIndex];
item = group.items.splice(dragItemIndex, 1)[0];
// If the group is now empty, destroy it.
if (!group.items.length) {
items.splice(dragGroupIndex, 1);
}
}
if (dropGroupIndex == null) {
items.splice(dropItemIndex, 0, item);
} else {
dropGroup.items.splice(dropItemIndex, 0, item);
}
this.props.setQualityProfileValue({
name: 'items',
value: qualityProfile.items.value
value: items
});
this.ensureCutoff(qualityProfile);
}
this.setState({
dragIndex: null,
dropIndex: null
dragQualityIndex: null,
dropQualityIndex: null,
dropPosition: null
});
}
onToggleEditGroupsMode = () => {
this.setState({ editGroups: !this.state.editGroups });
}
//
// Render
@ -165,9 +413,14 @@ class EditQualityProfileModalContentConnector extends Component {
onSavePress={this.onSavePress}
onInputChange={this.onInputChange}
onCutoffChange={this.onCutoffChange}
onCreateGroupPress={this.onCreateGroupPress}
onDeleteGroupPress={this.onDeleteGroupPress}
onQualityProfileItemAllowedChange={this.onQualityProfileItemAllowedChange}
onItemGroupAllowedChange={this.onItemGroupAllowedChange}
onItemGroupNameChange={this.onItemGroupNameChange}
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
onToggleEditGroupsMode={this.onToggleEditGroupsMode}
/>
);
}

View file

@ -17,3 +17,10 @@
flex-wrap: wrap;
margin-top: 5px;
}
.tooltipLabel {
composes: label from 'Components/Label.css';
margin: 0;
border: none;
}

View file

@ -1,9 +1,10 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import { kinds, tooltipPositions } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import Tooltip from 'Components/Tooltip/Tooltip';
import EditQualityProfileModalConnector from './EditQualityProfileModalConnector';
import styles from './QualityProfile.css';
@ -75,16 +76,54 @@ class QualityProfile extends Component {
return null;
}
const isCutoff = item.quality.id === cutoff.id;
if (item.quality) {
const isCutoff = item.quality.id === cutoff;
return (
<Label
key={item.quality.id}
kind={isCutoff ? kinds.INFO : kinds.default}
title={isCutoff ? 'Cutoff' : null}
>
{item.quality.name}
</Label>
);
}
const isCutoff = item.id === cutoff;
return (
<Label
key={item.quality.id}
kind={isCutoff ? kinds.INFO : kinds.default}
title={isCutoff ? 'Cutoff' : null}
>
{item.quality.name}
</Label>
<Tooltip
key={item.id}
className={styles.tooltipLabel}
anchor={
<Label
kind={isCutoff ? kinds.INFO : kinds.default}
title={isCutoff ? 'Cutoff' : null}
>
{item.name}
</Label>
}
tooltip={
<div>
{
item.items.map((groupItem) => {
return (
<Label
key={groupItem.quality.id}
kind={isCutoff ? kinds.INFO : kinds.default}
title={isCutoff ? 'Cutoff' : null}
>
{groupItem.quality.name}
</Label>
);
})
}
</div>
}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
);
})
}
@ -115,7 +154,7 @@ class QualityProfile extends Component {
QualityProfile.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
cutoff: PropTypes.object.isRequired,
cutoff: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteQualityProfile: PropTypes.func.isRequired

View file

@ -5,25 +5,56 @@
border: 1px solid #aaa;
border-radius: 4px;
background: #fafafa;
&.isInGroup {
border-style: dashed;
}
}
.checkContainer {
.checkInputContainer {
position: relative;
margin-right: 4px;
margin-bottom: 7px;
margin-bottom: 5px;
margin-left: 8px;
}
.qualityName {
.checkInput {
composes: input from 'Components/Form/CheckInput.css';
margin-top: 5px;
}
.qualityNameContainer {
display: flex;
flex-grow: 1;
margin-bottom: 0;
margin-left: 2px;
font-weight: normal;
line-height: 36px;
line-height: $qualityProfileItemHeight;
cursor: pointer;
}
.qualityName {
&.isInGroup {
margin-left: 14px;
}
&.notAllowed {
color: #c6c6c6;
}
}
.createGroupButton {
composes: buton from 'Components/Link/IconButton.css';
display: flex;
justify-content: center;
flex-shrink: 0;
margin-right: 5px;
margin-left: 8px;
width: 20px;
}
.dragHandle {
display: flex;
align-items: center;
@ -42,3 +73,13 @@
.isDragging {
opacity: 0.25;
}
.isPreview {
.qualityName {
margin-left: 14px;
&.isInGroup {
margin-left: 28px;
}
}
}

View file

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import CheckInput from 'Components/Form/CheckInput';
import styles from './QualityProfileItem.css';
@ -20,14 +21,27 @@ class QualityProfileItem extends Component {
onQualityProfileItemAllowedChange(qualityId, value);
}
onCreateGroupPress = () => {
const {
qualityId,
onCreateGroupPress
} = this.props;
onCreateGroupPress(qualityId);
}
//
// Render
render() {
const {
editGroups,
isPreview,
groupId,
name,
allowed,
isDragging,
isOverCurrent,
connectDragSource
} = this.props;
@ -36,18 +50,44 @@ class QualityProfileItem extends Component {
className={classNames(
styles.qualityProfileItem,
isDragging && styles.isDragging,
isPreview && styles.isPreview,
isOverCurrent && styles.isOverCurrent,
groupId && styles.isInGroup
)}
>
<label
className={styles.qualityName}
className={styles.qualityNameContainer}
>
<CheckInput
containerClassName={styles.checkContainer}
name={name}
value={allowed}
onChange={this.onAllowedChange}
/>
{name}
{
editGroups && !groupId && !isPreview &&
<IconButton
className={styles.createGroupButton}
name={icons.GROUP}
title="Group"
onPress={this.onCreateGroupPress}
/>
}
{
!editGroups &&
<CheckInput
className={styles.checkInput}
containerClassName={styles.checkInputContainer}
name={name}
value={allowed}
isDisabled={!!groupId}
onChange={this.onAllowedChange}
/>
}
<div className={classNames(
styles.qualityName,
groupId && styles.isInGroup,
!allowed && styles.notAllowed
)}
>
{name}
</div>
</label>
{
@ -55,6 +95,7 @@ class QualityProfileItem extends Component {
<div className={styles.dragHandle}>
<Icon
className={styles.dragIcon}
title="Create group"
name={icons.REORDER}
/>
</div>
@ -66,16 +107,23 @@ class QualityProfileItem extends Component {
}
QualityProfileItem.propTypes = {
editGroups: PropTypes.bool,
isPreview: PropTypes.bool,
groupId: PropTypes.number,
qualityId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
sortIndex: PropTypes.number.isRequired,
isDragging: PropTypes.bool.isRequired,
isOverCurrent: PropTypes.bool.isRequired,
isInGroup: PropTypes.bool,
connectDragSource: PropTypes.func,
onCreateGroupPress: PropTypes.func,
onQualityProfileItemAllowedChange: PropTypes.func
};
QualityProfileItem.defaultProps = {
isPreview: false,
isOverCurrent: false,
// The drag preview will not connect the drag handle.
connectDragSource: (node) => node
};

View file

@ -7,8 +7,8 @@ import DragPreviewLayer from 'Components/DragPreviewLayer';
import QualityProfileItem from './QualityProfileItem';
import styles from './QualityProfileItemDragPreview.css';
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
const formLabelWidth = parseInt(dimensions.formLabelWidth);
const formGroupExtraSmallWidth = parseInt(dimensions.formGroupExtraSmallWidth);
const formLabelSmallWidth = parseInt(dimensions.formLabelSmallWidth);
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
@ -40,7 +40,7 @@ class QualityProfileItemDragPreview extends Component {
// list item and the preview is wider than the drag handle.
const { x, y } = currentOffset;
const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth;
const handleOffset = formGroupExtraSmallWidth - formLabelSmallWidth - formLabelRightMarginWidth - dragHandleWidth;
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
const style = {
@ -51,12 +51,15 @@ class QualityProfileItemDragPreview extends Component {
};
const {
editGroups,
groupId,
qualityId,
name,
allowed,
sortIndex
allowed
} = item;
// TODO: Show a different preview for groups
return (
<DragPreviewLayer>
<div
@ -64,10 +67,11 @@ class QualityProfileItemDragPreview extends Component {
style={style}
>
<QualityProfileItem
qualityId={qualityId}
editGroups={editGroups}
isPreview={true}
qualityId={groupId || qualityId}
name={name}
allowed={allowed}
sortIndex={sortIndex}
isDragging={false}
/>
</div>

View file

@ -1,10 +1,10 @@
.qualityProfileItemDragSource {
padding: 4px 0;
padding: $qualityProfileItemDragSourcePadding 0;
}
.qualityProfileItemPlaceholder {
width: 100%;
height: 36px;
height: $qualityProfileItemHeight;
border: 1px dotted #aaa;
border-radius: 4px;
}

View file

@ -5,44 +5,86 @@ import { DragSource, DropTarget } from 'react-dnd';
import classNames from 'classnames';
import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes';
import QualityProfileItem from './QualityProfileItem';
import QualityProfileItemGroup from './QualityProfileItemGroup';
import styles from './QualityProfileItemDragSource.css';
const qualityProfileItemDragSource = {
beginDrag({ qualityId, name, allowed, sortIndex }) {
return {
beginDrag(props) {
const {
editGroups,
qualityIndex,
groupId,
qualityId,
name,
allowed,
sortIndex
allowed
} = props;
return {
editGroups,
qualityIndex,
groupId,
qualityId,
isGroup: !qualityId,
name,
allowed
};
},
endDrag(props, monitor, component) {
props.onQualityProfileItemDragEnd(monitor.getItem(), monitor.didDrop());
props.onQualityProfileItemDragEnd(monitor.didDrop());
}
};
const qualityProfileItemDropTarget = {
hover(props, monitor, component) {
const dragIndex = monitor.getItem().sortIndex;
const hoverIndex = props.sortIndex;
const {
qualityIndex: dragQualityIndex,
isGroup: isDragGroup
} = monitor.getItem();
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
const dropQualityIndex = props.qualityIndex;
const isDropGroupItem = !!(props.qualityId && props.groupId);
// Use childNodeIndex to select the correct node to get the middle of so
// we don't bounce between above and below causing rapid setState calls.
const childNodeIndex = component.props.isOverCurrent && component.props.isDraggingUp ? 1 :0;
const componentDOMNode = findDOMNode(component).children[childNodeIndex];
const hoverBoundingRect = componentDOMNode.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Moving up, only trigger if drag position is above 50%
if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) {
// If we're hovering over a child don't trigger on the parent
if (!monitor.isOver({ shallow: true })) {
return;
}
// Moving down, only trigger if drag position is below 50%
if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
// Don't show targets for dropping on self
if (dragQualityIndex === dropQualityIndex) {
return;
}
props.onQualityProfileItemDragMove(dragIndex, hoverIndex);
// Don't allow a group to be dropped inside a group
if (isDragGroup && isDropGroupItem) {
return;
}
let dropPosition = null;
// Determine drop position based on position over target
if (hoverClientY > hoverMiddleY) {
dropPosition = 'below';
} else if (hoverClientY < hoverMiddleY) {
dropPosition = 'above';
} else {
return;
}
props.onQualityProfileItemDragMove({
dragQualityIndex,
dropQualityIndex,
dropPosition
});
}
};
@ -56,7 +98,8 @@ function collectDragSource(connect, monitor) {
function collectDropTarget(connect, monitor) {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver()
isOver: monitor.isOver(),
isOverCurrent: monitor.isOver({ shallow: true })
};
}
@ -67,25 +110,30 @@ class QualityProfileItemDragSource extends Component {
render() {
const {
editGroups,
groupId,
qualityId,
name,
allowed,
sortIndex,
items,
qualityIndex,
isDragging,
isDraggingUp,
isDraggingDown,
isOver,
isOverCurrent,
connectDragSource,
connectDropTarget,
onQualityProfileItemAllowedChange
onCreateGroupPress,
onDeleteGroupPress,
onQualityProfileItemAllowedChange,
onItemGroupAllowedChange,
onItemGroupNameChange,
onQualityProfileItemDragMove,
onQualityProfileItemDragEnd
} = this.props;
const isBefore = !isDragging && isDraggingUp && isOver;
const isAfter = !isDragging && isDraggingDown && isOver;
// if (isDragging && !isOver) {
// return null;
// }
const isBefore = !isDragging && isDraggingUp && isOverCurrent;
const isAfter = !isDragging && isDraggingDown && isOverCurrent;
return connectDropTarget(
<div
@ -105,16 +153,44 @@ class QualityProfileItemDragSource extends Component {
/>
}
<QualityProfileItem
qualityId={qualityId}
name={name}
allowed={allowed}
sortIndex={sortIndex}
isDragging={isDragging}
isOver={isOver}
connectDragSource={connectDragSource}
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
/>
{
!!groupId && qualityId == null &&
<QualityProfileItemGroup
editGroups={editGroups}
groupId={groupId}
name={name}
allowed={allowed}
items={items}
qualityIndex={qualityIndex}
isDragging={isDragging}
isDraggingUp={isDraggingUp}
isDraggingDown={isDraggingDown}
connectDragSource={connectDragSource}
onDeleteGroupPress={onDeleteGroupPress}
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
onItemGroupAllowedChange={onItemGroupAllowedChange}
onItemGroupNameChange={onItemGroupNameChange}
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
/>
}
{
qualityId != null &&
<QualityProfileItem
editGroups={editGroups}
groupId={groupId}
qualityId={qualityId}
name={name}
allowed={allowed}
qualityIndex={qualityIndex}
isDragging={isDragging}
isOverCurrent={isOverCurrent}
connectDragSource={connectDragSource}
onCreateGroupPress={onCreateGroupPress}
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
/>
}
{
isAfter &&
@ -131,17 +207,25 @@ class QualityProfileItemDragSource extends Component {
}
QualityProfileItemDragSource.propTypes = {
qualityId: PropTypes.number.isRequired,
editGroups: PropTypes.bool.isRequired,
groupId: PropTypes.number,
qualityId: PropTypes.number,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
sortIndex: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object),
qualityIndex: PropTypes.string.isRequired,
isDragging: PropTypes.bool,
isDraggingUp: PropTypes.bool,
isDraggingDown: PropTypes.bool,
isOver: PropTypes.bool,
isOverCurrent: PropTypes.bool,
isInGroup: PropTypes.bool,
connectDragSource: PropTypes.func,
connectDropTarget: PropTypes.func,
onCreateGroupPress: PropTypes.func,
onDeleteGroupPress: PropTypes.func,
onQualityProfileItemAllowedChange: PropTypes.func.isRequired,
onItemGroupAllowedChange: PropTypes.func,
onItemGroupNameChange: PropTypes.func,
onQualityProfileItemDragMove: PropTypes.func.isRequired,
onQualityProfileItemDragEnd: PropTypes.func.isRequired
};

View file

@ -0,0 +1,105 @@
.qualityProfileItemGroup {
width: 100%;
border: 1px solid #aaa;
border-radius: 4px;
background: #fafafa;
&.editGroups {
background: #fcfcfc;
}
}
.qualityProfileItemGroupInfo {
display: flex;
align-items: stretch;
width: 100%;
}
.checkInputContainer {
composes: checkInputContainer from './QualityProfileItem.css';
display: flex;
align-items: center;
}
.checkInput {
composes: checkInput from './QualityProfileItem.css';
}
.nameInput {
composes: text from 'Components/Form/TextInput.css';
margin-top: 4px;
margin-right: 10px;
}
.nameContainer {
display: flex;
align-items: center;
flex-grow: 1;
}
.name {
flex-shrink: 0;
&.notAllowed {
color: #c6c6c6;
}
}
.groupQualities {
display: flex;
justify-content: flex-end;
flex-grow: 1;
flex-wrap: wrap;
margin: 2px 0 2px 10px;
}
.qualityNameContainer {
display: flex;
align-items: stretch;
flex-grow: 1;
margin-bottom: 0;
margin-left: 2px;
font-weight: normal;
}
.qualityNameLabel {
composes: qualityNameContainer;
cursor: pointer;
}
.deleteGroupButton {
composes: buton from 'Components/Link/IconButton.css';
display: flex;
justify-content: center;
flex-shrink: 0;
margin-right: 5px;
margin-left: 8px;
width: 20px;
}
.dragHandle {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-left: auto;
width: $dragHandleWidth;
text-align: center;
cursor: grab;
}
.dragIcon {
top: 0;
}
.isDragging {
opacity: 0.25;
}
.items {
margin: 0 50px 0 35px;
}

View file

@ -0,0 +1,200 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import CheckInput from 'Components/Form/CheckInput';
import TextInput from 'Components/Form/TextInput';
import QualityProfileItemDragSource from './QualityProfileItemDragSource';
import styles from './QualityProfileItemGroup.css';
class QualityProfileItemGroup extends Component {
//
// Listeners
onAllowedChange = ({ value }) => {
const {
groupId,
onItemGroupAllowedChange
} = this.props;
onItemGroupAllowedChange(groupId, value);
}
onNameChange = ({ value }) => {
const {
groupId,
onItemGroupNameChange
} = this.props;
onItemGroupNameChange(groupId, value);
}
onDeleteGroupPress = ({ value }) => {
const {
groupId,
onDeleteGroupPress
} = this.props;
onDeleteGroupPress(groupId, value);
}
//
// Render
render() {
const {
editGroups,
groupId,
name,
allowed,
items,
qualityIndex,
isDragging,
isDraggingUp,
isDraggingDown,
connectDragSource,
onQualityProfileItemAllowedChange,
onQualityProfileItemDragMove,
onQualityProfileItemDragEnd
} = this.props;
return (
<div
className={classNames(
styles.qualityProfileItemGroup,
editGroups && styles.editGroups,
isDragging && styles.isDragging,
)}
>
<div className={styles.qualityProfileItemGroupInfo}>
{
editGroups &&
<div className={styles.qualityNameContainer}>
<IconButton
className={styles.deleteGroupButton}
name={icons.UNGROUP}
title="Ungroup"
onPress={this.onDeleteGroupPress}
/>
<TextInput
className={styles.nameInput}
name="name"
value={name}
onChange={this.onNameChange}
/>
</div>
}
{
!editGroups &&
<label
className={styles.qualityNameLabel}
>
<CheckInput
className={styles.checkInput}
containerClassName={styles.checkInputContainer}
name="allowed"
value={allowed}
onChange={this.onAllowedChange}
/>
<div className={styles.nameContainer}>
<div className={classNames(
styles.name,
!allowed && styles.notAllowed
)}
>
{name}
</div>
<div className={styles.groupQualities}>
{
items.map(({ quality }) => {
return (
<Label key={quality.id}>
{quality.name}
</Label>
);
}).reverse()
}
</div>
</div>
</label>
}
{
connectDragSource(
<div className={styles.dragHandle}>
<Icon
className={styles.dragIcon}
name={icons.REORDER}
title="Reorder"
/>
</div>
)
}
</div>
{
editGroups &&
<div className={styles.items}>
{
items.map(({ quality }, index) => {
return (
<QualityProfileItemDragSource
key={quality.id}
editGroups={editGroups}
groupId={groupId}
qualityId={quality.id}
name={quality.name}
allowed={allowed}
items={items}
qualityIndex={`${qualityIndex}.${index + 1}`}
isDragging={isDragging}
isDraggingUp={isDraggingUp}
isDraggingDown={isDraggingDown}
isInGroup={true}
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
/>
);
}).reverse()
}
</div>
}
</div>
);
}
}
QualityProfileItemGroup.propTypes = {
editGroups: PropTypes.bool,
groupId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
qualityIndex: PropTypes.string.isRequired,
isDragging: PropTypes.bool.isRequired,
isDraggingUp: PropTypes.bool.isRequired,
isDraggingDown: PropTypes.bool.isRequired,
connectDragSource: PropTypes.func,
onItemGroupAllowedChange: PropTypes.func.isRequired,
onQualityProfileItemAllowedChange: PropTypes.func.isRequired,
onItemGroupNameChange: PropTypes.func.isRequired,
onDeleteGroupPress: PropTypes.func.isRequired,
onQualityProfileItemDragMove: PropTypes.func.isRequired,
onQualityProfileItemDragEnd: PropTypes.func.isRequired
};
QualityProfileItemGroup.defaultProps = {
// The drag preview will not connect the drag handle.
connectDragSource: (node) => node
};
export default QualityProfileItemGroup;

View file

@ -1,6 +1,15 @@
.editGroupsButton {
composes: button from 'Components/Link/Button.css';
margin-top: 10px;
}
.editGroupsButtonIcon {
margin-right: 8px;
}
.qualities {
margin-top: 10px;
/* TODO: This should consider the number of qualities in the list */
min-height: 550px;
transition: min-height 200ms;
user-select: none;
}

View file

@ -1,5 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Measure from 'react-measure';
import { icons, kinds, sizes } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
@ -9,26 +13,69 @@ import styles from './QualityProfileItems.css';
class QualityProfileItems extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
qualitiesHeight: 0,
qualitiesHeightEditGroups: 0
};
}
componentDidMount() {
this.props.onToggleEditGroupsMode();
}
//
// Listeners
onMeasure = ({ height }) => {
if (this.props.editGroups) {
this.setState({
qualitiesHeightEditGroups: height
});
} else {
this.setState({ qualitiesHeight: height });
}
}
onToggleEditGroupsMode = () => {
this.props.onToggleEditGroupsMode();
}
//
// Render
render() {
const {
dragIndex,
dropIndex,
editGroups,
dropQualityIndex,
dropPosition,
qualityProfileItems,
errors,
warnings,
...otherProps
} = this.props;
const isDragging = dropIndex !== null;
const isDraggingUp = isDragging && dropIndex > dragIndex;
const isDraggingDown = isDragging && dropIndex < dragIndex;
const {
qualitiesHeight,
qualitiesHeightEditGroups
} = this.state;
const isDragging = dropQualityIndex !== null;
const isDraggingUp = isDragging && dropPosition === 'above';
const isDraggingDown = isDragging && dropPosition === 'below';
const minHeight = editGroups ? qualitiesHeightEditGroups : qualitiesHeight;
return (
<FormGroup>
<FormLabel>Qualities</FormLabel>
<FormGroup size={sizes.EXTRA_SMALL}>
<FormLabel size={sizes.SMALL}>
Qualities
</FormLabel>
<div>
<FormInputHelpText
text="Qualities higher in the list are more preferred. Only checked qualities are wanted"
@ -60,27 +107,59 @@ class QualityProfileItems extends Component {
})
}
<div className={styles.qualities}>
{
qualityProfileItems.map(({ allowed, quality }, index) => {
return (
<QualityProfileItemDragSource
key={quality.id}
qualityId={quality.id}
name={quality.name}
allowed={allowed}
sortIndex={index}
isDragging={isDragging}
isDraggingUp={isDraggingUp}
isDraggingDown={isDraggingDown}
{...otherProps}
/>
);
}).reverse()
}
<Button
className={styles.editGroupsButton}
kind={kinds.PRIMARY}
onPress={this.onToggleEditGroupsMode}
>
<div>
<Icon
className={styles.editGroupsButtonIcon}
name={editGroups ? icons.REORDER : icons.GROUP}
/>
<QualityProfileItemDragPreview />
</div>
{
editGroups ? 'Done Editing Groups' : 'Edit Groups'
}
</div>
</Button>
<Measure
whitelist={['height']}
includeMargin={false}
onMeasure={this.onMeasure}
>
<div
className={styles.qualities}
style={{ minHeight: `${minHeight}px` }}
>
{
qualityProfileItems.map(({ id, name, allowed, quality, items }, index) => {
const identifier = quality ? quality.id : id;
return (
<QualityProfileItemDragSource
key={identifier}
editGroups={editGroups}
groupId={id}
qualityId={quality && quality.id}
name={quality ? quality.name : name}
allowed={allowed}
items={items}
qualityIndex={`${index + 1}`}
isInGroup={false}
isDragging={isDragging}
isDraggingUp={isDraggingUp}
isDraggingDown={isDraggingDown}
{...otherProps}
/>
);
}).reverse()
}
<QualityProfileItemDragPreview />
</div>
</Measure>
</div>
</FormGroup>
);
@ -88,11 +167,14 @@ class QualityProfileItems extends Component {
}
QualityProfileItems.propTypes = {
dragIndex: PropTypes.number,
dropIndex: PropTypes.number,
editGroups: PropTypes.bool.isRequired,
dragQualityIndex: PropTypes.string,
dropQualityIndex: PropTypes.string,
dropPosition: PropTypes.string,
qualityProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object)
warnings: PropTypes.arrayOf(PropTypes.object),
onToggleEditGroupsMode: PropTypes.func.isRequired
};
QualityProfileItems.defaultProps = {

View file

@ -91,7 +91,6 @@ class QualityProfiles extends Component {
}
QualityProfiles.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,

View file

@ -7,11 +7,9 @@ import QualityProfiles from './QualityProfiles';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
(state) => state.settings.qualityProfiles,
(advancedSettings, qualityProfiles) => {
(qualityProfiles) => {
return {
advancedSettings,
...qualityProfiles
};
}