mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-13 02:07:12 -07:00
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:
parent
e1e7cad951
commit
21428cba6f
154 changed files with 2946 additions and 701 deletions
|
@ -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}>
|
||||
{
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,3 +17,10 @@
|
|||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tooltipLabel {
|
||||
composes: label from 'Components/Label.css';
|
||||
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue