mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-23 22:25:14 -04:00
New: Project Aphrodite
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
.delayProfile {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin-bottom: 10px;
|
||||
height: 30px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 0 0 200px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.editButton {
|
||||
width: $dragHandleWidth;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TagList from 'Components/TagList';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import EditDelayProfileModalConnector from './EditDelayProfileModalConnector';
|
||||
import styles from './DelayProfile.css';
|
||||
|
||||
function getDelay(enabled, delay) {
|
||||
if (!enabled) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (!delay) {
|
||||
return 'No Delay';
|
||||
}
|
||||
|
||||
if (delay === 1) {
|
||||
return '1 Minute';
|
||||
}
|
||||
|
||||
// TODO: use better units of time than just minutes
|
||||
return `${delay} Minutes`;
|
||||
}
|
||||
|
||||
class DelayProfile extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditDelayProfileModalOpen: false,
|
||||
isDeleteDelayProfileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditDelayProfilePress = () => {
|
||||
this.setState({ isEditDelayProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onEditDelayProfileModalClose = () => {
|
||||
this.setState({ isEditDelayProfileModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteDelayProfilePress = () => {
|
||||
this.setState({
|
||||
isEditDelayProfileModalOpen: false,
|
||||
isDeleteDelayProfileModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteDelayProfileModalClose = () => {
|
||||
this.setState({ isDeleteDelayProfileModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmDeleteDelayProfile = () => {
|
||||
this.props.onConfirmDeleteDelayProfile(this.props.id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
enableUsenet,
|
||||
enableTorrent,
|
||||
preferredProtocol,
|
||||
usenetDelay,
|
||||
torrentDelay,
|
||||
tags,
|
||||
tagList,
|
||||
isDragging,
|
||||
connectDragSource
|
||||
} = this.props;
|
||||
|
||||
let preferred = titleCase(preferredProtocol);
|
||||
|
||||
if (!enableUsenet) {
|
||||
preferred = 'Only Torrent';
|
||||
} else if (!enableTorrent) {
|
||||
preferred = 'Only Usenet';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.delayProfile,
|
||||
isDragging && styles.isDragging,
|
||||
)}
|
||||
>
|
||||
<div className={styles.column}>{preferred}</div>
|
||||
<div className={styles.column}>{getDelay(enableUsenet, usenetDelay)}</div>
|
||||
<div className={styles.column}>{getDelay(enableTorrent, torrentDelay)}</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Link
|
||||
className={id === 1 ? styles.editButton : undefined}
|
||||
onPress={this.onEditDelayProfilePress}
|
||||
>
|
||||
<Icon name={icons.EDIT} />
|
||||
</Link>
|
||||
|
||||
{
|
||||
id !== 1 &&
|
||||
connectDragSource(
|
||||
<div className={styles.dragHandle}>
|
||||
<Icon
|
||||
className={styles.dragIcon}
|
||||
name={icons.REORDER}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditDelayProfileModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditDelayProfileModalOpen}
|
||||
onModalClose={this.onEditDelayProfileModalClose}
|
||||
onDeleteDelayProfilePress={this.onDeleteDelayProfilePress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteDelayProfileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Delay Profile"
|
||||
message="Are you sure you want to delete this delay profile?"
|
||||
confirmLabel="Delete"
|
||||
onConfirm={this.onConfirmDeleteDelayProfile}
|
||||
onCancel={this.onDeleteDelayProfileModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DelayProfile.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
enableUsenet: PropTypes.bool.isRequired,
|
||||
enableTorrent: PropTypes.bool.isRequired,
|
||||
preferredProtocol: PropTypes.string.isRequired,
|
||||
usenetDelay: PropTypes.number.isRequired,
|
||||
torrentDelay: PropTypes.number.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDragging: PropTypes.bool.isRequired,
|
||||
connectDragSource: PropTypes.func,
|
||||
onConfirmDeleteDelayProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
DelayProfile.defaultProps = {
|
||||
// The drag preview will not connect the drag handle.
|
||||
connectDragSource: (node) => node
|
||||
};
|
||||
|
||||
export default DelayProfile;
|
||||
@@ -0,0 +1,3 @@
|
||||
.dragPreview {
|
||||
opacity: 0.75;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DragLayer } from 'react-dnd';
|
||||
import dimensions from 'Styles/Variables/dimensions.js';
|
||||
import { DELAY_PROFILE } from 'Helpers/dragTypes';
|
||||
import DragPreviewLayer from 'Components/DragPreviewLayer';
|
||||
import DelayProfile from './DelayProfile';
|
||||
import styles from './DelayProfileDragPreview.css';
|
||||
|
||||
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
||||
|
||||
function collectDragLayer(monitor) {
|
||||
return {
|
||||
item: monitor.getItem(),
|
||||
itemType: monitor.getItemType(),
|
||||
currentOffset: monitor.getSourceClientOffset()
|
||||
};
|
||||
}
|
||||
|
||||
class DelayProfileDragPreview extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
width,
|
||||
item,
|
||||
itemType,
|
||||
currentOffset
|
||||
} = this.props;
|
||||
|
||||
if (!currentOffset || itemType !== DELAY_PROFILE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The offset is shifted because the drag handle is on the right edge of the
|
||||
// list item and the preview is wider than the drag handle.
|
||||
|
||||
const { x, y } = currentOffset;
|
||||
const handleOffset = width - dragHandleWidth;
|
||||
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
||||
|
||||
const style = {
|
||||
width,
|
||||
position: 'absolute',
|
||||
WebkitTransform: transform,
|
||||
msTransform: transform,
|
||||
transform
|
||||
};
|
||||
|
||||
return (
|
||||
<DragPreviewLayer>
|
||||
<div
|
||||
className={styles.dragPreview}
|
||||
style={style}
|
||||
>
|
||||
<DelayProfile
|
||||
isDragging={false}
|
||||
{...item}
|
||||
/>
|
||||
</div>
|
||||
</DragPreviewLayer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DelayProfileDragPreview.propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
item: PropTypes.object,
|
||||
itemType: PropTypes.string,
|
||||
currentOffset: PropTypes.shape({
|
||||
x: PropTypes.number.isRequired,
|
||||
y: PropTypes.number.isRequired
|
||||
})
|
||||
};
|
||||
|
||||
export default DragLayer(collectDragLayer)(DelayProfileDragPreview);
|
||||
@@ -0,0 +1,17 @@
|
||||
.delayProfileDragSource {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.delayProfilePlaceholder {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
border-bottom: 1px dotted #aaa;
|
||||
}
|
||||
|
||||
.delayProfilePlaceholderBefore {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.delayProfilePlaceholderAfter {
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
import classNames from 'classnames';
|
||||
import { DELAY_PROFILE } from 'Helpers/dragTypes';
|
||||
import DelayProfile from './DelayProfile';
|
||||
import styles from './DelayProfileDragSource.css';
|
||||
|
||||
const delayProfileDragSource = {
|
||||
beginDrag(item) {
|
||||
return item;
|
||||
},
|
||||
|
||||
endDrag(props, monitor, component) {
|
||||
props.onDelayProfileDragEnd(monitor.getItem(), monitor.didDrop());
|
||||
}
|
||||
};
|
||||
|
||||
const delayProfileDropTarget = {
|
||||
hover(props, monitor, component) {
|
||||
const dragIndex = monitor.getItem().order;
|
||||
const hoverIndex = props.order;
|
||||
|
||||
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
if (dragIndex === hoverIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When moving up, only trigger if drag position is above 50% and
|
||||
// when moving down, only trigger if drag position is below 50%.
|
||||
// If we're moving down the hoverIndex needs to be increased
|
||||
// by one so it's ordered properly. Otherwise the hoverIndex will work.
|
||||
|
||||
if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) {
|
||||
props.onDelayProfileDragMove(dragIndex, hoverIndex + 1);
|
||||
} else if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
|
||||
props.onDelayProfileDragMove(dragIndex, hoverIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function collectDragSource(connect, monitor) {
|
||||
return {
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
};
|
||||
}
|
||||
|
||||
function collectDropTarget(connect, monitor) {
|
||||
return {
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver()
|
||||
};
|
||||
}
|
||||
|
||||
class DelayProfileDragSource extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
order,
|
||||
isDragging,
|
||||
isDraggingUp,
|
||||
isDraggingDown,
|
||||
isOver,
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const isBefore = !isDragging && isDraggingUp && isOver;
|
||||
const isAfter = !isDragging && isDraggingDown && isOver;
|
||||
|
||||
// if (isDragging && !isOver) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
return connectDropTarget(
|
||||
<div
|
||||
className={classNames(
|
||||
styles.delayProfileDragSource,
|
||||
isBefore && styles.isDraggingUp,
|
||||
isAfter && styles.isDraggingDown
|
||||
)}
|
||||
>
|
||||
{
|
||||
isBefore &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.delayProfilePlaceholder,
|
||||
styles.delayProfilePlaceholderBefore
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
<DelayProfile
|
||||
id={id}
|
||||
order={order}
|
||||
isDragging={isDragging}
|
||||
isOver={isOver}
|
||||
{...otherProps}
|
||||
connectDragSource={connectDragSource}
|
||||
/>
|
||||
|
||||
{
|
||||
isAfter &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.delayProfilePlaceholder,
|
||||
styles.delayProfilePlaceholderAfter
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DelayProfileDragSource.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
order: PropTypes.number.isRequired,
|
||||
isDragging: PropTypes.bool,
|
||||
isDraggingUp: PropTypes.bool,
|
||||
isDraggingDown: PropTypes.bool,
|
||||
isOver: PropTypes.bool,
|
||||
connectDragSource: PropTypes.func,
|
||||
connectDropTarget: PropTypes.func,
|
||||
onDelayProfileDragMove: PropTypes.func.isRequired,
|
||||
onDelayProfileDragEnd: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DropTarget(
|
||||
DELAY_PROFILE,
|
||||
delayProfileDropTarget,
|
||||
collectDropTarget
|
||||
)(DragSource(
|
||||
DELAY_PROFILE,
|
||||
delayProfileDragSource,
|
||||
collectDragSource
|
||||
)(DelayProfileDragSource));
|
||||
@@ -0,0 +1,27 @@
|
||||
.delayProfiles {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.delayProfilesHeader {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 0 0 200px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.addDelayProfile {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
width: $dragHandleWidth;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Measure from 'Components/Measure';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import DelayProfileDragSource from './DelayProfileDragSource';
|
||||
import DelayProfileDragPreview from './DelayProfileDragPreview';
|
||||
import DelayProfile from './DelayProfile';
|
||||
import EditDelayProfileModalConnector from './EditDelayProfileModalConnector';
|
||||
import styles from './DelayProfiles.css';
|
||||
|
||||
class DelayProfiles extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddDelayProfileModalOpen: false,
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddDelayProfilePress = () => {
|
||||
this.setState({ isAddDelayProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ isAddDelayProfileModalOpen: false });
|
||||
}
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.setState({ width });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
defaultProfile,
|
||||
items,
|
||||
tagList,
|
||||
dragIndex,
|
||||
dropIndex,
|
||||
onConfirmDeleteDelayProfile,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isAddDelayProfileModalOpen,
|
||||
width
|
||||
} = this.state;
|
||||
|
||||
const isDragging = dropIndex !== null;
|
||||
const isDraggingUp = isDragging && dropIndex < dragIndex;
|
||||
const isDraggingDown = isDragging && dropIndex > dragIndex;
|
||||
|
||||
return (
|
||||
<Measure onMeasure={this.onMeasure}>
|
||||
<FieldSet legend="Delay Profiles">
|
||||
<PageSectionContent
|
||||
errorMessage="Unable to load Delay Profiles"
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.delayProfilesHeader}>
|
||||
<div className={styles.column}>Protocol</div>
|
||||
<div className={styles.column}>Usenet Delay</div>
|
||||
<div className={styles.column}>Torrent Delay</div>
|
||||
<div className={styles.tags}>Tags</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.delayProfiles}>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
return (
|
||||
<DelayProfileDragSource
|
||||
key={item.id}
|
||||
tagList={tagList}
|
||||
{...item}
|
||||
{...otherProps}
|
||||
index={index}
|
||||
isDragging={isDragging}
|
||||
isDraggingUp={isDraggingUp}
|
||||
isDraggingDown={isDraggingDown}
|
||||
onConfirmDeleteDelayProfile={onConfirmDeleteDelayProfile}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<DelayProfileDragPreview
|
||||
width={width}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
defaultProfile &&
|
||||
<div>
|
||||
<DelayProfile
|
||||
tagList={tagList}
|
||||
isDragging={false}
|
||||
onConfirmDeleteDelayProfile={onConfirmDeleteDelayProfile}
|
||||
{...defaultProfile}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.addDelayProfile}>
|
||||
<Link
|
||||
className={styles.addButton}
|
||||
onPress={this.onAddDelayProfilePress}
|
||||
>
|
||||
<Icon name={icons.ADD} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<EditDelayProfileModalConnector
|
||||
isOpen={isAddDelayProfileModalOpen}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DelayProfiles.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
defaultProfile: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dragIndex: PropTypes.number,
|
||||
dropIndex: PropTypes.number,
|
||||
onConfirmDeleteDelayProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DelayProfiles;
|
||||
@@ -0,0 +1,105 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDelayProfiles, deleteDelayProfile, reorderDelayProfile } from 'Store/Actions/settingsActions';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import DelayProfiles from './DelayProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.delayProfiles,
|
||||
createTagsSelector(),
|
||||
(delayProfiles, tagList) => {
|
||||
const defaultProfile = _.find(delayProfiles.items, { id: 1 });
|
||||
const items = _.sortBy(_.reject(delayProfiles.items, { id: 1 }), ['order']);
|
||||
|
||||
return {
|
||||
defaultProfile,
|
||||
...delayProfiles,
|
||||
items,
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchDelayProfiles,
|
||||
deleteDelayProfile,
|
||||
reorderDelayProfile
|
||||
};
|
||||
|
||||
class DelayProfilesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
dragIndex: null,
|
||||
dropIndex: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchDelayProfiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteDelayProfile = (id) => {
|
||||
this.props.deleteDelayProfile({ id });
|
||||
}
|
||||
|
||||
onDelayProfileDragMove = (dragIndex, dropIndex) => {
|
||||
if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) {
|
||||
this.setState({
|
||||
dragIndex,
|
||||
dropIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDelayProfileDragEnd = ({ id }, didDrop) => {
|
||||
const {
|
||||
dropIndex
|
||||
} = this.state;
|
||||
|
||||
if (didDrop && dropIndex !== null) {
|
||||
this.props.reorderDelayProfile({ id, moveIndex: dropIndex - 1 });
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dragIndex: null,
|
||||
dropIndex: null
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DelayProfiles
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
onConfirmDeleteDelayProfile={this.onConfirmDeleteDelayProfile}
|
||||
onDelayProfileDragMove={this.onDelayProfileDragMove}
|
||||
onDelayProfileDragEnd={this.onDelayProfileDragEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DelayProfilesConnector.propTypes = {
|
||||
fetchDelayProfiles: PropTypes.func.isRequired,
|
||||
deleteDelayProfile: PropTypes.func.isRequired,
|
||||
reorderDelayProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DelayProfilesConnector);
|
||||
@@ -0,0 +1,27 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditDelayProfileModalContentConnector from './EditDelayProfileModalContentConnector';
|
||||
|
||||
function EditDelayProfileModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditDelayProfileModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditDelayProfileModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditDelayProfileModal;
|
||||
@@ -0,0 +1,43 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditDelayProfileModal from './EditDelayProfileModal';
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditDelayProfileModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'settings.delayProfiles' });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditDelayProfileModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditDelayProfileModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EditDelayProfileModalConnector);
|
||||
@@ -0,0 +1,5 @@
|
||||
.deleteButton {
|
||||
composes: button from 'Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { boolSettingShape, numberSettingShape, tagSettingShape } from 'Helpers/Props/Shapes/settingShape';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Alert from 'Components/Alert';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import styles from './EditDelayProfileModalContent.css';
|
||||
|
||||
function EditDelayProfileModalContent(props) {
|
||||
const {
|
||||
id,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
protocol,
|
||||
protocolOptions,
|
||||
onInputChange,
|
||||
onProtocolChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
onDeleteDelayProfilePress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
enableUsenet,
|
||||
enableTorrent,
|
||||
usenetDelay,
|
||||
torrentDelay,
|
||||
tags
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id ? 'Edit Delay Profile' : 'Add Delay Profile'}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to add a new quality profile, please try again.</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Protocol</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="protocol"
|
||||
value={protocol}
|
||||
values={protocolOptions}
|
||||
helpText="Choose which protocol(s) to use and which one is preferred when choosing between otherwise equal releases"
|
||||
onChange={onProtocolChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
enableUsenet.value &&
|
||||
<FormGroup>
|
||||
<FormLabel>Usenet Delay</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="usenetDelay"
|
||||
unit="minutes"
|
||||
{...usenetDelay}
|
||||
helpText="Delay in minutes to wait before grabbing a release from Usenet"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
enableTorrent.value &&
|
||||
<FormGroup>
|
||||
<FormLabel>Torrent Delay</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="torrentDelay"
|
||||
unit="minutes"
|
||||
{...torrentDelay}
|
||||
helpText="Delay in minutes to wait before grabbing a torrent"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
id === 1 ?
|
||||
<Alert>
|
||||
This is the default profile. It applies to all series that don't have an explicit profile.
|
||||
</Alert> :
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
{...tags}
|
||||
helpText="Applies to series with at least one matching tag"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id && id > 1 &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteDelayProfilePress}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
Save
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
const delayProfileShape = {
|
||||
enableUsenet: PropTypes.shape(boolSettingShape).isRequired,
|
||||
enableTorrent: PropTypes.shape(boolSettingShape).isRequired,
|
||||
usenetDelay: PropTypes.shape(numberSettingShape).isRequired,
|
||||
torrentDelay: PropTypes.shape(numberSettingShape).isRequired,
|
||||
order: PropTypes.shape(numberSettingShape),
|
||||
tags: PropTypes.shape(tagSettingShape).isRequired
|
||||
};
|
||||
|
||||
EditDelayProfileModalContent.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.shape(delayProfileShape).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
protocolOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onProtocolChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteDelayProfilePress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditDelayProfileModalContent;
|
||||
@@ -0,0 +1,178 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { setDelayProfileValue, saveDelayProfile } from 'Store/Actions/settingsActions';
|
||||
import EditDelayProfileModalContent from './EditDelayProfileModalContent';
|
||||
|
||||
const newDelayProfile = {
|
||||
enableUsenet: true,
|
||||
enableTorrent: true,
|
||||
preferredProtocol: 'usenet',
|
||||
usenetDelay: 0,
|
||||
torrentDelay: 0,
|
||||
tags: []
|
||||
};
|
||||
|
||||
const protocolOptions = [
|
||||
{ key: 'preferUsenet', value: 'Prefer Usenet' },
|
||||
{ key: 'preferTorrent', value: 'Prefer Torrent' },
|
||||
{ key: 'onlyUsenet', value: 'Only Usenet' },
|
||||
{ key: 'onlyTorrent', value: 'Only Torrent' }
|
||||
];
|
||||
|
||||
function createDelayProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.settings.delayProfiles,
|
||||
(id, delayProfiles) => {
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges,
|
||||
items
|
||||
} = delayProfiles;
|
||||
|
||||
const profile = id ? _.find(items, { id }) : newDelayProfile;
|
||||
const settings = selectSettings(profile, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
id,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createDelayProfileSelector(),
|
||||
(delayProfile) => {
|
||||
const enableUsenet = delayProfile.item.enableUsenet.value;
|
||||
const enableTorrent = delayProfile.item.enableTorrent.value;
|
||||
const preferredProtocol = delayProfile.item.preferredProtocol.value;
|
||||
let protocol = 'preferUsenet';
|
||||
|
||||
if (preferredProtocol === 'usenet') {
|
||||
protocol = 'preferUsenet';
|
||||
} else {
|
||||
protocol = 'preferTorrent';
|
||||
}
|
||||
|
||||
if (!enableUsenet) {
|
||||
protocol = 'onlyTorrent';
|
||||
}
|
||||
|
||||
if (!enableTorrent) {
|
||||
protocol = 'onlyUsenet';
|
||||
}
|
||||
|
||||
return {
|
||||
protocol,
|
||||
protocolOptions,
|
||||
...delayProfile
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setDelayProfileValue,
|
||||
saveDelayProfile
|
||||
};
|
||||
|
||||
class EditDelayProfileModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.id) {
|
||||
Object.keys(newDelayProfile).forEach((name) => {
|
||||
this.props.setDelayProfileValue({
|
||||
name,
|
||||
value: newDelayProfile[name]
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setDelayProfileValue({ name, value });
|
||||
}
|
||||
|
||||
onProtocolChange = ({ value }) => {
|
||||
switch (value) {
|
||||
case 'preferUsenet':
|
||||
this.props.setDelayProfileValue({ name: 'enableUsenet', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'enableTorrent', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'usenet' });
|
||||
break;
|
||||
case 'preferTorrent':
|
||||
this.props.setDelayProfileValue({ name: 'enableUsenet', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'enableTorrent', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'torrent' });
|
||||
break;
|
||||
case 'onlyUsenet':
|
||||
this.props.setDelayProfileValue({ name: 'enableUsenet', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'enableTorrent', value: false });
|
||||
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'usenet' });
|
||||
break;
|
||||
case 'onlyTorrent':
|
||||
this.props.setDelayProfileValue({ name: 'enableUsenet', value: false });
|
||||
this.props.setDelayProfileValue({ name: 'enableTorrent', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'torrent' });
|
||||
break;
|
||||
default:
|
||||
throw Error(`Unknown protocol option: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveDelayProfile({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditDelayProfileModalContent
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
onProtocolChange={this.onProtocolChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditDelayProfileModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setDelayProfileValue: PropTypes.func.isRequired,
|
||||
saveDelayProfile: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditDelayProfileModalContentConnector);
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import QualityProfilesConnector from './Quality/QualityProfilesConnector';
|
||||
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
|
||||
|
||||
class Profiles extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageContent title="Profiles">
|
||||
<SettingsToolbarConnector
|
||||
showSave={false}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<QualityProfilesConnector />
|
||||
<DelayProfilesConnector />
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Only a single DragDropContext can exist so it's done here to allow editing
|
||||
// quality profiles and reordering delay profiles to work.
|
||||
|
||||
export default DragDropContext(HTML5Backend)(Profiles);
|
||||
@@ -0,0 +1,61 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditQualityProfileModalContentConnector from './EditQualityProfileModalContentConnector';
|
||||
|
||||
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}
|
||||
>
|
||||
<EditQualityProfileModalContentConnector
|
||||
{...otherProps}
|
||||
onContentHeightChange={this.onContentHeightChange}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditQualityProfileModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditQualityProfileModal;
|
||||
@@ -0,0 +1,43 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditQualityProfileModal from './EditQualityProfileModal';
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditQualityProfileModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'settings.qualityProfiles' });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditQualityProfileModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditQualityProfileModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EditQualityProfileModalConnector);
|
||||
@@ -0,0 +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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
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';
|
||||
import Measure from 'Components/Measure';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import QualityProfileItems from './QualityProfileItems';
|
||||
import styles from './EditQualityProfileModalContent.css';
|
||||
|
||||
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
|
||||
|
||||
class EditQualityProfileModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
headerHeight: 0,
|
||||
bodyHeight: 0,
|
||||
footerHeight: 0
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
headerHeight,
|
||||
bodyHeight,
|
||||
footerHeight
|
||||
} = this.state;
|
||||
|
||||
if (
|
||||
headerHeight > 0 &&
|
||||
bodyHeight > 0 &&
|
||||
footerHeight > 0 &&
|
||||
(
|
||||
headerHeight !== prevState.headerHeight ||
|
||||
bodyHeight !== prevState.bodyHeight ||
|
||||
footerHeight !== prevState.footerHeight
|
||||
)
|
||||
) {
|
||||
const padding = MODAL_BODY_PADDING * 2;
|
||||
|
||||
this.props.onContentHeightChange(
|
||||
headerHeight + bodyHeight + footerHeight + padding
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onHeaderMeasure = ({ height }) => {
|
||||
if (height > this.state.headerHeight) {
|
||||
this.setState({ headerHeight: height });
|
||||
}
|
||||
}
|
||||
|
||||
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 Radarr will no longer download movies"
|
||||
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>
|
||||
|
||||
<Measure
|
||||
whitelist={['height']}
|
||||
includeMargin={false}
|
||||
onMeasure={this.onFooterMeasure}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
|
||||
<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,
|
||||
saveError: PropTypes.object,
|
||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
isInUse: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onCutoffChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onContentHeightChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteQualityProfilePress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditQualityProfileModalContent;
|
||||
@@ -0,0 +1,442 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import { fetchQualityProfileSchema, setQualityProfileValue, saveQualityProfile } from 'Store/Actions/settingsActions';
|
||||
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('qualityProfiles'),
|
||||
(qualityProfile) => {
|
||||
const items = qualityProfile.item.items;
|
||||
if (!items || !items.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return _.reduceRight(items.value, (result, { allowed, id, name, quality }) => {
|
||||
if (allowed) {
|
||||
if (id) {
|
||||
result.push({
|
||||
key: id,
|
||||
value: name
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
key: quality.id,
|
||||
value: quality.name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createProviderSettingsSelector('qualityProfiles'),
|
||||
createQualitiesSelector(),
|
||||
createProfileInUseSelector('qualityProfileId'),
|
||||
(qualityProfile, qualities, isInUse) => {
|
||||
return {
|
||||
qualities,
|
||||
...qualityProfile,
|
||||
isInUse
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchQualityProfileSchema,
|
||||
setQualityProfileValue,
|
||||
saveQualityProfile
|
||||
};
|
||||
|
||||
class EditQualityProfileModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
dragQualityIndex: null,
|
||||
dropQualityIndex: null,
|
||||
dropPosition: null,
|
||||
editGroups: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.id && !this.props.isPopulated) {
|
||||
this.props.fetchQualityProfileSchema();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
ensureCutoff = (qualityProfile) => {
|
||||
const cutoff = qualityProfile.cutoff.value;
|
||||
|
||||
const cutoffItem = _.find(qualityProfile.items.value, (i) => {
|
||||
if (!cutoff) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return i.id === cutoff.id || (i.quality && i.quality.id === cutoff.id);
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setQualityProfileValue({ name, value });
|
||||
}
|
||||
|
||||
onCutoffChange = ({ name, value }) => {
|
||||
const id = parseInt(value);
|
||||
const item = _.find(this.props.item.items.value, (i) => {
|
||||
if (i.quality) {
|
||||
return i.quality.id === id;
|
||||
}
|
||||
|
||||
return i.id === id;
|
||||
});
|
||||
|
||||
const cutoffId = item.quality ? item.quality.id : item.id;
|
||||
|
||||
this.props.setQualityProfileValue({ name, value: cutoffId });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveQualityProfile({ id: this.props.id });
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
item.allowed = allowed;
|
||||
|
||||
this.props.setQualityProfileValue({
|
||||
name: 'items',
|
||||
value: items
|
||||
});
|
||||
|
||||
this.ensureCutoff(qualityProfile);
|
||||
}
|
||||
|
||||
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({
|
||||
dragQualityIndex,
|
||||
dropQualityIndex: adjustedDropQualityIndex,
|
||||
dropPosition
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onQualityProfileItemDragEnd = (didDrop) => {
|
||||
const {
|
||||
dragQualityIndex,
|
||||
dropQualityIndex
|
||||
} = this.state;
|
||||
|
||||
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);
|
||||
|
||||
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: items
|
||||
});
|
||||
|
||||
this.ensureCutoff(qualityProfile);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dragQualityIndex: null,
|
||||
dropQualityIndex: null,
|
||||
dropPosition: null
|
||||
});
|
||||
}
|
||||
|
||||
onToggleEditGroupsMode = () => {
|
||||
this.setState({ editGroups: !this.state.editGroups });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
if (_.isEmpty(this.props.item.items) && !this.props.isFetching) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EditQualityProfileModalContent
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditQualityProfileModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setQualityProfileValue: PropTypes.func.isRequired,
|
||||
fetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||
saveQualityProfile: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditQualityProfileModalContentConnector);
|
||||
@@ -0,0 +1,38 @@
|
||||
.qualityProfile {
|
||||
composes: card from 'Components/Card.css';
|
||||
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from 'Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.qualities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tooltipLabel {
|
||||
composes: label from 'Components/Label.css';
|
||||
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import EditQualityProfileModalConnector from './EditQualityProfileModalConnector';
|
||||
import styles from './QualityProfile.css';
|
||||
|
||||
class QualityProfile extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditQualityProfileModalOpen: false,
|
||||
isDeleteQualityProfileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditQualityProfilePress = () => {
|
||||
this.setState({ isEditQualityProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onEditQualityProfileModalClose = () => {
|
||||
this.setState({ isEditQualityProfileModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteQualityProfilePress = () => {
|
||||
this.setState({
|
||||
isEditQualityProfileModalOpen: false,
|
||||
isDeleteQualityProfileModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteQualityProfileModalClose = () => {
|
||||
this.setState({ isDeleteQualityProfileModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmDeleteQualityProfile = () => {
|
||||
this.props.onConfirmDeleteQualityProfile(this.props.id);
|
||||
}
|
||||
|
||||
onCloneQualityProfilePress = () => {
|
||||
const {
|
||||
id,
|
||||
onCloneQualityProfilePress
|
||||
} = this.props;
|
||||
|
||||
onCloneQualityProfilePress(id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
cutoff,
|
||||
items,
|
||||
isDeleting
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.qualityProfile}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditQualityProfilePress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title="Clone Profile"
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneQualityProfilePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.qualities}>
|
||||
{
|
||||
items.map((item) => {
|
||||
if (!item.allowed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item.quality) {
|
||||
const isCutoff = item.quality.id === cutoff.id;
|
||||
|
||||
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.id;
|
||||
|
||||
return (
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditQualityProfileModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditQualityProfileModalOpen}
|
||||
onModalClose={this.onEditQualityProfileModalClose}
|
||||
onDeleteQualityProfilePress={this.onDeleteQualityProfilePress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteQualityProfileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Quality Profile"
|
||||
message={`Are you sure you want to delete the quality profile '${name}'?`}
|
||||
confirmLabel="Delete"
|
||||
isSpinning={isDeleting}
|
||||
onConfirm={this.onConfirmDeleteQualityProfile}
|
||||
onCancel={this.onDeleteQualityProfileModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfile.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
cutoff: PropTypes.object.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteQualityProfile: PropTypes.func.isRequired,
|
||||
onCloneQualityProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QualityProfile;
|
||||
@@ -0,0 +1,85 @@
|
||||
.qualityProfileItem {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
|
||||
&.isInGroup {
|
||||
border-style: dashed;
|
||||
}
|
||||
}
|
||||
|
||||
.checkInputContainer {
|
||||
position: relative;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.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: $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;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
width: $dragHandleWidth;
|
||||
text-align: center;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.dragIcon {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.isDragging {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.isPreview {
|
||||
.qualityName {
|
||||
margin-left: 14px;
|
||||
|
||||
&.isInGroup {
|
||||
margin-left: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
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 IconButton from 'Components/Link/IconButton';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import styles from './QualityProfileItem.css';
|
||||
|
||||
class QualityProfileItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAllowedChange = ({ value }) => {
|
||||
const {
|
||||
qualityId,
|
||||
onQualityProfileItemAllowedChange
|
||||
} = this.props;
|
||||
|
||||
onQualityProfileItemAllowedChange(qualityId, value);
|
||||
}
|
||||
|
||||
onCreateGroupPress = () => {
|
||||
const {
|
||||
qualityId,
|
||||
onCreateGroupPress
|
||||
} = this.props;
|
||||
|
||||
onCreateGroupPress(qualityId);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
editGroups,
|
||||
isPreview,
|
||||
groupId,
|
||||
name,
|
||||
allowed,
|
||||
isDragging,
|
||||
isOverCurrent,
|
||||
connectDragSource
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileItem,
|
||||
isDragging && styles.isDragging,
|
||||
isPreview && styles.isPreview,
|
||||
isOverCurrent && styles.isOverCurrent,
|
||||
groupId && styles.isInGroup
|
||||
)}
|
||||
>
|
||||
<label
|
||||
className={styles.qualityNameContainer}
|
||||
>
|
||||
{
|
||||
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>
|
||||
|
||||
{
|
||||
connectDragSource(
|
||||
<div className={styles.dragHandle}>
|
||||
<Icon
|
||||
className={styles.dragIcon}
|
||||
title="Create group"
|
||||
name={icons.REORDER}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfileItem.propTypes = {
|
||||
editGroups: PropTypes.bool,
|
||||
isPreview: PropTypes.bool,
|
||||
groupId: PropTypes.number,
|
||||
qualityId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.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
|
||||
};
|
||||
|
||||
export default QualityProfileItem;
|
||||
@@ -0,0 +1,4 @@
|
||||
.dragPreview {
|
||||
width: 380px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DragLayer } from 'react-dnd';
|
||||
import dimensions from 'Styles/Variables/dimensions.js';
|
||||
import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes';
|
||||
import DragPreviewLayer from 'Components/DragPreviewLayer';
|
||||
import QualityProfileItem from './QualityProfileItem';
|
||||
import styles from './QualityProfileItemDragPreview.css';
|
||||
|
||||
const formGroupExtraSmallWidth = parseInt(dimensions.formGroupExtraSmallWidth);
|
||||
const formLabelSmallWidth = parseInt(dimensions.formLabelSmallWidth);
|
||||
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
||||
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
||||
|
||||
function collectDragLayer(monitor) {
|
||||
return {
|
||||
item: monitor.getItem(),
|
||||
itemType: monitor.getItemType(),
|
||||
currentOffset: monitor.getSourceClientOffset()
|
||||
};
|
||||
}
|
||||
|
||||
class QualityProfileItemDragPreview extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
item,
|
||||
itemType,
|
||||
currentOffset
|
||||
} = this.props;
|
||||
|
||||
if (!currentOffset || itemType !== QUALITY_PROFILE_ITEM) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The offset is shifted because the drag handle is on the right edge of the
|
||||
// list item and the preview is wider than the drag handle.
|
||||
|
||||
const { x, y } = currentOffset;
|
||||
const handleOffset = formGroupExtraSmallWidth - formLabelSmallWidth - formLabelRightMarginWidth - dragHandleWidth;
|
||||
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
WebkitTransform: transform,
|
||||
msTransform: transform,
|
||||
transform
|
||||
};
|
||||
|
||||
const {
|
||||
editGroups,
|
||||
groupId,
|
||||
qualityId,
|
||||
name,
|
||||
allowed
|
||||
} = item;
|
||||
|
||||
// TODO: Show a different preview for groups
|
||||
|
||||
return (
|
||||
<DragPreviewLayer>
|
||||
<div
|
||||
className={styles.dragPreview}
|
||||
style={style}
|
||||
>
|
||||
<QualityProfileItem
|
||||
editGroups={editGroups}
|
||||
isPreview={true}
|
||||
qualityId={groupId || qualityId}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
isDragging={false}
|
||||
/>
|
||||
</div>
|
||||
</DragPreviewLayer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfileItemDragPreview.propTypes = {
|
||||
item: PropTypes.object,
|
||||
itemType: PropTypes.string,
|
||||
currentOffset: PropTypes.shape({
|
||||
x: PropTypes.number.isRequired,
|
||||
y: PropTypes.number.isRequired
|
||||
})
|
||||
};
|
||||
|
||||
export default DragLayer(collectDragLayer)(QualityProfileItemDragPreview);
|
||||
@@ -0,0 +1,18 @@
|
||||
.qualityProfileItemDragSource {
|
||||
padding: $qualityProfileItemDragSourcePadding 0;
|
||||
}
|
||||
|
||||
.qualityProfileItemPlaceholder {
|
||||
width: 100%;
|
||||
height: $qualityProfileItemHeight;
|
||||
border: 1px dotted #aaa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.qualityProfileItemPlaceholderBefore {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.qualityProfileItemPlaceholderAfter {
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
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(props) {
|
||||
const {
|
||||
editGroups,
|
||||
qualityIndex,
|
||||
groupId,
|
||||
qualityId,
|
||||
name,
|
||||
allowed
|
||||
} = props;
|
||||
|
||||
return {
|
||||
editGroups,
|
||||
qualityIndex,
|
||||
groupId,
|
||||
qualityId,
|
||||
isGroup: !qualityId,
|
||||
name,
|
||||
allowed
|
||||
};
|
||||
},
|
||||
|
||||
endDrag(props, monitor, component) {
|
||||
props.onQualityProfileItemDragEnd(monitor.didDrop());
|
||||
}
|
||||
};
|
||||
|
||||
const qualityProfileItemDropTarget = {
|
||||
hover(props, monitor, component) {
|
||||
const {
|
||||
qualityIndex: dragQualityIndex,
|
||||
isGroup: isDragGroup
|
||||
} = monitor.getItem();
|
||||
|
||||
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;
|
||||
|
||||
// If we're hovering over a child don't trigger on the parent
|
||||
if (!monitor.isOver({ shallow: true })) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show targets for dropping on self
|
||||
if (dragQualityIndex === dropQualityIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function collectDragSource(connect, monitor) {
|
||||
return {
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
};
|
||||
}
|
||||
|
||||
function collectDropTarget(connect, monitor) {
|
||||
return {
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver(),
|
||||
isOverCurrent: monitor.isOver({ shallow: true })
|
||||
};
|
||||
}
|
||||
|
||||
class QualityProfileItemDragSource extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
editGroups,
|
||||
groupId,
|
||||
qualityId,
|
||||
name,
|
||||
allowed,
|
||||
items,
|
||||
qualityIndex,
|
||||
isDragging,
|
||||
isDraggingUp,
|
||||
isDraggingDown,
|
||||
isOverCurrent,
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
onCreateGroupPress,
|
||||
onDeleteGroupPress,
|
||||
onQualityProfileItemAllowedChange,
|
||||
onItemGroupAllowedChange,
|
||||
onItemGroupNameChange,
|
||||
onQualityProfileItemDragMove,
|
||||
onQualityProfileItemDragEnd
|
||||
} = this.props;
|
||||
|
||||
const isBefore = !isDragging && isDraggingUp && isOverCurrent;
|
||||
const isAfter = !isDragging && isDraggingDown && isOverCurrent;
|
||||
|
||||
return connectDropTarget(
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileItemDragSource,
|
||||
isBefore && styles.isDraggingUp,
|
||||
isAfter && styles.isDraggingDown
|
||||
)}
|
||||
>
|
||||
{
|
||||
isBefore &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileItemPlaceholder,
|
||||
styles.qualityProfileItemPlaceholderBefore
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!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 &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileItemPlaceholder,
|
||||
styles.qualityProfileItemPlaceholderAfter
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfileItemDragSource.propTypes = {
|
||||
editGroups: PropTypes.bool.isRequired,
|
||||
groupId: PropTypes.number,
|
||||
qualityId: PropTypes.number,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
qualityIndex: PropTypes.string.isRequired,
|
||||
isDragging: PropTypes.bool,
|
||||
isDraggingUp: PropTypes.bool,
|
||||
isDraggingDown: 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
|
||||
};
|
||||
|
||||
export default DropTarget(
|
||||
QUALITY_PROFILE_ITEM,
|
||||
qualityProfileItemDropTarget,
|
||||
collectDropTarget
|
||||
)(DragSource(
|
||||
QUALITY_PROFILE_ITEM,
|
||||
qualityProfileItemDragSource,
|
||||
collectDragSource
|
||||
)(QualityProfileItemDragSource));
|
||||
@@ -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: input 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;
|
||||
@@ -0,0 +1,15 @@
|
||||
.editGroupsButton {
|
||||
composes: button from 'Components/Link/Button.css';
|
||||
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.editGroupsButtonIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.qualities {
|
||||
margin-top: 10px;
|
||||
transition: min-height 200ms;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
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';
|
||||
import Measure from 'Components/Measure';
|
||||
import QualityProfileItemDragSource from './QualityProfileItemDragSource';
|
||||
import QualityProfileItemDragPreview from './QualityProfileItemDragPreview';
|
||||
import styles from './QualityProfileItems.css';
|
||||
|
||||
class QualityProfileItems extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
qualitiesHeight: 0,
|
||||
qualitiesHeightEditGroups: 0
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ height }) => {
|
||||
if (this.props.editGroups) {
|
||||
this.setState({
|
||||
qualitiesHeightEditGroups: height
|
||||
});
|
||||
} else {
|
||||
this.setState({ qualitiesHeight: height });
|
||||
}
|
||||
}
|
||||
|
||||
onToggleEditGroupsMode = () => {
|
||||
this.props.onToggleEditGroupsMode();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
editGroups,
|
||||
dropQualityIndex,
|
||||
dropPosition,
|
||||
qualityProfileItems,
|
||||
errors,
|
||||
warnings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
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 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"
|
||||
/>
|
||||
|
||||
{
|
||||
errors.map((error, index) => {
|
||||
return (
|
||||
<FormInputHelpText
|
||||
key={index}
|
||||
text={error.message}
|
||||
isError={true}
|
||||
isCheckInput={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
warnings.map((warning, index) => {
|
||||
return (
|
||||
<FormInputHelpText
|
||||
key={index}
|
||||
text={warning.message}
|
||||
isWarning={true}
|
||||
isCheckInput={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Button
|
||||
className={styles.editGroupsButton}
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={this.onToggleEditGroupsMode}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.editGroupsButtonIcon}
|
||||
name={editGroups ? icons.REORDER : icons.GROUP}
|
||||
/>
|
||||
|
||||
{
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfileItems.propTypes = {
|
||||
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),
|
||||
onToggleEditGroupsMode: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
QualityProfileItems.defaultProps = {
|
||||
errors: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
export default QualityProfileItems;
|
||||
@@ -0,0 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createQualityProfileSelector(),
|
||||
(qualityProfile) => {
|
||||
return {
|
||||
name: qualityProfile.name
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function QualityProfileNameConnector({ name, ...otherProps }) {
|
||||
return (
|
||||
<span>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
QualityProfileNameConnector.propTypes = {
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(QualityProfileNameConnector);
|
||||
@@ -0,0 +1,21 @@
|
||||
.qualityProfiles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.addQualityProfile {
|
||||
composes: qualityProfile from './QualityProfile.css';
|
||||
|
||||
background-color: $cardAlternateBackgroundColor;
|
||||
color: $gray;
|
||||
text-align: center;
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 0;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Card from 'Components/Card';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import QualityProfile from './QualityProfile';
|
||||
import EditQualityProfileModalConnector from './EditQualityProfileModalConnector';
|
||||
import styles from './QualityProfiles.css';
|
||||
|
||||
class QualityProfiles extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isQualityProfileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCloneQualityProfilePress = (id) => {
|
||||
this.props.onCloneQualityProfilePress(id);
|
||||
this.setState({ isQualityProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onEditQualityProfilePress = () => {
|
||||
this.setState({ isQualityProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ isQualityProfileModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
isDeleting,
|
||||
onConfirmDeleteQualityProfile,
|
||||
onCloneQualityProfilePress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend="Quality Profiles">
|
||||
<PageSectionContent
|
||||
errorMessage="Unable to load Quality Profiles"
|
||||
{...otherProps}c={true}
|
||||
>
|
||||
<div className={styles.qualityProfiles}>
|
||||
{
|
||||
items.sort(sortByName).map((item) => {
|
||||
return (
|
||||
<QualityProfile
|
||||
key={item.id}
|
||||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
onConfirmDeleteQualityProfile={onConfirmDeleteQualityProfile}
|
||||
onCloneQualityProfilePress={this.onCloneQualityProfilePress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addQualityProfile}
|
||||
onPress={this.onEditQualityProfilePress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<EditQualityProfileModalConnector
|
||||
isOpen={this.state.isQualityProfileModalOpen}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfiles.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteQualityProfile: PropTypes.func.isRequired,
|
||||
onCloneQualityProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QualityProfiles;
|
||||
@@ -0,0 +1,65 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchQualityProfiles, deleteQualityProfile, cloneQualityProfile } from 'Store/Actions/settingsActions';
|
||||
import QualityProfiles from './QualityProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
return {
|
||||
...qualityProfiles
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchQualityProfiles: fetchQualityProfiles,
|
||||
dispatchDeleteQualityProfile: deleteQualityProfile,
|
||||
dispatchCloneQualityProfile: cloneQualityProfile
|
||||
};
|
||||
|
||||
class QualityProfilesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchQualityProfiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteQualityProfile = (id) => {
|
||||
this.props.dispatchDeleteQualityProfile({ id });
|
||||
}
|
||||
|
||||
onCloneQualityProfilePress = (id) => {
|
||||
this.props.dispatchCloneQualityProfile({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QualityProfiles
|
||||
onConfirmDeleteQualityProfile={this.onConfirmDeleteQualityProfile}
|
||||
onCloneQualityProfilePress={this.onCloneQualityProfilePress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfilesConnector.propTypes = {
|
||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||
dispatchDeleteQualityProfile: PropTypes.func.isRequired,
|
||||
dispatchCloneQualityProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QualityProfilesConnector);
|
||||
Reference in New Issue
Block a user