mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-04-24 22:37:06 -04:00
Upgrade react-dnd and DnD Components to TypeScript
This commit is contained in:
parent
572bdc979c
commit
1bc1b080d1
85 changed files with 3525 additions and 4767 deletions
|
@ -39,7 +39,7 @@ export interface AutoTaggingSpecificationAppState
|
||||||
AppSectionSchemaState<AutoTaggingSpecification> {}
|
AppSectionSchemaState<AutoTaggingSpecification> {}
|
||||||
|
|
||||||
export interface DelayProfileAppState
|
export interface DelayProfileAppState
|
||||||
extends AppSectionState<DelayProfile>,
|
extends AppSectionListState<DelayProfile>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ export interface NotificationAppState
|
||||||
AppSectionDeleteState {}
|
AppSectionDeleteState {}
|
||||||
|
|
||||||
export interface QualityDefinitionsAppState
|
export interface QualityDefinitionsAppState
|
||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityDefinition>,
|
||||||
AppSectionSaveState {
|
AppSectionSaveState {
|
||||||
pendingChanges: {
|
pendingChanges: {
|
||||||
[key: number]: Partial<QualityProfile>;
|
[key: number]: Partial<QualityProfile>;
|
||||||
|
@ -86,7 +86,9 @@ export interface QualityDefinitionsAppState
|
||||||
|
|
||||||
export interface QualityProfilesAppState
|
export interface QualityProfilesAppState
|
||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionItemSchemaState<QualityProfile> {}
|
AppSectionItemSchemaState<QualityProfile>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface ReleaseProfilesAppState
|
export interface ReleaseProfilesAppState
|
||||||
extends AppSectionState<ReleaseProfile>,
|
extends AppSectionState<ReleaseProfile>,
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
.dragLayer {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'dragLayer': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,21 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import styles from './DragPreviewLayer.css';
|
|
||||||
|
|
||||||
interface DragPreviewLayerProps {
|
|
||||||
className?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DragPreviewLayer({
|
|
||||||
className = styles.dragLayer,
|
|
||||||
children,
|
|
||||||
...otherProps
|
|
||||||
}: DragPreviewLayerProps) {
|
|
||||||
return (
|
|
||||||
<div className={className} {...otherProps}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DragPreviewLayer;
|
|
|
@ -1,16 +1,21 @@
|
||||||
import React from 'react';
|
import React, { ForwardedRef, forwardRef, ReactNode } from 'react';
|
||||||
import styles from './ModalFooter.css';
|
import styles from './ModalFooter.css';
|
||||||
|
|
||||||
interface ModalFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface ModalFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
children?: React.ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModalFooter({ children, ...otherProps }: ModalFooterProps) {
|
const ModalFooter = forwardRef(
|
||||||
return (
|
(
|
||||||
<div className={styles.modalFooter} {...otherProps}>
|
{ children, ...otherProps }: ModalFooterProps,
|
||||||
{children}
|
ref: ForwardedRef<HTMLDivElement>
|
||||||
</div>
|
) => {
|
||||||
);
|
return (
|
||||||
}
|
<div ref={ref} className={styles.modalFooter} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default ModalFooter;
|
export default ModalFooter;
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import React from 'react';
|
import React, { ForwardedRef, forwardRef, ReactNode } from 'react';
|
||||||
import styles from './ModalHeader.css';
|
import styles from './ModalHeader.css';
|
||||||
|
|
||||||
interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
children?: React.ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModalHeader({ children, ...otherProps }: ModalHeaderProps) {
|
const ModalHeader = forwardRef(
|
||||||
return (
|
(
|
||||||
<div className={styles.modalHeader} {...otherProps}>
|
{ children, ...otherProps }: ModalHeaderProps,
|
||||||
{children}
|
ref: ForwardedRef<HTMLDivElement>
|
||||||
</div>
|
) => {
|
||||||
);
|
return (
|
||||||
}
|
<div ref={ref} className={styles.modalHeader} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default ModalHeader;
|
export default ModalHeader;
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
.columnContainer {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
@ -43,6 +47,17 @@
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notDragable {
|
.placeholder {
|
||||||
padding: 4px 0;
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px dotted #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholderBefore {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholderAfter {
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,14 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'checkContainer': string;
|
'checkContainer': string;
|
||||||
'column': string;
|
'column': string;
|
||||||
|
'columnContainer': string;
|
||||||
'dragHandle': string;
|
'dragHandle': string;
|
||||||
'dragIcon': string;
|
'dragIcon': string;
|
||||||
'isDragging': string;
|
'isDragging': string;
|
||||||
'label': string;
|
'label': string;
|
||||||
'notDragable': string;
|
'placeholder': string;
|
||||||
|
'placeholderAfter': string;
|
||||||
|
'placeholderBefore': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import styles from './TableOptionsColumn.css';
|
|
||||||
|
|
||||||
function TableOptionsColumn(props) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
isVisible,
|
|
||||||
isModifiable,
|
|
||||||
isDragging,
|
|
||||||
connectDragSource,
|
|
||||||
onVisibleChange
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={isModifiable ? undefined : styles.notDragable}>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.column,
|
|
||||||
isDragging && styles.isDragging
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
className={styles.label}
|
|
||||||
>
|
|
||||||
<CheckInput
|
|
||||||
containerClassName={styles.checkContainer}
|
|
||||||
name={name}
|
|
||||||
value={isVisible}
|
|
||||||
isDisabled={isModifiable === false}
|
|
||||||
onChange={onVisibleChange}
|
|
||||||
/>
|
|
||||||
{typeof label === 'function' ? label() : label}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{
|
|
||||||
!!connectDragSource &&
|
|
||||||
connectDragSource(
|
|
||||||
<div className={styles.dragHandle}>
|
|
||||||
<Icon
|
|
||||||
className={styles.dragIcon}
|
|
||||||
name={icons.REORDER}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TableOptionsColumn.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
|
||||||
isVisible: PropTypes.bool.isRequired,
|
|
||||||
isModifiable: PropTypes.bool.isRequired,
|
|
||||||
index: PropTypes.number.isRequired,
|
|
||||||
isDragging: PropTypes.bool,
|
|
||||||
connectDragSource: PropTypes.func,
|
|
||||||
onVisibleChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableOptionsColumn;
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { DragSourceMonitor, useDrag, useDrop, XYCoord } from 'react-dnd';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import DragType from 'Helpers/DragType';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import Column from '../Column';
|
||||||
|
import styles from './TableOptionsColumn.css';
|
||||||
|
|
||||||
|
interface DragItem {
|
||||||
|
name: string;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableOptionsColumnProps {
|
||||||
|
name: string;
|
||||||
|
label: Column['label'];
|
||||||
|
isDraggingDown: boolean;
|
||||||
|
isDraggingUp: boolean;
|
||||||
|
isVisible: boolean;
|
||||||
|
isModifiable: boolean;
|
||||||
|
index: number;
|
||||||
|
onVisibleChange: (change: CheckInputChanged) => void;
|
||||||
|
onColumnDragEnd: (didDrop: boolean) => void;
|
||||||
|
onColumnDragMove: (dragIndex: number, hoverIndex: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableOptionsColumn({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
index,
|
||||||
|
isDraggingDown,
|
||||||
|
isDraggingUp,
|
||||||
|
isVisible,
|
||||||
|
isModifiable,
|
||||||
|
onVisibleChange,
|
||||||
|
onColumnDragEnd,
|
||||||
|
onColumnDragMove,
|
||||||
|
}: TableOptionsColumnProps) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [{ isOver }, dropRef] = useDrop<DragItem, void, { isOver: boolean }>({
|
||||||
|
accept: DragType.TableColumn,
|
||||||
|
collect(monitor) {
|
||||||
|
return {
|
||||||
|
isOver: monitor.isOver(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hover(item: DragItem, monitor) {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isModifiable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragIndex = item.index;
|
||||||
|
const hoverIndex = index;
|
||||||
|
|
||||||
|
// Don't replace items with themselves
|
||||||
|
if (dragIndex === hoverIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine rectangle on screen
|
||||||
|
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Get vertical middle
|
||||||
|
const hoverMiddleY =
|
||||||
|
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||||
|
|
||||||
|
// Determine mouse position
|
||||||
|
const clientOffset = monitor.getClientOffset();
|
||||||
|
|
||||||
|
// Get pixels to the top
|
||||||
|
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// Dragging downwards
|
||||||
|
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dragging upwards
|
||||||
|
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onColumnDragMove(dragIndex, hoverIndex);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [{ isDragging }, dragRef, previewRef] = useDrag<
|
||||||
|
DragItem,
|
||||||
|
unknown,
|
||||||
|
{ isDragging: boolean }
|
||||||
|
>({
|
||||||
|
type: DragType.TableColumn,
|
||||||
|
item: () => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
collect: (monitor: DragSourceMonitor<unknown, unknown>) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
end: (_item: DragItem, monitor) => {
|
||||||
|
onColumnDragEnd(monitor.didDrop());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dropRef(previewRef(ref));
|
||||||
|
|
||||||
|
const isBefore = !isDragging && isDraggingUp && isOver;
|
||||||
|
const isAfter = !isDragging && isDraggingDown && isOver;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={styles.columnContainer}>
|
||||||
|
{isBefore ? (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.placeholder, styles.placeholderBefore)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(styles.column, isDragging && styles.isDragging)}
|
||||||
|
>
|
||||||
|
<label className={styles.label}>
|
||||||
|
<CheckInput
|
||||||
|
containerClassName={styles.checkContainer}
|
||||||
|
name={name}
|
||||||
|
value={isVisible}
|
||||||
|
isDisabled={isModifiable === false}
|
||||||
|
onChange={onVisibleChange}
|
||||||
|
/>
|
||||||
|
{typeof label === 'function' ? label() : label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{isModifiable ? (
|
||||||
|
<div ref={dragRef} className={styles.dragHandle}>
|
||||||
|
<Icon className={styles.dragIcon} name={icons.REORDER} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAfter ? (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.placeholder, styles.placeholderAfter)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableOptionsColumn;
|
|
@ -1,4 +0,0 @@
|
||||||
.dragPreview {
|
|
||||||
width: 380px;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'dragPreview': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,78 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { DragLayer } from 'react-dnd';
|
|
||||||
import DragPreviewLayer from 'Components/DragPreviewLayer';
|
|
||||||
import { TABLE_COLUMN } from 'Helpers/dragTypes';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions.js';
|
|
||||||
import TableOptionsColumn from './TableOptionsColumn';
|
|
||||||
import styles from './TableOptionsColumnDragPreview.css';
|
|
||||||
|
|
||||||
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
|
||||||
const formLabelLargeWidth = parseInt(dimensions.formLabelLargeWidth);
|
|
||||||
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
|
||||||
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
|
||||||
|
|
||||||
function collectDragLayer(monitor) {
|
|
||||||
return {
|
|
||||||
item: monitor.getItem(),
|
|
||||||
itemType: monitor.getItemType(),
|
|
||||||
currentOffset: monitor.getSourceClientOffset()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class TableOptionsColumnDragPreview extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
item,
|
|
||||||
itemType,
|
|
||||||
currentOffset
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!currentOffset || itemType !== TABLE_COLUMN) {
|
|
||||||
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 = formGroupSmallWidth - formLabelLargeWidth - formLabelRightMarginWidth - dragHandleWidth;
|
|
||||||
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
position: 'absolute',
|
|
||||||
WebkitTransform: transform,
|
|
||||||
msTransform: transform,
|
|
||||||
transform
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DragPreviewLayer>
|
|
||||||
<div
|
|
||||||
className={styles.dragPreview}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<TableOptionsColumn
|
|
||||||
isDragging={false}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DragPreviewLayer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TableOptionsColumnDragPreview.propTypes = {
|
|
||||||
item: PropTypes.object,
|
|
||||||
itemType: PropTypes.string,
|
|
||||||
currentOffset: PropTypes.shape({
|
|
||||||
x: PropTypes.number.isRequired,
|
|
||||||
y: PropTypes.number.isRequired
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragLayer(collectDragLayer)(TableOptionsColumnDragPreview);
|
|
|
@ -1,18 +0,0 @@
|
||||||
.columnDragSource {
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.columnPlaceholder {
|
|
||||||
width: 100%;
|
|
||||||
height: 36px;
|
|
||||||
border: 1px dotted #aaa;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.columnPlaceholderBefore {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.columnPlaceholderAfter {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'columnDragSource': string;
|
|
||||||
'columnPlaceholder': string;
|
|
||||||
'columnPlaceholderAfter': string;
|
|
||||||
'columnPlaceholderBefore': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,164 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { DragSource, DropTarget } from 'react-dnd';
|
|
||||||
import { findDOMNode } from 'react-dom';
|
|
||||||
import { TABLE_COLUMN } from 'Helpers/dragTypes';
|
|
||||||
import TableOptionsColumn from './TableOptionsColumn';
|
|
||||||
import styles from './TableOptionsColumnDragSource.css';
|
|
||||||
|
|
||||||
const columnDragSource = {
|
|
||||||
beginDrag(column) {
|
|
||||||
return column;
|
|
||||||
},
|
|
||||||
|
|
||||||
endDrag(props, monitor, component) {
|
|
||||||
props.onColumnDragEnd(monitor.getItem(), monitor.didDrop());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columnDropTarget = {
|
|
||||||
hover(props, monitor, component) {
|
|
||||||
const dragIndex = monitor.getItem().index;
|
|
||||||
const hoverIndex = props.index;
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
// Dragging downwards
|
|
||||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dragging upwards
|
|
||||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
props.onColumnDragMove(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 TableOptionsColumnDragSource extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
isVisible,
|
|
||||||
isModifiable,
|
|
||||||
index,
|
|
||||||
isDragging,
|
|
||||||
isDraggingUp,
|
|
||||||
isDraggingDown,
|
|
||||||
isOver,
|
|
||||||
connectDragSource,
|
|
||||||
connectDropTarget,
|
|
||||||
onVisibleChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isBefore = !isDragging && isDraggingUp && isOver;
|
|
||||||
const isAfter = !isDragging && isDraggingDown && isOver;
|
|
||||||
|
|
||||||
// if (isDragging && !isOver) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
return connectDropTarget(
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.columnDragSource,
|
|
||||||
isBefore && styles.isDraggingUp,
|
|
||||||
isAfter && styles.isDraggingDown
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isBefore &&
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.columnPlaceholder,
|
|
||||||
styles.columnPlaceholderBefore
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<TableOptionsColumn
|
|
||||||
name={name}
|
|
||||||
label={typeof label === 'function' ? label() : label}
|
|
||||||
isVisible={isVisible}
|
|
||||||
isModifiable={isModifiable}
|
|
||||||
index={index}
|
|
||||||
isDragging={isDragging}
|
|
||||||
isOver={isOver}
|
|
||||||
connectDragSource={connectDragSource}
|
|
||||||
onVisibleChange={onVisibleChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
isAfter &&
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.columnPlaceholder,
|
|
||||||
styles.columnPlaceholderAfter
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TableOptionsColumnDragSource.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
|
||||||
isVisible: PropTypes.bool.isRequired,
|
|
||||||
isModifiable: PropTypes.bool.isRequired,
|
|
||||||
index: PropTypes.number.isRequired,
|
|
||||||
isDragging: PropTypes.bool,
|
|
||||||
isDraggingUp: PropTypes.bool,
|
|
||||||
isDraggingDown: PropTypes.bool,
|
|
||||||
isOver: PropTypes.bool,
|
|
||||||
connectDragSource: PropTypes.func,
|
|
||||||
connectDropTarget: PropTypes.func,
|
|
||||||
onVisibleChange: PropTypes.func.isRequired,
|
|
||||||
onColumnDragMove: PropTypes.func.isRequired,
|
|
||||||
onColumnDragEnd: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DropTarget(
|
|
||||||
TABLE_COLUMN,
|
|
||||||
columnDropTarget,
|
|
||||||
collectDropTarget
|
|
||||||
)(DragSource(
|
|
||||||
TABLE_COLUMN,
|
|
||||||
columnDragSource,
|
|
||||||
collectDragSource
|
|
||||||
)(TableOptionsColumnDragSource));
|
|
|
@ -1,263 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { DndProvider } from 'react-dnd-multi-backend';
|
|
||||||
import HTML5toTouch from 'react-dnd-multi-backend/dist/esm/HTML5toTouch';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import TableOptionsColumn from './TableOptionsColumn';
|
|
||||||
import TableOptionsColumnDragPreview from './TableOptionsColumnDragPreview';
|
|
||||||
import TableOptionsColumnDragSource from './TableOptionsColumnDragSource';
|
|
||||||
import styles from './TableOptionsModal.css';
|
|
||||||
|
|
||||||
class TableOptionsModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
hasPageSize: !!props.pageSize,
|
|
||||||
pageSize: props.pageSize,
|
|
||||||
pageSizeError: null,
|
|
||||||
dragIndex: null,
|
|
||||||
dropIndex: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.pageSize !== this.state.pageSize) {
|
|
||||||
this.setState({ pageSize: this.props.pageSize });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPageSizeChange = ({ value }) => {
|
|
||||||
let pageSizeError = null;
|
|
||||||
const maxPageSize = this.props.maxPageSize ?? 250;
|
|
||||||
|
|
||||||
if (value < 5) {
|
|
||||||
pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' });
|
|
||||||
} else if (value > maxPageSize) {
|
|
||||||
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: `${maxPageSize}` });
|
|
||||||
} else {
|
|
||||||
this.props.onTableOptionChange({ pageSize: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
pageSize: value,
|
|
||||||
pageSizeError
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onVisibleChange = ({ name, value }) => {
|
|
||||||
const columns = _.cloneDeep(this.props.columns);
|
|
||||||
|
|
||||||
const column = _.find(columns, { name });
|
|
||||||
column.isVisible = value;
|
|
||||||
|
|
||||||
this.props.onTableOptionChange({ columns });
|
|
||||||
};
|
|
||||||
|
|
||||||
onColumnDragMove = (dragIndex, dropIndex) => {
|
|
||||||
if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) {
|
|
||||||
this.setState({
|
|
||||||
dragIndex,
|
|
||||||
dropIndex
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onColumnDragEnd = ({ id }, didDrop) => {
|
|
||||||
const {
|
|
||||||
dragIndex,
|
|
||||||
dropIndex
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (didDrop && dropIndex !== null) {
|
|
||||||
const columns = _.cloneDeep(this.props.columns);
|
|
||||||
const items = columns.splice(dragIndex, 1);
|
|
||||||
columns.splice(dropIndex, 0, items[0]);
|
|
||||||
|
|
||||||
this.props.onTableOptionChange({ columns });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
dragIndex: null,
|
|
||||||
dropIndex: null
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
columns,
|
|
||||||
canModifyColumns,
|
|
||||||
optionsComponent: OptionsComponent,
|
|
||||||
onTableOptionChange,
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
hasPageSize,
|
|
||||||
pageSize,
|
|
||||||
pageSizeError,
|
|
||||||
dragIndex,
|
|
||||||
dropIndex
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isDragging = dropIndex !== null;
|
|
||||||
const isDraggingUp = isDragging && dropIndex < dragIndex;
|
|
||||||
const isDraggingDown = isDragging && dropIndex > dragIndex;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndProvider options={HTML5toTouch}>
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isOpen ?
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('TableOptions')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Form>
|
|
||||||
{
|
|
||||||
hasPageSize ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('TablePageSize')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.NUMBER}
|
|
||||||
name="pageSize"
|
|
||||||
value={pageSize || 0}
|
|
||||||
helpText={translate('TablePageSizeHelpText')}
|
|
||||||
errors={pageSizeError ? [{ message: pageSizeError }] : undefined}
|
|
||||||
onChange={this.onPageSizeChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
OptionsComponent ?
|
|
||||||
<OptionsComponent
|
|
||||||
onTableOptionChange={onTableOptionChange}
|
|
||||||
/> : null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
canModifyColumns ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('TableColumns')}</FormLabel>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<FormInputHelpText
|
|
||||||
text={translate('TableColumnsHelpText')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.columns}>
|
|
||||||
{
|
|
||||||
columns.map((column, index) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
columnLabel,
|
|
||||||
isVisible,
|
|
||||||
isModifiable
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (isModifiable !== false) {
|
|
||||||
return (
|
|
||||||
<TableOptionsColumnDragSource
|
|
||||||
key={name}
|
|
||||||
name={name}
|
|
||||||
label={columnLabel || label}
|
|
||||||
isVisible={isVisible}
|
|
||||||
isModifiable={true}
|
|
||||||
index={index}
|
|
||||||
isDragging={isDragging}
|
|
||||||
isDraggingUp={isDraggingUp}
|
|
||||||
isDraggingDown={isDraggingDown}
|
|
||||||
onVisibleChange={this.onVisibleChange}
|
|
||||||
onColumnDragMove={this.onColumnDragMove}
|
|
||||||
onColumnDragEnd={this.onColumnDragEnd}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableOptionsColumn
|
|
||||||
key={name}
|
|
||||||
name={name}
|
|
||||||
label={columnLabel || label}
|
|
||||||
isVisible={isVisible}
|
|
||||||
index={index}
|
|
||||||
isModifiable={false}
|
|
||||||
onVisibleChange={this.onVisibleChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<TableOptionsColumnDragPreview />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</Modal>
|
|
||||||
</DndProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TableOptionsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
pageSize: PropTypes.number,
|
|
||||||
maxPageSize: PropTypes.number,
|
|
||||||
canModifyColumns: PropTypes.bool.isRequired,
|
|
||||||
optionsComponent: PropTypes.elementType,
|
|
||||||
onTableOptionChange: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
TableOptionsModal.defaultProps = {
|
|
||||||
canModifyColumns: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableOptionsModal;
|
|
216
frontend/src/Components/Table/TableOptions/TableOptionsModal.tsx
Normal file
216
frontend/src/Components/Table/TableOptions/TableOptionsModal.tsx
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { HTML5toTouch } from 'rdndmb-html5-to-touch';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { DndProvider } from 'react-dnd-multi-backend';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import { CheckInputChanged, InputChanged } from 'typings/inputs';
|
||||||
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import Column from '../Column';
|
||||||
|
import TableOptionsColumn from './TableOptionsColumn';
|
||||||
|
import styles from './TableOptionsModal.css';
|
||||||
|
|
||||||
|
interface TableOptionsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
columns: Column[];
|
||||||
|
pageSize?: number;
|
||||||
|
maxPageSize?: number;
|
||||||
|
canModifyColumns: boolean;
|
||||||
|
optionsComponent?: React.ElementType;
|
||||||
|
onTableOptionChange: (payload: TableOptionsChangePayload) => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableOptionsModal({
|
||||||
|
isOpen,
|
||||||
|
columns,
|
||||||
|
canModifyColumns = true,
|
||||||
|
optionsComponent: OptionsComponent,
|
||||||
|
pageSize: propsPageSize,
|
||||||
|
maxPageSize = 250,
|
||||||
|
onTableOptionChange,
|
||||||
|
onModalClose,
|
||||||
|
}: TableOptionsModalProps) {
|
||||||
|
const [pageSize, setPageSize] = useState(propsPageSize);
|
||||||
|
const [pageSizeError, setPageSizeError] = useState<string | null>(null);
|
||||||
|
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
||||||
|
const [dropIndex, setDropIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const hasPageSize = !!propsPageSize;
|
||||||
|
const isDragging = dropIndex !== null;
|
||||||
|
const isDraggingUp =
|
||||||
|
isDragging &&
|
||||||
|
dropIndex != null &&
|
||||||
|
dragIndex != null &&
|
||||||
|
dropIndex < dragIndex;
|
||||||
|
const isDraggingDown =
|
||||||
|
isDragging &&
|
||||||
|
dropIndex != null &&
|
||||||
|
dragIndex != null &&
|
||||||
|
dropIndex > dragIndex;
|
||||||
|
|
||||||
|
const handlePageSizeChange = useCallback(
|
||||||
|
({ value }: InputChanged<number>) => {
|
||||||
|
let error: string | null = null;
|
||||||
|
|
||||||
|
if (value < 5) {
|
||||||
|
error = translate('TablePageSizeMinimum', {
|
||||||
|
minimumValue: '5',
|
||||||
|
});
|
||||||
|
} else if (value > maxPageSize) {
|
||||||
|
error = translate('TablePageSizeMaximum', {
|
||||||
|
maximumValue: `${maxPageSize}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onTableOptionChange({ pageSize: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
setPageSize(value);
|
||||||
|
setPageSizeError(error);
|
||||||
|
},
|
||||||
|
[maxPageSize, onTableOptionChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleVisibleChange = useCallback(
|
||||||
|
({ name, value }: CheckInputChanged) => {
|
||||||
|
const newColumns = columns.map((column) => {
|
||||||
|
if (column.name === name) {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
isVisible: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
|
||||||
|
onTableOptionChange({ columns: newColumns });
|
||||||
|
},
|
||||||
|
[columns, onTableOptionChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleColumnDragMove = useCallback(
|
||||||
|
(newDragIndex: number, newDropIndex: number) => {
|
||||||
|
setDropIndex(newDropIndex);
|
||||||
|
setDragIndex(newDragIndex);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleColumnDragEnd = useCallback(
|
||||||
|
(didDrop: boolean) => {
|
||||||
|
if (didDrop && dragIndex && dropIndex !== null) {
|
||||||
|
const newColumns = [...columns];
|
||||||
|
const items = newColumns.splice(dragIndex, 1);
|
||||||
|
newColumns.splice(dropIndex, 0, items[0]);
|
||||||
|
|
||||||
|
onTableOptionChange({ columns: newColumns });
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragIndex(null);
|
||||||
|
setDropIndex(null);
|
||||||
|
},
|
||||||
|
[dragIndex, dropIndex, columns, onTableOptionChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPageSize(propsPageSize);
|
||||||
|
}, [propsPageSize]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndProvider options={HTML5toTouch}>
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
{isOpen ? (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('TableOptions')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
{hasPageSize ? (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('TablePageSize')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="pageSize"
|
||||||
|
value={pageSize || 0}
|
||||||
|
helpText={translate('TablePageSizeHelpText')}
|
||||||
|
errors={
|
||||||
|
pageSizeError ? [{ message: pageSizeError }] : undefined
|
||||||
|
}
|
||||||
|
onChange={handlePageSizeChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{OptionsComponent ? (
|
||||||
|
<OptionsComponent onTableOptionChange={onTableOptionChange} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{canModifyColumns ? (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('TableColumns')}</FormLabel>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormInputHelpText
|
||||||
|
text={translate('TableColumnsHelpText')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.columns}>
|
||||||
|
{columns.map((column, index) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
columnLabel,
|
||||||
|
isVisible,
|
||||||
|
isModifiable = true,
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableOptionsColumn
|
||||||
|
key={name}
|
||||||
|
name={name}
|
||||||
|
label={columnLabel ?? label}
|
||||||
|
isVisible={isVisible}
|
||||||
|
isModifiable={isModifiable}
|
||||||
|
index={index}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
onVisibleChange={handleVisibleChange}
|
||||||
|
onColumnDragMove={handleColumnDragMove}
|
||||||
|
onColumnDragEnd={handleColumnDragEnd}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
|
</DndProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TableOptionsModal.defaultProps = {
|
||||||
|
canModifyColumns: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableOptionsModal;
|
|
@ -11,7 +11,7 @@ import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
import Series from 'Series/Series';
|
import Series from 'Series/Series';
|
||||||
import useSeries from 'Series/useSeries';
|
import useSeries from 'Series/useSeries';
|
||||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileName';
|
||||||
import {
|
import {
|
||||||
deleteEpisodeFile,
|
deleteEpisodeFile,
|
||||||
fetchEpisodeFile,
|
fetchEpisodeFile,
|
||||||
|
|
7
frontend/src/Helpers/DragType.ts
Normal file
7
frontend/src/Helpers/DragType.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
enum DragType {
|
||||||
|
DelayProfile = 'delayProfile',
|
||||||
|
QualityProfileItem = 'qualityProfileItem',
|
||||||
|
TableColumn = 'tableColumn',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DragType;
|
|
@ -1,3 +0,0 @@
|
||||||
export const QUALITY_PROFILE_ITEM = 'qualityProfileItem';
|
|
||||||
export const DELAY_PROFILE = 'delayProfile';
|
|
||||||
export const TABLE_COLUMN = 'tableColumn';
|
|
|
@ -20,6 +20,9 @@ interface Quality {
|
||||||
name: string;
|
name: string;
|
||||||
resolution: number;
|
resolution: number;
|
||||||
source: QualitySource;
|
source: QualitySource;
|
||||||
|
minSize: number | null;
|
||||||
|
maxSize: number | null;
|
||||||
|
preferredSize: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QualityModel {
|
export interface QualityModel {
|
||||||
|
|
|
@ -30,7 +30,7 @@ import MonitoringOptionsModal from 'Series/MonitoringOptions/MonitoringOptionsMo
|
||||||
import SeriesGenres from 'Series/SeriesGenres';
|
import SeriesGenres from 'Series/SeriesGenres';
|
||||||
import SeriesPoster from 'Series/SeriesPoster';
|
import SeriesPoster from 'Series/SeriesPoster';
|
||||||
import { getSeriesStatusDetails } from 'Series/SeriesStatus';
|
import { getSeriesStatusDetails } from 'Series/SeriesStatus';
|
||||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileName';
|
||||||
import fonts from 'Styles/Variables/fonts';
|
import fonts from 'Styles/Variables/fonts';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
|
@ -38,3 +38,17 @@
|
||||||
width: $dragHandleWidth;
|
width: $dragHandleWidth;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
border-bottom: 1px dotted #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholderBefore {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholderAfter {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@ interface CssExports {
|
||||||
'dragIcon': string;
|
'dragIcon': string;
|
||||||
'editButton': string;
|
'editButton': string;
|
||||||
'isDragging': string;
|
'isDragging': string;
|
||||||
|
'placeholder': string;
|
||||||
|
'placeholderAfter': string;
|
||||||
|
'placeholderBefore': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
|
@ -1,173 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import TagList from 'Components/TagList';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EditDelayProfileModalConnector from './EditDelayProfileModalConnector';
|
|
||||||
import styles from './DelayProfile.css';
|
|
||||||
|
|
||||||
function getDelay(enabled, delay) {
|
|
||||||
if (!enabled) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!delay) {
|
|
||||||
return translate('NoDelay');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delay === 1) {
|
|
||||||
return translate('OneMinute');
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: use better units of time than just minutes
|
|
||||||
return translate('DelayMinutes', { delay });
|
|
||||||
}
|
|
||||||
|
|
||||||
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(translate('PreferProtocol', { preferredProtocol }));
|
|
||||||
|
|
||||||
if (!enableUsenet) {
|
|
||||||
preferred = translate('OnlyTorrent');
|
|
||||||
} else if (!enableTorrent) {
|
|
||||||
preferred = translate('OnlyUsenet');
|
|
||||||
}
|
|
||||||
|
|
||||||
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={translate('DeleteDelayProfile')}
|
|
||||||
message={translate('DeleteDelayProfileMessageText')}
|
|
||||||
confirmLabel={translate('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;
|
|
240
frontend/src/Settings/Profiles/Delay/DelayProfile.tsx
Normal file
240
frontend/src/Settings/Profiles/Delay/DelayProfile.tsx
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { DragSourceMonitor, useDrag, useDrop, XYCoord } from 'react-dnd';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { Tag } from 'App/State/TagsAppState';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TagList from 'Components/TagList';
|
||||||
|
import DragType from 'Helpers/DragType';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import { deleteDelayProfile } from 'Store/Actions/settingsActions';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EditDelayProfileModal from './EditDelayProfileModal';
|
||||||
|
import styles from './DelayProfile.css';
|
||||||
|
|
||||||
|
function getDelay(enabled: boolean, delay: number) {
|
||||||
|
if (!enabled) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!delay) {
|
||||||
|
return translate('NoDelay');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay === 1) {
|
||||||
|
return translate('OneMinute');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use better units of time than just minutes
|
||||||
|
return translate('DelayMinutes', { delay });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DragItem {
|
||||||
|
id: number;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DelayProfileProps {
|
||||||
|
id: number;
|
||||||
|
enableUsenet: boolean;
|
||||||
|
enableTorrent: boolean;
|
||||||
|
preferredProtocol: string;
|
||||||
|
usenetDelay: number;
|
||||||
|
torrentDelay: number;
|
||||||
|
order: number;
|
||||||
|
tags: number[];
|
||||||
|
tagList: Tag[];
|
||||||
|
isDraggingDown: boolean;
|
||||||
|
isDraggingUp: boolean;
|
||||||
|
onDelayProfileDragEnd: (id: number, didDrop: boolean) => void;
|
||||||
|
onDelayProfileDragMove: (dragIndex: number, hoverIndex: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DelayProfile({
|
||||||
|
id,
|
||||||
|
enableUsenet,
|
||||||
|
enableTorrent,
|
||||||
|
preferredProtocol,
|
||||||
|
usenetDelay,
|
||||||
|
torrentDelay,
|
||||||
|
order,
|
||||||
|
tags,
|
||||||
|
tagList,
|
||||||
|
isDraggingDown,
|
||||||
|
isDraggingUp,
|
||||||
|
onDelayProfileDragEnd,
|
||||||
|
onDelayProfileDragMove,
|
||||||
|
}: DelayProfileProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [isEditDelayProfileModalOpen, setIsEditDelayProfileModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const [isDeleteDelayProfileModalOpen, setIsDeleteDelayProfileModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const preferred = useMemo(() => {
|
||||||
|
if (!enableUsenet) {
|
||||||
|
return translate('OnlyTorrent');
|
||||||
|
} else if (!enableTorrent) {
|
||||||
|
return translate('OnlyUsenet');
|
||||||
|
}
|
||||||
|
|
||||||
|
return titleCase(translate('PreferProtocol', { preferredProtocol }));
|
||||||
|
}, [preferredProtocol, enableUsenet, enableTorrent]);
|
||||||
|
|
||||||
|
const handleEditDelayProfilePress = useCallback(() => {
|
||||||
|
setIsEditDelayProfileModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEditDelayProfileModalClose = useCallback(() => {
|
||||||
|
setIsEditDelayProfileModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteDelayProfilePress = useCallback(() => {
|
||||||
|
setIsEditDelayProfileModalOpen(false);
|
||||||
|
setIsDeleteDelayProfileModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteDelayProfileModalClose = useCallback(() => {
|
||||||
|
setIsDeleteDelayProfileModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleConfirmDeleteDelayProfile = useCallback(() => {
|
||||||
|
dispatch(deleteDelayProfile(id));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
const [{ isOver }, dropRef] = useDrop<DragItem, void, { isOver: boolean }>({
|
||||||
|
accept: DragType.DelayProfile,
|
||||||
|
collect(monitor) {
|
||||||
|
return {
|
||||||
|
isOver: monitor.isOver(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hover(item: DragItem, monitor) {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dragIndex = item.order;
|
||||||
|
const hoverIndex = order;
|
||||||
|
|
||||||
|
// Don't replace items with themselves
|
||||||
|
if (dragIndex === hoverIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||||
|
const hoverMiddleY =
|
||||||
|
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||||
|
const clientOffset = monitor.getClientOffset();
|
||||||
|
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
onDelayProfileDragMove(dragIndex, hoverIndex + 1);
|
||||||
|
} else if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
|
||||||
|
onDelayProfileDragMove(dragIndex, hoverIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [{ isDragging }, dragRef, previewRef] = useDrag<
|
||||||
|
DragItem,
|
||||||
|
unknown,
|
||||||
|
{ isDragging: boolean }
|
||||||
|
>({
|
||||||
|
type: DragType.DelayProfile,
|
||||||
|
item: () => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
order,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
collect: (monitor: DragSourceMonitor<unknown, unknown>) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
end: (item: DragItem, monitor) => {
|
||||||
|
onDelayProfileDragEnd(item.id, monitor.didDrop());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dropRef(previewRef(ref));
|
||||||
|
|
||||||
|
const isBefore = !isDragging && isDraggingUp && isOver;
|
||||||
|
const isAfter = !isDragging && isDraggingDown && isOver;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={id === 1 ? undefined : ref}>
|
||||||
|
{isBefore ? (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.placeholder, styles.placeholderBefore)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<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={handleEditDelayProfilePress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.EDIT} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{id === 1 ? null : (
|
||||||
|
<div ref={dragRef} className={styles.dragHandle}>
|
||||||
|
<Icon className={styles.dragIcon} name={icons.REORDER} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAfter ? (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.placeholder, styles.placeholderAfter)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<EditDelayProfileModal
|
||||||
|
id={id}
|
||||||
|
isOpen={isEditDelayProfileModalOpen}
|
||||||
|
onModalClose={handleEditDelayProfileModalClose}
|
||||||
|
onDeleteDelayProfilePress={handleDeleteDelayProfilePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteDelayProfileModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteDelayProfile')}
|
||||||
|
message={translate('DeleteDelayProfileMessageText')}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={handleConfirmDeleteDelayProfile}
|
||||||
|
onCancel={handleDeleteDelayProfileModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DelayProfile;
|
|
@ -1,3 +0,0 @@
|
||||||
.dragPreview {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'dragPreview': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,78 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { DragLayer } from 'react-dnd';
|
|
||||||
import DragPreviewLayer from 'Components/DragPreviewLayer';
|
|
||||||
import { DELAY_PROFILE } from 'Helpers/dragTypes';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions.js';
|
|
||||||
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);
|
|
|
@ -1,17 +0,0 @@
|
||||||
.delayProfileDragSource {
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delayProfilePlaceholder {
|
|
||||||
width: 100%;
|
|
||||||
height: 30px;
|
|
||||||
border-bottom: 1px dotted #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delayProfilePlaceholderBefore {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delayProfilePlaceholderAfter {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'delayProfileDragSource': string;
|
|
||||||
'delayProfilePlaceholder': string;
|
|
||||||
'delayProfilePlaceholderAfter': string;
|
|
||||||
'delayProfilePlaceholderBefore': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,148 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { DragSource, DropTarget } from 'react-dnd';
|
|
||||||
import { findDOMNode } from 'react-dom';
|
|
||||||
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));
|
|
|
@ -1,169 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
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 Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import { icons, scrollDirections } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import DelayProfile from './DelayProfile';
|
|
||||||
import DelayProfileDragPreview from './DelayProfileDragPreview';
|
|
||||||
import DelayProfileDragSource from './DelayProfileDragSource';
|
|
||||||
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={translate('DelayProfiles')}>
|
|
||||||
<PageSectionContent
|
|
||||||
errorMessage={translate('DelayProfilesLoadError')}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<Scroller
|
|
||||||
className={styles.horizontalScroll}
|
|
||||||
scrollDirection={
|
|
||||||
scrollDirections.HORIZONTAL
|
|
||||||
}
|
|
||||||
autoFocus={false}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div className={styles.delayProfilesHeader}>
|
|
||||||
<div className={styles.column}>
|
|
||||||
{translate('PreferredProtocol')}
|
|
||||||
</div>
|
|
||||||
<div className={styles.column}>
|
|
||||||
{translate('UsenetDelay')}
|
|
||||||
</div>
|
|
||||||
<div className={styles.column}>
|
|
||||||
{translate('TorrentDelay')}
|
|
||||||
</div>
|
|
||||||
<div className={styles.tags}>
|
|
||||||
{translate('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> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Scroller>
|
|
||||||
|
|
||||||
<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;
|
|
186
frontend/src/Settings/Profiles/Delay/DelayProfiles.tsx
Normal file
186
frontend/src/Settings/Profiles/Delay/DelayProfiles.tsx
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import { icons, scrollDirections } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
fetchDelayProfiles,
|
||||||
|
reorderDelayProfile,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
|
import DelayProfileModel from 'typings/DelayProfile';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import DelayProfile from './DelayProfile';
|
||||||
|
import EditDelayProfileModal from './EditDelayProfileModal';
|
||||||
|
import styles from './DelayProfiles.css';
|
||||||
|
|
||||||
|
function createDisplayProfilesSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.delayProfiles,
|
||||||
|
(delayProfiles) => {
|
||||||
|
const { defaultProfile, items } = delayProfiles.items.reduce<{
|
||||||
|
defaultProfile: null | DelayProfileModel;
|
||||||
|
items: DelayProfileModel[];
|
||||||
|
}>(
|
||||||
|
(acc, item) => {
|
||||||
|
if (item.id === 1) {
|
||||||
|
acc.defaultProfile = item;
|
||||||
|
} else {
|
||||||
|
acc.items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultProfile: null,
|
||||||
|
items: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
items.sort((a, b) => a.order - b.order);
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultProfile,
|
||||||
|
...delayProfiles,
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DelayProfiles() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { error, isFetching, isPopulated, items, defaultProfile } = useSelector(
|
||||||
|
createDisplayProfilesSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const tagList = useSelector(createTagsSelector());
|
||||||
|
|
||||||
|
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
||||||
|
const [dropIndex, setDropIndex] = useState<number | null>(null);
|
||||||
|
const [isAddDelayProfileModalOpen, setIsAddDelayProfileModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const isDragging = dropIndex !== null;
|
||||||
|
const isDraggingUp =
|
||||||
|
isDragging &&
|
||||||
|
dropIndex != null &&
|
||||||
|
dragIndex != null &&
|
||||||
|
dropIndex < dragIndex;
|
||||||
|
const isDraggingDown =
|
||||||
|
isDragging &&
|
||||||
|
dropIndex != null &&
|
||||||
|
dragIndex != null &&
|
||||||
|
dropIndex > dragIndex;
|
||||||
|
|
||||||
|
const handleAddDelayProfilePress = useCallback(() => {
|
||||||
|
setIsAddDelayProfileModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddDelayProfileModalClose = useCallback(() => {
|
||||||
|
setIsAddDelayProfileModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDelayProfileDragMove = useCallback(
|
||||||
|
(newDragIndex: number, newDropIndex: number) => {
|
||||||
|
setDragIndex(newDragIndex);
|
||||||
|
setDropIndex(newDropIndex);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelayProfileDragEnd = useCallback(
|
||||||
|
(id: number, didDrop: boolean) => {
|
||||||
|
if (didDrop && dropIndex !== null) {
|
||||||
|
dispatch(reorderDelayProfile({ id, moveIndex: dropIndex - 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragIndex(null);
|
||||||
|
setDropIndex(null);
|
||||||
|
},
|
||||||
|
[dropIndex, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchDelayProfiles());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('DelayProfiles')}>
|
||||||
|
<PageSectionContent
|
||||||
|
errorMessage={translate('DelayProfilesLoadError')}
|
||||||
|
error={error}
|
||||||
|
isFetching={isFetching}
|
||||||
|
isPopulated={isPopulated}
|
||||||
|
>
|
||||||
|
<Scroller
|
||||||
|
className={styles.horizontalScroll}
|
||||||
|
scrollDirection={scrollDirections.HORIZONTAL}
|
||||||
|
autoFocus={false}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className={styles.delayProfilesHeader}>
|
||||||
|
<div className={styles.column}>
|
||||||
|
{translate('PreferredProtocol')}
|
||||||
|
</div>
|
||||||
|
<div className={styles.column}>{translate('UsenetDelay')}</div>
|
||||||
|
<div className={styles.column}>{translate('TorrentDelay')}</div>
|
||||||
|
<div className={styles.tags}>{translate('Tags')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.delayProfiles}>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<DelayProfile
|
||||||
|
key={item.id}
|
||||||
|
{...item}
|
||||||
|
tagList={tagList}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
onDelayProfileDragEnd={handleDelayProfileDragEnd}
|
||||||
|
onDelayProfileDragMove={handleDelayProfileDragMove}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{defaultProfile ? (
|
||||||
|
<div>
|
||||||
|
<DelayProfile
|
||||||
|
{...defaultProfile}
|
||||||
|
tagList={tagList}
|
||||||
|
isDraggingDown={false}
|
||||||
|
isDraggingUp={false}
|
||||||
|
onDelayProfileDragEnd={handleDelayProfileDragEnd}
|
||||||
|
onDelayProfileDragMove={handleDelayProfileDragMove}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Scroller>
|
||||||
|
|
||||||
|
<div className={styles.addDelayProfile}>
|
||||||
|
<Link
|
||||||
|
className={styles.addButton}
|
||||||
|
onPress={handleAddDelayProfilePress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.ADD} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditDelayProfileModal
|
||||||
|
isOpen={isAddDelayProfileModalOpen}
|
||||||
|
onModalClose={handleAddDelayProfileModalClose}
|
||||||
|
/>
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DelayProfiles;
|
|
@ -1,105 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { deleteDelayProfile, fetchDelayProfiles, 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);
|
|
|
@ -1,27 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
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,37 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import EditDelayProfileModalContent, {
|
||||||
|
EditDelayProfileModalContentProps,
|
||||||
|
} from './EditDelayProfileModalContent';
|
||||||
|
|
||||||
|
interface EditDelayProfileModalProps extends EditDelayProfileModalContentProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditDelayProfileModal({
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: EditDelayProfileModalProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
dispatch(clearPendingChanges({ section: 'settings.delayProfiles' }));
|
||||||
|
onModalClose();
|
||||||
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
|
<EditDelayProfileModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditDelayProfileModal;
|
|
@ -1,43 +0,0 @@
|
||||||
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);
|
|
|
@ -1,266 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
|
||||||
import { boolSettingShape, numberSettingShape, tagSettingShape } from 'Helpers/Props/Shapes/settingShape';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './EditDelayProfileModalContent.css';
|
|
||||||
|
|
||||||
const protocolOptions = [
|
|
||||||
{
|
|
||||||
key: 'preferUsenet',
|
|
||||||
get value() {
|
|
||||||
return translate('PreferUsenet');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'preferTorrent',
|
|
||||||
get value() {
|
|
||||||
return translate('PreferTorrent');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'onlyUsenet',
|
|
||||||
get value() {
|
|
||||||
return translate('OnlyUsenet');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'onlyTorrent',
|
|
||||||
get value() {
|
|
||||||
return translate('OnlyTorrent');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function EditDelayProfileModalContent(props) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
isFetching,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
item,
|
|
||||||
protocol,
|
|
||||||
onInputChange,
|
|
||||||
onProtocolChange,
|
|
||||||
onSavePress,
|
|
||||||
onModalClose,
|
|
||||||
onDeleteDelayProfilePress,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
enableUsenet,
|
|
||||||
enableTorrent,
|
|
||||||
usenetDelay,
|
|
||||||
torrentDelay,
|
|
||||||
bypassIfHighestQuality,
|
|
||||||
bypassIfAboveCustomFormatScore,
|
|
||||||
minimumCustomFormatScore,
|
|
||||||
tags
|
|
||||||
} = item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{id ? translate('EditDelayProfile') : translate('AddDelayProfile')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
{
|
|
||||||
isFetching ?
|
|
||||||
<LoadingIndicator /> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error ?
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('AddDelayProfileError')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !error ?
|
|
||||||
<Form {...otherProps}>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('PreferredProtocol')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="protocol"
|
|
||||||
value={protocol}
|
|
||||||
values={protocolOptions}
|
|
||||||
helpText={translate('ProtocolHelpText')}
|
|
||||||
onChange={onProtocolChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
enableUsenet.value ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('UsenetDelay')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.NUMBER}
|
|
||||||
name="usenetDelay"
|
|
||||||
unit="minutes"
|
|
||||||
{...usenetDelay}
|
|
||||||
helpText={translate('UsenetDelayHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
enableTorrent.value ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('TorrentDelay')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.NUMBER}
|
|
||||||
name="torrentDelay"
|
|
||||||
unit="minutes"
|
|
||||||
{...torrentDelay}
|
|
||||||
helpText={translate('TorrentDelayHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('BypassDelayIfHighestQuality')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="bypassIfHighestQuality"
|
|
||||||
{...bypassIfHighestQuality}
|
|
||||||
helpText={translate('BypassDelayIfHighestQualityHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('BypassDelayIfAboveCustomFormatScore')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="bypassIfAboveCustomFormatScore"
|
|
||||||
{...bypassIfAboveCustomFormatScore}
|
|
||||||
helpText={translate('BypassDelayIfAboveCustomFormatScoreHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
bypassIfAboveCustomFormatScore.value ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('BypassDelayIfAboveCustomFormatScoreMinimumScore')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.NUMBER}
|
|
||||||
name="minimumCustomFormatScore"
|
|
||||||
{...minimumCustomFormatScore}
|
|
||||||
helpText={translate('BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
id === 1 ?
|
|
||||||
<Alert>
|
|
||||||
{translate('DefaultDelayProfileSeries')}
|
|
||||||
</Alert> :
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Tags')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TAG}
|
|
||||||
name="tags"
|
|
||||||
{...tags}
|
|
||||||
helpText={translate('DelayProfileSeriesTagsHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
</Form> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
{
|
|
||||||
id && id > 1 ?
|
|
||||||
<Button
|
|
||||||
className={styles.deleteButton}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={onDeleteDelayProfilePress}
|
|
||||||
>
|
|
||||||
{translate('Delete')}
|
|
||||||
</Button> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerErrorButton
|
|
||||||
isSpinning={isSaving}
|
|
||||||
error={saveError}
|
|
||||||
onPress={onSavePress}
|
|
||||||
>
|
|
||||||
{translate('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,
|
|
||||||
bypassIfHighestQuality: PropTypes.shape(boolSettingShape).isRequired,
|
|
||||||
bypassIfAboveCustomFormatScore: PropTypes.shape(boolSettingShape).isRequired,
|
|
||||||
minimumCustomFormatScore: 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,
|
|
||||||
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,370 @@
|
||||||
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
saveDelayProfile,
|
||||||
|
setDelayProfileValue,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './EditDelayProfileModalContent.css';
|
||||||
|
|
||||||
|
const newDelayProfile: Record<string, boolean | number | number[] | string> = {
|
||||||
|
enableUsenet: true,
|
||||||
|
enableTorrent: true,
|
||||||
|
preferredProtocol: 'usenet',
|
||||||
|
usenetDelay: 0,
|
||||||
|
torrentDelay: 0,
|
||||||
|
bypassIfHighestQuality: false,
|
||||||
|
bypassIfAboveCustomFormatScore: false,
|
||||||
|
minimumCustomFormatScore: 0,
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const protocolOptions = [
|
||||||
|
{
|
||||||
|
key: 'preferUsenet',
|
||||||
|
get value() {
|
||||||
|
return translate('PreferUsenet');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'preferTorrent',
|
||||||
|
get value() {
|
||||||
|
return translate('PreferTorrent');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'onlyUsenet',
|
||||||
|
get value() {
|
||||||
|
return translate('OnlyUsenet');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'onlyTorrent',
|
||||||
|
get value() {
|
||||||
|
return translate('OnlyTorrent');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function createDelayProfileSelector(id: number | undefined) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.delayProfiles,
|
||||||
|
(delayProfiles) => {
|
||||||
|
const { isFetching, error, isSaving, saveError, pendingChanges, items } =
|
||||||
|
delayProfiles;
|
||||||
|
|
||||||
|
const profile = id ? items.find((i) => i.id === id) : newDelayProfile;
|
||||||
|
const settings = selectSettings(profile!, pendingChanges, saveError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
item: settings.settings,
|
||||||
|
...settings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditDelayProfileModalContentProps {
|
||||||
|
id?: number;
|
||||||
|
onDeleteDelayProfilePress?: () => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditDelayProfileModalContent({
|
||||||
|
id,
|
||||||
|
onModalClose,
|
||||||
|
onDeleteDelayProfilePress,
|
||||||
|
...otherProps
|
||||||
|
}: EditDelayProfileModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { item, isFetching, error, isSaving, saveError } = useSelector(
|
||||||
|
createDelayProfileSelector(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
enableUsenet,
|
||||||
|
enableTorrent,
|
||||||
|
preferredProtocol,
|
||||||
|
usenetDelay,
|
||||||
|
torrentDelay,
|
||||||
|
bypassIfHighestQuality,
|
||||||
|
bypassIfAboveCustomFormatScore,
|
||||||
|
minimumCustomFormatScore,
|
||||||
|
tags,
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
const protocol = useMemo(() => {
|
||||||
|
if (!enableUsenet.value) {
|
||||||
|
return 'onlyTorrent';
|
||||||
|
} else if (!enableTorrent.value) {
|
||||||
|
return 'onlyUsenet';
|
||||||
|
}
|
||||||
|
|
||||||
|
return preferredProtocol.value === 'usenet'
|
||||||
|
? 'preferUsenet'
|
||||||
|
: 'preferTorrent';
|
||||||
|
}, [enableUsenet, enableTorrent, preferredProtocol]);
|
||||||
|
|
||||||
|
const wasSaving = usePrevious(isSaving);
|
||||||
|
|
||||||
|
const onInputChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
dispatch(setDelayProfileValue({ name, value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onProtocolChange = useCallback(
|
||||||
|
({ value }: InputChanged) => {
|
||||||
|
let enableUsenet = false;
|
||||||
|
let enableTorrent = false;
|
||||||
|
let preferredProtocol: 'usenet' | 'torrent' = 'usenet';
|
||||||
|
|
||||||
|
switch (value) {
|
||||||
|
case 'preferUsenet':
|
||||||
|
enableUsenet = true;
|
||||||
|
enableTorrent = true;
|
||||||
|
preferredProtocol = 'usenet';
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'preferTorrent':
|
||||||
|
enableUsenet = true;
|
||||||
|
enableTorrent = true;
|
||||||
|
preferredProtocol = 'torrent';
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'onlyUsenet':
|
||||||
|
enableUsenet = true;
|
||||||
|
enableTorrent = false;
|
||||||
|
preferredProtocol = 'usenet';
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'onlyTorrent':
|
||||||
|
enableUsenet = false;
|
||||||
|
enableTorrent = true;
|
||||||
|
preferredProtocol = 'torrent';
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Error(`Unknown protocol option: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
setDelayProfileValue({ name: 'enableUsenet', value: enableUsenet })
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
setDelayProfileValue({
|
||||||
|
name: 'enableTorrent',
|
||||||
|
value: enableTorrent,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
setDelayProfileValue({
|
||||||
|
name: 'preferredProtocol',
|
||||||
|
value: preferredProtocol,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSavePress = useCallback(() => {
|
||||||
|
dispatch(saveDelayProfile({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) {
|
||||||
|
Object.keys(newDelayProfile).forEach((name) => {
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
setDelayProfileValue({
|
||||||
|
name,
|
||||||
|
value: newDelayProfile[name],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasSaving && !isSaving && !saveError) {
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{id ? translate('EditDelayProfile') : translate('AddDelayProfile')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && !!error ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('AddDelayProfileError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isFetching && !error ? (
|
||||||
|
<Form {...otherProps}>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('PreferredProtocol')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="protocol"
|
||||||
|
value={protocol}
|
||||||
|
values={protocolOptions}
|
||||||
|
helpText={translate('ProtocolHelpText')}
|
||||||
|
onChange={onProtocolChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{enableUsenet.value ? (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('UsenetDelay')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="usenetDelay"
|
||||||
|
unit="minutes"
|
||||||
|
{...usenetDelay}
|
||||||
|
helpText={translate('UsenetDelayHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{enableTorrent.value ? (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('TorrentDelay')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="torrentDelay"
|
||||||
|
unit="minutes"
|
||||||
|
{...torrentDelay}
|
||||||
|
helpText={translate('TorrentDelayHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('BypassDelayIfHighestQuality')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="bypassIfHighestQuality"
|
||||||
|
{...bypassIfHighestQuality}
|
||||||
|
helpText={translate('BypassDelayIfHighestQualityHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('BypassDelayIfAboveCustomFormatScore')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="bypassIfAboveCustomFormatScore"
|
||||||
|
{...bypassIfAboveCustomFormatScore}
|
||||||
|
helpText={translate(
|
||||||
|
'BypassDelayIfAboveCustomFormatScoreHelpText'
|
||||||
|
)}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{bypassIfAboveCustomFormatScore.value ? (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('BypassDelayIfAboveCustomFormatScoreMinimumScore')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="minimumCustomFormatScore"
|
||||||
|
{...minimumCustomFormatScore}
|
||||||
|
helpText={translate(
|
||||||
|
'BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText'
|
||||||
|
)}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{id === 1 ? (
|
||||||
|
<Alert>{translate('DefaultDelayProfileSeries')}</Alert>
|
||||||
|
) : (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
{...tags}
|
||||||
|
helpText={translate('DelayProfileSeriesTagsHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
{id && id > 1 ? (
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onDeleteDelayProfilePress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={handleSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditDelayProfileModalContent;
|
|
@ -1,172 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { saveDelayProfile, setDelayProfileValue } from 'Store/Actions/settingsActions';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
import EditDelayProfileModalContent from './EditDelayProfileModalContent';
|
|
||||||
|
|
||||||
const newDelayProfile = {
|
|
||||||
enableUsenet: true,
|
|
||||||
enableTorrent: true,
|
|
||||||
preferredProtocol: 'usenet',
|
|
||||||
usenetDelay: 0,
|
|
||||||
torrentDelay: 0,
|
|
||||||
bypassIfHighestQuality: false,
|
|
||||||
bypassIfAboveCustomFormatScore: false,
|
|
||||||
minimumCustomFormatScore: 0,
|
|
||||||
tags: []
|
|
||||||
};
|
|
||||||
|
|
||||||
function createDelayProfileSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { id }) => id,
|
|
||||||
(state) => state.settings.delayProfiles,
|
|
||||||
(id, delayProfiles) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
pendingChanges,
|
|
||||||
items
|
|
||||||
} = delayProfiles;
|
|
||||||
|
|
||||||
const profile = id ? items.find((i) => i.id === 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,
|
|
||||||
...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);
|
|
|
@ -1,37 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { DndProvider } from 'react-dnd-multi-backend';
|
|
||||||
import HTML5toTouch from 'react-dnd-multi-backend/dist/esm/HTML5toTouch';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
|
|
||||||
import QualityProfilesConnector from './Quality/QualityProfilesConnector';
|
|
||||||
import ReleaseProfiles from './Release/ReleaseProfiles';
|
|
||||||
|
|
||||||
// Only a single DragDrop Context can exist so it's done here to allow editing
|
|
||||||
// quality profiles and reordering delay profiles to work.
|
|
||||||
|
|
||||||
class Profiles extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('Profiles')}>
|
|
||||||
<SettingsToolbar showSave={false} />
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
<DndProvider options={HTML5toTouch}>
|
|
||||||
<QualityProfilesConnector />
|
|
||||||
<DelayProfilesConnector />
|
|
||||||
<ReleaseProfiles />
|
|
||||||
</DndProvider>
|
|
||||||
</PageContentBody>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Profiles;
|
|
31
frontend/src/Settings/Profiles/Profiles.tsx
Normal file
31
frontend/src/Settings/Profiles/Profiles.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { HTML5toTouch } from 'rdndmb-html5-to-touch';
|
||||||
|
import React from 'react';
|
||||||
|
import { DndProvider } from 'react-dnd-multi-backend';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import DelayProfiles from './Delay/DelayProfiles';
|
||||||
|
import QualityProfiles from './Quality/QualityProfiles';
|
||||||
|
import ReleaseProfiles from './Release/ReleaseProfiles';
|
||||||
|
|
||||||
|
// Only a single DragDrop Context can exist so it's done here to allow editing
|
||||||
|
// quality profiles and reordering delay profiles to work.
|
||||||
|
|
||||||
|
function Profiles() {
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Profiles')}>
|
||||||
|
<SettingsToolbar showSave={false} />
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
<DndProvider options={HTML5toTouch}>
|
||||||
|
<QualityProfiles />
|
||||||
|
<DelayProfiles />
|
||||||
|
<ReleaseProfiles />
|
||||||
|
</DndProvider>
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Profiles;
|
|
@ -1,61 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
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 !== 0) {
|
|
||||||
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,55 @@
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import EditQualityProfileModalContent from './EditQualityProfileModalContent';
|
||||||
|
|
||||||
|
interface EditQualityProfileModalProps {
|
||||||
|
id?: number;
|
||||||
|
isOpen: boolean;
|
||||||
|
onDeleteQualityProfilePress?: () => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditQualityProfileModal({
|
||||||
|
id,
|
||||||
|
isOpen,
|
||||||
|
onDeleteQualityProfilePress,
|
||||||
|
onModalClose,
|
||||||
|
}: EditQualityProfileModalProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [height, setHeight] = useState<'auto' | number>('auto');
|
||||||
|
|
||||||
|
const handleOnModalClose = useCallback(() => {
|
||||||
|
dispatch(clearPendingChanges({ section: 'settings.qualityProfiles' }));
|
||||||
|
onModalClose();
|
||||||
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
|
const handleContentHeightChange = useCallback(
|
||||||
|
(newHeight: number) => {
|
||||||
|
if (height === 'auto' || newHeight !== 0) {
|
||||||
|
setHeight(newHeight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[height]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
style={{ height: height === 'auto' ? 'auto' : `${height}px` }}
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
|
onModalClose={handleOnModalClose}
|
||||||
|
>
|
||||||
|
<EditQualityProfileModalContent
|
||||||
|
id={id}
|
||||||
|
onContentHeightChange={handleContentHeightChange}
|
||||||
|
onDeleteQualityProfilePress={onDeleteQualityProfilePress}
|
||||||
|
onModalClose={handleOnModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditQualityProfileModal;
|
|
@ -1,43 +0,0 @@
|
||||||
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);
|
|
|
@ -1,363 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QualityProfileFormatItems from './QualityProfileFormatItems';
|
|
||||||
import QualityProfileItems from './QualityProfileItems';
|
|
||||||
import styles from './EditQualityProfileModalContent.css';
|
|
||||||
|
|
||||||
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
|
|
||||||
|
|
||||||
function getCustomFormatRender(formatItems, otherProps) {
|
|
||||||
return (
|
|
||||||
<QualityProfileFormatItems
|
|
||||||
profileFormatItems={formatItems.value}
|
|
||||||
errors={formatItems.errors}
|
|
||||||
warnings={formatItems.warnings}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditQualityProfileModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
headerHeight: 0,
|
|
||||||
bodyHeight: 0,
|
|
||||||
defaultBodyHeight: 0,
|
|
||||||
editGroupsBodyHeight: 0,
|
|
||||||
editSizesBodyHeight: 0,
|
|
||||||
footerHeight: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const {
|
|
||||||
headerHeight,
|
|
||||||
footerHeight
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const bodyHeight = this.state[`${this.props.mode}BodyHeight`];
|
|
||||||
|
|
||||||
if (
|
|
||||||
headerHeight > 0 &&
|
|
||||||
bodyHeight > 0 &&
|
|
||||||
footerHeight > 0 &&
|
|
||||||
(
|
|
||||||
headerHeight !== prevState.headerHeight ||
|
|
||||||
bodyHeight !== prevState[`${prevProps.mode}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 }) => {
|
|
||||||
const heightKey = `${this.props.mode}BodyHeight`;
|
|
||||||
|
|
||||||
if (height !== this.state[heightKey]) {
|
|
||||||
this.setState({ [heightKey]: height });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onFooterMeasure = ({ height }) => {
|
|
||||||
if (height > this.state.footerHeight) {
|
|
||||||
this.setState({ footerHeight: height });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
mode,
|
|
||||||
isFetching,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
qualities,
|
|
||||||
customFormats,
|
|
||||||
item,
|
|
||||||
isInUse,
|
|
||||||
onInputChange,
|
|
||||||
onCutoffChange,
|
|
||||||
onSavePress,
|
|
||||||
onModalClose,
|
|
||||||
onDeleteQualityProfilePress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
upgradeAllowed,
|
|
||||||
cutoff,
|
|
||||||
minFormatScore,
|
|
||||||
minUpgradeFormatScore,
|
|
||||||
cutoffFormatScore,
|
|
||||||
items,
|
|
||||||
formatItems
|
|
||||||
} = item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<Measure
|
|
||||||
whitelist={['height']}
|
|
||||||
includeMargin={false}
|
|
||||||
onMeasure={this.onHeaderMeasure}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
{id ? translate('EditQualityProfile') : translate('AddQualityProfile')}
|
|
||||||
</ModalHeader>
|
|
||||||
</Measure>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Measure
|
|
||||||
whitelist={['height']}
|
|
||||||
onMeasure={this.onBodyMeasure}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('AddQualityProfileError')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !error &&
|
|
||||||
<Form
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.formGroupsContainer}>
|
|
||||||
<div className={styles.formGroupWrapper}>
|
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
|
||||||
<FormLabel size={sizes.SMALL}>
|
|
||||||
{translate('Name')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="name"
|
|
||||||
{...name}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
|
||||||
<FormLabel size={sizes.SMALL}>
|
|
||||||
{translate('UpgradesAllowed')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="upgradeAllowed"
|
|
||||||
{...upgradeAllowed}
|
|
||||||
helpText={translate('UpgradesAllowedHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
upgradeAllowed.value &&
|
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
|
||||||
<FormLabel size={sizes.SMALL}>
|
|
||||||
{translate('UpgradeUntil')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="cutoff"
|
|
||||||
{...cutoff}
|
|
||||||
values={qualities}
|
|
||||||
helpText={translate('UpgradeUntilEpisodeHelpText')}
|
|
||||||
onChange={onCutoffChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
formatItems.value.length > 0 &&
|
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
|
||||||
<FormLabel size={sizes.SMALL}>
|
|
||||||
{translate('MinimumCustomFormatScore')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.NUMBER}
|
|
||||||
name="minFormatScore"
|
|
||||||
{...minFormatScore}
|
|
||||||
helpText={translate('MinimumCustomFormatScoreHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
upgradeAllowed.value && formatItems.value.length > 0 &&
|
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
|
||||||
<FormLabel size={sizes.SMALL}>
|
|
||||||
{translate('UpgradeUntilCustomFormatScore')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.NUMBER}
|
|
||||||
name="cutoffFormatScore"
|
|
||||||
{...cutoffFormatScore}
|
|
||||||
helpText={translate('UpgradeUntilCustomFormatScoreEpisodeHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
upgradeAllowed.value && formatItems.value.length > 0 ?
|
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
|
||||||
<FormLabel size={sizes.SMALL}>
|
|
||||||
{translate('MinimumCustomFormatScoreIncrement')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.NUMBER}
|
|
||||||
name="minUpgradeFormatScore"
|
|
||||||
min={1}
|
|
||||||
{...minUpgradeFormatScore}
|
|
||||||
helpText={translate('MinimumCustomFormatScoreIncrementHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.formatItemLarge}>
|
|
||||||
{getCustomFormatRender(formatItems, otherProps)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.formGroupWrapper}>
|
|
||||||
<QualityProfileItems
|
|
||||||
mode={mode}
|
|
||||||
qualityProfileItems={items.value}
|
|
||||||
errors={items.errors}
|
|
||||||
warnings={items.warnings}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.formatItemSmall}>
|
|
||||||
{getCustomFormatRender(formatItems, otherProps)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Measure>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<Measure
|
|
||||||
whitelist={['height']}
|
|
||||||
includeMargin={false}
|
|
||||||
onMeasure={this.onFooterMeasure}
|
|
||||||
>
|
|
||||||
<ModalFooter>
|
|
||||||
{
|
|
||||||
id ?
|
|
||||||
<div
|
|
||||||
className={styles.deleteButtonContainer}
|
|
||||||
title={
|
|
||||||
isInUse ?
|
|
||||||
translate('QualityProfileInUseSeriesListCollection') :
|
|
||||||
undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
isDisabled={isInUse}
|
|
||||||
onPress={onDeleteQualityProfilePress}
|
|
||||||
>
|
|
||||||
{translate('Delete')}
|
|
||||||
</Button>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerErrorButton
|
|
||||||
isSpinning={isSaving}
|
|
||||||
error={saveError}
|
|
||||||
onPress={onSavePress}
|
|
||||||
>
|
|
||||||
{translate('Save')}
|
|
||||||
</SpinnerErrorButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</Measure>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditQualityProfileModalContent.propTypes = {
|
|
||||||
mode: PropTypes.string.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFormats: 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,763 @@
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { QualityProfilesAppState } from 'App/State/SettingsAppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
fetchQualityProfileSchema,
|
||||||
|
saveQualityProfile,
|
||||||
|
setQualityProfileValue,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
|
import createQualityProfileInUseSelector from 'Store/Selectors/createQualityProfileInUseSelector';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import QualityProfile, {
|
||||||
|
QualityProfileGroup,
|
||||||
|
QualityProfileQualityItem,
|
||||||
|
} from 'typings/QualityProfile';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QualityProfileFormatItems from './QualityProfileFormatItems';
|
||||||
|
import { DragMoveState } from './QualityProfileItemDragSource';
|
||||||
|
import QualityProfileItems, {
|
||||||
|
EditQualityProfileMode,
|
||||||
|
} from './QualityProfileItems';
|
||||||
|
import { SizeChanged } from './QualityProfileItemSize';
|
||||||
|
import styles from './EditQualityProfileModalContent.css';
|
||||||
|
|
||||||
|
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
|
||||||
|
|
||||||
|
function parseIndex(index: string): [number | null, number] {
|
||||||
|
const split = index.split('.');
|
||||||
|
|
||||||
|
if (split.length === 1) {
|
||||||
|
return [null, parseInt(split[0]) - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [parseInt(split[0]) - 1, parseInt(split[1]) - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditQualityProfileModalContentProps {
|
||||||
|
id?: number;
|
||||||
|
onContentHeightChange: (height: number) => void;
|
||||||
|
onDeleteQualityProfilePress?: () => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditQualityProfileModalContent({
|
||||||
|
id,
|
||||||
|
onContentHeightChange,
|
||||||
|
onDeleteQualityProfilePress,
|
||||||
|
onModalClose,
|
||||||
|
}: EditQualityProfileModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { error, isFetching, isPopulated, isSaving, saveError, item } =
|
||||||
|
useSelector(
|
||||||
|
createProviderSettingsSelectorHook<
|
||||||
|
QualityProfile,
|
||||||
|
QualityProfilesAppState
|
||||||
|
>('qualityProfiles', id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const isInUse = useSelector(createQualityProfileInUseSelector(id));
|
||||||
|
|
||||||
|
const [measureHeaderRef, { height: headerHeight }] = useMeasure();
|
||||||
|
const [measureBodyRef, { height: bodyHeight }] = useMeasure();
|
||||||
|
const [measureFooterRef, { height: footerHeight }] = useMeasure();
|
||||||
|
|
||||||
|
const [mode, setMode] = useState<EditQualityProfileMode>('default');
|
||||||
|
const [defaultBodyHeight, setDefaultBodyHeight] = useState(0);
|
||||||
|
const [editGroupsBodyHeight, setEditGroupsBodyHeight] = useState(0);
|
||||||
|
const [editSizesBodyHeight, setEditSizesBodyHeight] = useState(0);
|
||||||
|
const [dndState, setDndState] = useState<DragMoveState>({
|
||||||
|
dragQualityIndex: null,
|
||||||
|
dropQualityIndex: null,
|
||||||
|
dropPosition: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wasSaving = usePrevious(isSaving);
|
||||||
|
const { dragQualityIndex, dropQualityIndex, dropPosition } = dndState;
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
upgradeAllowed,
|
||||||
|
cutoff,
|
||||||
|
minFormatScore,
|
||||||
|
minUpgradeFormatScore,
|
||||||
|
cutoffFormatScore,
|
||||||
|
items,
|
||||||
|
formatItems,
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
const qualities = useMemo(() => {
|
||||||
|
if (!items?.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.value.reduceRight<{ key: number; value: string }[]>(
|
||||||
|
(acc, item) => {
|
||||||
|
if (item.allowed) {
|
||||||
|
if ('id' in item) {
|
||||||
|
acc.push({
|
||||||
|
key: item.id,
|
||||||
|
value: item.name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
acc.push({
|
||||||
|
key: item.quality.id,
|
||||||
|
value: item.quality.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
dispatch(setQualityProfileValue({ name, value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSavePress = useCallback(() => {
|
||||||
|
dispatch(saveQualityProfile({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
const handleCutoffChange = useCallback(
|
||||||
|
({ name, value }: InputChanged<number>) => {
|
||||||
|
const cutoffItem = items.value.find((item) => {
|
||||||
|
return 'id' in item ? item.id === value : item.quality.id === value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cutoffItem) {
|
||||||
|
const cutoffId =
|
||||||
|
'id' in cutoffItem ? cutoffItem.id : cutoffItem.quality.id;
|
||||||
|
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
dispatch(setQualityProfileValue({ name, value: cutoffId }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[items, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleItemAllowedChange = useCallback(
|
||||||
|
(qualityId: number, allowed: boolean) => {
|
||||||
|
const newItems = items.value.map((item) => {
|
||||||
|
if ('quality' in item && item.quality.id === qualityId) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
allowed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
setQualityProfileValue({
|
||||||
|
name: 'items',
|
||||||
|
value: newItems,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[items, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGroupAllowedChange = useCallback(
|
||||||
|
(groupId: number, allowed: boolean) => {
|
||||||
|
const newItems = items.value.map((item) => {
|
||||||
|
if ('id' in item && item.id === groupId) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
allowed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
setQualityProfileValue({
|
||||||
|
name: 'items',
|
||||||
|
value: newItems,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[items, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGroupNameChange = useCallback(
|
||||||
|
(groupId: number, name: string) => {
|
||||||
|
const newItems = items.value.map((item) => {
|
||||||
|
if ('id' in item && item.id === groupId) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
dispatch(setQualityProfileValue({ name: 'items', value: newItems }));
|
||||||
|
},
|
||||||
|
[items, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSizeChange = useCallback(
|
||||||
|
(sizeChange: SizeChanged) => {
|
||||||
|
const { qualityId, ...sizes } = sizeChange;
|
||||||
|
|
||||||
|
const newItems = items.value.map((item) => {
|
||||||
|
if ('quality' in item && item.quality.id === qualityId) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...sizes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
items: (item as QualityProfileGroup).items.map((subItem) => {
|
||||||
|
if (subItem.quality.id === qualityId) {
|
||||||
|
return {
|
||||||
|
...subItem,
|
||||||
|
...sizes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return subItem;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
setQualityProfileValue({
|
||||||
|
name: 'items',
|
||||||
|
value: newItems,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[items, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateGroupPress = useCallback(
|
||||||
|
(qualityId: number) => {
|
||||||
|
const groupId =
|
||||||
|
items.value.reduce((acc, item) => {
|
||||||
|
if ('id' in item && item.id > acc) {
|
||||||
|
acc = item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, 1000) + 1;
|
||||||
|
|
||||||
|
const newItems = items.value.map((item) => {
|
||||||
|
if ('quality' in item && item.quality.id === qualityId) {
|
||||||
|
return {
|
||||||
|
id: groupId,
|
||||||
|
name: item.quality.name,
|
||||||
|
allowed: item.allowed,
|
||||||
|
items: [item],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
dispatch(setQualityProfileValue({ name: 'items', newItems }));
|
||||||
|
},
|
||||||
|
[items, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteGroupPress = useCallback(
|
||||||
|
(groupId: number) => {
|
||||||
|
const newItems = items.value.reduce<QualityProfileQualityItem[]>(
|
||||||
|
(acc, item) => {
|
||||||
|
if ('id' in item && item.id === groupId) {
|
||||||
|
acc.push(...item.items);
|
||||||
|
} else {
|
||||||
|
acc.push(item as QualityProfileQualityItem);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
dispatch(setQualityProfileValue({ name: 'items', value: newItems }));
|
||||||
|
},
|
||||||
|
[items, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragMove = useCallback((options: DragMoveState) => {
|
||||||
|
const { dragQualityIndex, dropQualityIndex, dropPosition } = options;
|
||||||
|
|
||||||
|
if (!dragQualityIndex || !dropQualityIndex || !dropPosition) {
|
||||||
|
setDndState({
|
||||||
|
dragQualityIndex: null,
|
||||||
|
dropQualityIndex: null,
|
||||||
|
dropPosition: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [dragGroupIndex, dragItemIndex] = parseIndex(dragQualityIndex);
|
||||||
|
const [dropGroupIndex, dropItemIndex] = parseIndex(dropQualityIndex);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(dropPosition === 'below' && dropItemIndex - 1 === dragItemIndex) ||
|
||||||
|
(dropPosition === 'above' && dropItemIndex + 1 === dragItemIndex)
|
||||||
|
) {
|
||||||
|
setDndState({
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDndState({
|
||||||
|
dragQualityIndex,
|
||||||
|
dropQualityIndex: adjustedDropQualityIndex,
|
||||||
|
dropPosition,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(
|
||||||
|
(didDrop: boolean) => {
|
||||||
|
if (didDrop && dragQualityIndex != null && dropQualityIndex != null) {
|
||||||
|
const newItems = items.value.map((i) => {
|
||||||
|
if ('id' in i) {
|
||||||
|
return {
|
||||||
|
...i,
|
||||||
|
items: [...i.items],
|
||||||
|
} as QualityProfileGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...i,
|
||||||
|
} as QualityProfileQualityItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [dragGroupIndex, dragItemIndex] = parseIndex(dragQualityIndex);
|
||||||
|
const [dropGroupIndex, dropItemIndex] = parseIndex(dropQualityIndex);
|
||||||
|
|
||||||
|
let item: QualityProfileQualityItem | null = null;
|
||||||
|
let dropGroup: QualityProfileGroup | null = null;
|
||||||
|
|
||||||
|
// Get the group before moving anything so we know the correct place to drop it.
|
||||||
|
if (dropGroupIndex != null) {
|
||||||
|
dropGroup = newItems[dropGroupIndex] as QualityProfileGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragGroupIndex == null) {
|
||||||
|
item = newItems.splice(
|
||||||
|
dragItemIndex,
|
||||||
|
1
|
||||||
|
)[0] as QualityProfileQualityItem;
|
||||||
|
} else {
|
||||||
|
const group = newItems[dragGroupIndex] as QualityProfileGroup;
|
||||||
|
|
||||||
|
item = group.items.splice(dragItemIndex, 1)[0];
|
||||||
|
|
||||||
|
// If the group is now empty, destroy it.
|
||||||
|
if (!group.items.length) {
|
||||||
|
newItems.splice(dragGroupIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropGroup == null) {
|
||||||
|
newItems.splice(dropItemIndex, 0, item);
|
||||||
|
} else {
|
||||||
|
dropGroup.items.splice(dropItemIndex, 0, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
setQualityProfileValue({
|
||||||
|
name: 'items',
|
||||||
|
value: newItems,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDndState({
|
||||||
|
dragQualityIndex: null,
|
||||||
|
dropQualityIndex: null,
|
||||||
|
dropPosition: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dragQualityIndex, dropQualityIndex, items, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeMode = useCallback((newMode: EditQualityProfileMode) => {
|
||||||
|
setMode(newMode);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFormatItemScoreChange = useCallback(
|
||||||
|
(formatId: number, score: number) => {
|
||||||
|
const newFormatItems = formatItems.value.map((formatItem) => {
|
||||||
|
if (formatItem.format === formatId) {
|
||||||
|
return {
|
||||||
|
...formatItem,
|
||||||
|
score,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
setQualityProfileValue({
|
||||||
|
name: 'formatItems',
|
||||||
|
value: newFormatItems,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[formatItems, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let bodyHeight = 0;
|
||||||
|
|
||||||
|
if (mode === 'default') {
|
||||||
|
bodyHeight = defaultBodyHeight;
|
||||||
|
} else if (mode === 'editGroups') {
|
||||||
|
bodyHeight = editGroupsBodyHeight;
|
||||||
|
} else if (mode === 'editSizes') {
|
||||||
|
bodyHeight = editSizesBodyHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const padding = MODAL_BODY_PADDING * 2;
|
||||||
|
|
||||||
|
onContentHeightChange(headerHeight + bodyHeight + footerHeight + padding);
|
||||||
|
}, [
|
||||||
|
headerHeight,
|
||||||
|
defaultBodyHeight,
|
||||||
|
editGroupsBodyHeight,
|
||||||
|
editSizesBodyHeight,
|
||||||
|
footerHeight,
|
||||||
|
mode,
|
||||||
|
onContentHeightChange,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === 'default') {
|
||||||
|
setDefaultBodyHeight(bodyHeight);
|
||||||
|
} else if (mode === 'editGroups') {
|
||||||
|
setEditGroupsBodyHeight(bodyHeight);
|
||||||
|
} else if (mode === 'editSizes') {
|
||||||
|
setEditSizesBodyHeight(bodyHeight);
|
||||||
|
}
|
||||||
|
}, [bodyHeight, mode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id && !isPopulated) {
|
||||||
|
dispatch(fetchQualityProfileSchema());
|
||||||
|
}
|
||||||
|
}, [id, isPopulated, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasSaving && !isSaving && !saveError) {
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!items?.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cutoffItem = items.value.find((item) =>
|
||||||
|
'id' in item ? item.id === cutoff.value : item.quality.id === cutoff.value
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the cutoff isn't allowed anymore or there isn't a cutoff set one
|
||||||
|
if (!cutoff || !cutoffItem || !cutoffItem.allowed) {
|
||||||
|
const firstAllowed = items.value.find((item) => item.allowed);
|
||||||
|
|
||||||
|
let cutoffId = null;
|
||||||
|
|
||||||
|
if (firstAllowed) {
|
||||||
|
cutoffId =
|
||||||
|
'id' in firstAllowed ? firstAllowed.id : firstAllowed.quality.id;
|
||||||
|
|
||||||
|
// @ts-expect-error - actions are not typed
|
||||||
|
dispatch(setQualityProfileValue({ name: 'cutoff', value: cutoffId }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [cutoff, items, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader ref={measureHeaderRef}>
|
||||||
|
{id ? translate('EditQualityProfile') : translate('AddQualityProfile')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div ref={measureBodyRef}>
|
||||||
|
{isPopulated ? null : <LoadingIndicator />}
|
||||||
|
|
||||||
|
{!isFetching && error ? (
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('AddQualityProfileError')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !error ? (
|
||||||
|
<Form>
|
||||||
|
<div className={styles.formGroupsContainer}>
|
||||||
|
<div className={styles.formGroupWrapper}>
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
{translate('Name')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="name"
|
||||||
|
{...name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
{translate('UpgradesAllowed')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="upgradeAllowed"
|
||||||
|
{...upgradeAllowed}
|
||||||
|
helpText={translate('UpgradesAllowedHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{upgradeAllowed.value ? (
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
{translate('UpgradeUntil')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="cutoff"
|
||||||
|
{...cutoff}
|
||||||
|
values={qualities}
|
||||||
|
helpText={translate('UpgradeUntilEpisodeHelpText')}
|
||||||
|
onChange={handleCutoffChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{formatItems.value.length > 0 ? (
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
{translate('MinimumCustomFormatScore')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="minFormatScore"
|
||||||
|
{...minFormatScore}
|
||||||
|
helpText={translate('MinimumCustomFormatScoreHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{upgradeAllowed.value && formatItems.value.length > 0 ? (
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
{translate('UpgradeUntilCustomFormatScore')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="cutoffFormatScore"
|
||||||
|
{...cutoffFormatScore}
|
||||||
|
helpText={translate(
|
||||||
|
'UpgradeUntilCustomFormatScoreEpisodeHelpText'
|
||||||
|
)}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{upgradeAllowed.value && formatItems.value.length > 0 ? (
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
{translate('MinimumCustomFormatScoreIncrement')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="minUpgradeFormatScore"
|
||||||
|
min={1}
|
||||||
|
{...minUpgradeFormatScore}
|
||||||
|
helpText={translate(
|
||||||
|
'MinimumCustomFormatScoreIncrementHelpText'
|
||||||
|
)}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className={styles.formatItemLarge}>
|
||||||
|
<QualityProfileFormatItems
|
||||||
|
profileFormatItems={formatItems.value}
|
||||||
|
errors={formatItems.errors}
|
||||||
|
warnings={formatItems.warnings}
|
||||||
|
onQualityProfileFormatItemScoreChange={
|
||||||
|
handleFormatItemScoreChange
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroupWrapper}>
|
||||||
|
<QualityProfileItems
|
||||||
|
mode={mode}
|
||||||
|
qualityProfileItems={items.value}
|
||||||
|
errors={items.errors}
|
||||||
|
warnings={items.warnings}
|
||||||
|
dragQualityIndex={dragQualityIndex}
|
||||||
|
dropQualityIndex={dropQualityIndex}
|
||||||
|
dropPosition={dropPosition}
|
||||||
|
onChangeMode={handleChangeMode}
|
||||||
|
onCreateGroupPress={handleCreateGroupPress}
|
||||||
|
onDeleteGroupPress={handleDeleteGroupPress}
|
||||||
|
onItemAllowedChange={handleItemAllowedChange}
|
||||||
|
onGroupAllowedChange={handleGroupAllowedChange}
|
||||||
|
onItemGroupNameChange={handleGroupNameChange}
|
||||||
|
onDragMove={handleDragMove}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onSizeChange={handleSizeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formatItemSmall}>
|
||||||
|
<QualityProfileFormatItems
|
||||||
|
profileFormatItems={formatItems.value}
|
||||||
|
errors={formatItems.errors}
|
||||||
|
warnings={formatItems.warnings}
|
||||||
|
onQualityProfileFormatItemScoreChange={
|
||||||
|
handleFormatItemScoreChange
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter ref={measureFooterRef}>
|
||||||
|
{id ? (
|
||||||
|
<div
|
||||||
|
className={styles.deleteButtonContainer}
|
||||||
|
title={
|
||||||
|
isInUse
|
||||||
|
? translate('QualityProfileInUseSeriesListCollection')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
isDisabled={isInUse}
|
||||||
|
onPress={onDeleteQualityProfilePress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={handleSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditQualityProfileModalContent;
|
|
@ -1,532 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchQualityProfileSchema, saveQualityProfile, setQualityProfileValue } from 'Store/Actions/settingsActions';
|
|
||||||
import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector';
|
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
|
||||||
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 createFormatsSelector() {
|
|
||||||
return createSelector(
|
|
||||||
createProviderSettingsSelector('qualityProfiles'),
|
|
||||||
(customFormat) => {
|
|
||||||
const items = customFormat.item.formatItems;
|
|
||||||
if (!items || !items.value) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.reduceRight(items.value, (result, { id, name, format, score }) => {
|
|
||||||
if (id) {
|
|
||||||
result.push({
|
|
||||||
key: id,
|
|
||||||
value: name,
|
|
||||||
score
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result.push({
|
|
||||||
key: format,
|
|
||||||
value: name,
|
|
||||||
score
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createProviderSettingsSelector('qualityProfiles'),
|
|
||||||
createQualitiesSelector(),
|
|
||||||
createFormatsSelector(),
|
|
||||||
createProfileInUseSelector('qualityProfileId'),
|
|
||||||
(qualityProfile, qualities, customFormats, isInUse) => {
|
|
||||||
return {
|
|
||||||
qualities,
|
|
||||||
customFormats,
|
|
||||||
...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,
|
|
||||||
mode: 'default' // default, editGroups, editSizes
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 || (i.quality && i.quality.id === cutoff);
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the cutoff isn't allowed anymore or there isn't a cutoff set one
|
|
||||||
if (!cutoff || !cutoffItem || !cutoffItem.allowed) {
|
|
||||||
const firstAllowed = _.find(qualityProfile.items.value, { allowed: true });
|
|
||||||
let cutoffId = null;
|
|
||||||
|
|
||||||
if (firstAllowed) {
|
|
||||||
cutoffId = firstAllowed.quality ? firstAllowed.quality.id : firstAllowed.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.setQualityProfileValue({ name: 'cutoff', value: cutoffId });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
onQualityProfileFormatItemScoreChange = (id, score) => {
|
|
||||||
const qualityProfile = _.cloneDeep(this.props.item);
|
|
||||||
const formatItems = qualityProfile.formatItems.value;
|
|
||||||
const item = _.find(qualityProfile.formatItems.value, (i) => i.format === id);
|
|
||||||
|
|
||||||
item.score = score;
|
|
||||||
|
|
||||||
this.props.setQualityProfileValue({
|
|
||||||
name: 'formatItems',
|
|
||||||
value: formatItems
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSizeChange = ({ id, minSize, maxSize, preferredSize }) => {
|
|
||||||
const qualityProfile = _.cloneDeep(this.props.item);
|
|
||||||
const items = qualityProfile.items.value;
|
|
||||||
let quality = null;
|
|
||||||
|
|
||||||
// eslint-disable-next-line guard-for-in
|
|
||||||
for (const index in items) {
|
|
||||||
const item = items[index];
|
|
||||||
|
|
||||||
if (item.quality?.id === id) {
|
|
||||||
quality = item;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line guard-for-in
|
|
||||||
for (const i in item.items) {
|
|
||||||
const nestedItem = items[i];
|
|
||||||
|
|
||||||
if (nestedItem.quality?.id === id) {
|
|
||||||
quality = nestedItem;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quality) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!quality) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
quality.minSize = minSize;
|
|
||||||
quality.maxSize = maxSize;
|
|
||||||
quality.preferredSize = preferredSize;
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onChangeMode = (mode) => {
|
|
||||||
this.setState({ mode });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// 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}
|
|
||||||
onQualityProfileFormatItemScoreChange={this.onQualityProfileFormatItemScoreChange}
|
|
||||||
onChangeMode={this.onChangeMode}
|
|
||||||
onSizeChange={this.onSizeChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
|
@ -1,187 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Card from 'Components/Card';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
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,
|
|
||||||
upgradeAllowed,
|
|
||||||
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={translate('CloneProfile')}
|
|
||||||
name={icons.CLONE}
|
|
||||||
onPress={this.onCloneQualityProfilePress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.qualities}>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
if (!item.allowed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.quality) {
|
|
||||||
const isCutoff = upgradeAllowed && item.quality.id === cutoff;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={item.quality.id}
|
|
||||||
kind={isCutoff ? kinds.INFO : kinds.DEFAULT}
|
|
||||||
title={isCutoff ? translate('UpgradeUntilThisQualityIsMetOrExceeded') : null}
|
|
||||||
>
|
|
||||||
{item.quality.name}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCutoff = upgradeAllowed && item.id === cutoff;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
key={item.id}
|
|
||||||
className={styles.tooltipLabel}
|
|
||||||
anchor={
|
|
||||||
<Label
|
|
||||||
kind={isCutoff ? kinds.INFO : kinds.DEFAULT}
|
|
||||||
title={isCutoff ? translate('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 ? translate('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={translate('DeleteQualityProfile')}
|
|
||||||
message={translate('DeleteQualityProfileMessageText', { name })}
|
|
||||||
confirmLabel={translate('Delete')}
|
|
||||||
isSpinning={isDeleting}
|
|
||||||
onConfirm={this.onConfirmDeleteQualityProfile}
|
|
||||||
onCancel={this.onDeleteQualityProfileModalClose}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityProfile.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
upgradeAllowed: PropTypes.bool.isRequired,
|
|
||||||
cutoff: PropTypes.number.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
|
||||||
onConfirmDeleteQualityProfile: PropTypes.func.isRequired,
|
|
||||||
onCloneQualityProfilePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QualityProfile;
|
|
165
frontend/src/Settings/Profiles/Quality/QualityProfile.tsx
Normal file
165
frontend/src/Settings/Profiles/Quality/QualityProfile.tsx
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
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 { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import { deleteQualityProfile } from 'Store/Actions/settingsActions';
|
||||||
|
import { QualityProfileItems } from 'typings/QualityProfile';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EditQualityProfileModal from './EditQualityProfileModal';
|
||||||
|
import styles from './QualityProfile.css';
|
||||||
|
|
||||||
|
interface QualityProfileProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
upgradeAllowed: boolean;
|
||||||
|
cutoff: number;
|
||||||
|
items: QualityProfileItems;
|
||||||
|
|
||||||
|
isDeleting: boolean;
|
||||||
|
onCloneQualityProfilePress: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityProfile({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
upgradeAllowed,
|
||||||
|
cutoff,
|
||||||
|
items,
|
||||||
|
isDeleting,
|
||||||
|
onCloneQualityProfilePress,
|
||||||
|
}: QualityProfileProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [isEditQualityProfileModalOpen, setIsEditQualityProfileModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const [isDeleteQualityProfileModalOpen, setIsDeleteQualityProfileModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const handleEditQualityProfilePress = useCallback(() => {
|
||||||
|
setIsEditQualityProfileModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEditQualityProfileModalClose = useCallback(() => {
|
||||||
|
setIsEditQualityProfileModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteQualityProfilePress = useCallback(() => {
|
||||||
|
setIsDeleteQualityProfileModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteQualityProfileModalClose = useCallback(() => {
|
||||||
|
setIsDeleteQualityProfileModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleConfirmDeleteQualityProfile = useCallback(() => {
|
||||||
|
dispatch(deleteQualityProfile({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
const handleCloneQualityProfilePress = useCallback(() => {
|
||||||
|
onCloneQualityProfilePress(id);
|
||||||
|
}, [id, onCloneQualityProfilePress]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={styles.qualityProfile}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={handleEditQualityProfilePress}
|
||||||
|
>
|
||||||
|
<div className={styles.nameContainer}>
|
||||||
|
<div className={styles.name}>{name}</div>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
className={styles.cloneButton}
|
||||||
|
title={translate('CloneProfile')}
|
||||||
|
name={icons.CLONE}
|
||||||
|
onPress={handleCloneQualityProfilePress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.qualities}>
|
||||||
|
{items.map((item) => {
|
||||||
|
if (!item.allowed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('quality' in item) {
|
||||||
|
const isCutoff = upgradeAllowed && item.quality.id === cutoff;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={item.quality.id}
|
||||||
|
kind={isCutoff ? kinds.INFO : kinds.DEFAULT}
|
||||||
|
title={
|
||||||
|
isCutoff
|
||||||
|
? translate('UpgradeUntilThisQualityIsMetOrExceeded')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.quality.name}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCutoff = upgradeAllowed && item.id === cutoff;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={item.id}
|
||||||
|
className={styles.tooltipLabel}
|
||||||
|
anchor={
|
||||||
|
<Label
|
||||||
|
kind={isCutoff ? kinds.INFO : kinds.DEFAULT}
|
||||||
|
title={isCutoff ? translate('Cutoff') : undefined}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
tooltip={
|
||||||
|
<div>
|
||||||
|
{item.items.map((groupItem) => {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={groupItem.quality.id}
|
||||||
|
kind={isCutoff ? kinds.INFO : kinds.DEFAULT}
|
||||||
|
title={isCutoff ? translate('Cutoff') : undefined}
|
||||||
|
>
|
||||||
|
{groupItem.quality.name}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditQualityProfileModal
|
||||||
|
id={id}
|
||||||
|
isOpen={isEditQualityProfileModalOpen}
|
||||||
|
onModalClose={handleEditQualityProfileModalClose}
|
||||||
|
onDeleteQualityProfilePress={handleDeleteQualityProfilePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteQualityProfileModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteQualityProfile')}
|
||||||
|
message={translate('DeleteQualityProfileMessageText', { name })}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
isSpinning={isDeleting}
|
||||||
|
onConfirm={handleConfirmDeleteQualityProfile}
|
||||||
|
onCancel={handleDeleteQualityProfileModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityProfile;
|
|
@ -1,68 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import NumberInput from 'Components/Form/NumberInput';
|
|
||||||
import styles from './QualityProfileFormatItem.css';
|
|
||||||
|
|
||||||
class QualityProfileFormatItem extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onScoreChange = ({ value }) => {
|
|
||||||
const {
|
|
||||||
formatId
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.onScoreChange(formatId, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
score
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.qualityProfileFormatItemContainer}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={styles.qualityProfileFormatItem}
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
className={styles.formatNameContainer}
|
|
||||||
>
|
|
||||||
<div className={styles.formatName}>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
<NumberInput
|
|
||||||
containerClassName={styles.scoreContainer}
|
|
||||||
className={styles.scoreInput}
|
|
||||||
name={name}
|
|
||||||
value={score}
|
|
||||||
onChange={this.onScoreChange}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityProfileFormatItem.propTypes = {
|
|
||||||
formatId: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
score: PropTypes.number.isRequired,
|
|
||||||
onScoreChange: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
QualityProfileFormatItem.defaultProps = {
|
|
||||||
// To handle the case score is deleted during edit
|
|
||||||
score: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QualityProfileFormatItem;
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import NumberInput from 'Components/Form/NumberInput';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import styles from './QualityProfileFormatItem.css';
|
||||||
|
|
||||||
|
interface QualityProfileFormatItemProps {
|
||||||
|
formatId: number;
|
||||||
|
name: string;
|
||||||
|
score?: number;
|
||||||
|
onScoreChange: (formatId: number, score: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityProfileFormatItem({
|
||||||
|
formatId,
|
||||||
|
name,
|
||||||
|
score = 0,
|
||||||
|
onScoreChange,
|
||||||
|
}: QualityProfileFormatItemProps) {
|
||||||
|
const handleScoreChange = useCallback(
|
||||||
|
({ value }: InputChanged<number>) => {
|
||||||
|
onScoreChange(formatId, value);
|
||||||
|
},
|
||||||
|
[formatId, onScoreChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.qualityProfileFormatItemContainer}>
|
||||||
|
<div className={styles.qualityProfileFormatItem}>
|
||||||
|
<label className={styles.formatNameContainer}>
|
||||||
|
<div className={styles.formatName}>{name}</div>
|
||||||
|
<NumberInput
|
||||||
|
containerClassName={styles.scoreContainer}
|
||||||
|
className={styles.scoreInput}
|
||||||
|
name={name}
|
||||||
|
value={score}
|
||||||
|
// @ts-expect-error - mismatched types
|
||||||
|
onChange={handleScoreChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityProfileFormatItem;
|
|
@ -1,158 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QualityProfileFormatItem from './QualityProfileFormatItem';
|
|
||||||
import styles from './QualityProfileFormatItems.css';
|
|
||||||
|
|
||||||
function calcOrder(profileFormatItems) {
|
|
||||||
const items = profileFormatItems.reduce((acc, cur, index) => {
|
|
||||||
acc[cur.format] = index;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return [...profileFormatItems].sort((a, b) => {
|
|
||||||
if (b.score !== a.score) {
|
|
||||||
return b.score - a.score;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.name.localeCompare(b.name, undefined, { numeric: true });
|
|
||||||
}).map((x) => items[x.format]);
|
|
||||||
}
|
|
||||||
|
|
||||||
class QualityProfileFormatItems extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
order: calcOrder(this.props.profileFormatItems)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onScoreChange = (formatId, value) => {
|
|
||||||
const {
|
|
||||||
onQualityProfileFormatItemScoreChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onQualityProfileFormatItemScoreChange(formatId, value);
|
|
||||||
this.reorderItems();
|
|
||||||
};
|
|
||||||
|
|
||||||
reorderItems = _.debounce(() => this.setState({ order: calcOrder(this.props.profileFormatItems) }), 1000);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
profileFormatItems,
|
|
||||||
errors,
|
|
||||||
warnings
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
order
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (profileFormatItems.length < 1) {
|
|
||||||
return (
|
|
||||||
<InlineMarkdown className={styles.addCustomFormatMessage} data={translate('WantMoreControlAddACustomFormat')} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
|
||||||
<FormLabel size={sizes.SMALL}>
|
|
||||||
{translate('CustomFormats')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<FormInputHelpText
|
|
||||||
text={translate('CustomFormatHelpText')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
errors.map((error, index) => {
|
|
||||||
return (
|
|
||||||
<FormInputHelpText
|
|
||||||
key={index}
|
|
||||||
text={error.message}
|
|
||||||
isError={true}
|
|
||||||
isCheckInput={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
warnings.map((warning, index) => {
|
|
||||||
return (
|
|
||||||
<FormInputHelpText
|
|
||||||
key={index}
|
|
||||||
text={warning.message}
|
|
||||||
isWarning={true}
|
|
||||||
isCheckInput={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.formats}>
|
|
||||||
<div className={styles.headerContainer}>
|
|
||||||
<div className={styles.headerTitle}>
|
|
||||||
{translate('CustomFormat')}
|
|
||||||
</div>
|
|
||||||
<div className={styles.headerScore}>
|
|
||||||
{translate('Score')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
order.map((index) => {
|
|
||||||
const {
|
|
||||||
format,
|
|
||||||
name,
|
|
||||||
score
|
|
||||||
} = profileFormatItems[index];
|
|
||||||
return (
|
|
||||||
<QualityProfileFormatItem
|
|
||||||
key={format}
|
|
||||||
formatId={format}
|
|
||||||
name={name}
|
|
||||||
score={score}
|
|
||||||
onScoreChange={this.onScoreChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityProfileFormatItems.propTypes = {
|
|
||||||
profileFormatItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
onQualityProfileFormatItemScoreChange: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
QualityProfileFormatItems.defaultProps = {
|
|
||||||
errors: [],
|
|
||||||
warnings: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QualityProfileFormatItems;
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { QualityProfileFormatItem as QualityProfileFormatItemModel } from 'typings/CustomFormat';
|
||||||
|
import { Failure } from 'typings/pending';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QualityProfileFormatItem from './QualityProfileFormatItem';
|
||||||
|
import styles from './QualityProfileFormatItems.css';
|
||||||
|
|
||||||
|
interface QualityProfileFormatItemsProps {
|
||||||
|
profileFormatItems: QualityProfileFormatItemModel[];
|
||||||
|
errors?: Failure[];
|
||||||
|
warnings?: Failure[];
|
||||||
|
onQualityProfileFormatItemScoreChange: (
|
||||||
|
formatId: number,
|
||||||
|
score: number
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityProfileFormatItems({
|
||||||
|
profileFormatItems,
|
||||||
|
errors = [],
|
||||||
|
warnings = [],
|
||||||
|
onQualityProfileFormatItemScoreChange,
|
||||||
|
}: QualityProfileFormatItemsProps) {
|
||||||
|
const order = useMemo(() => {
|
||||||
|
const items = profileFormatItems.reduce<Record<number, number>>(
|
||||||
|
(acc, cur, index) => {
|
||||||
|
acc[cur.format] = index;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...profileFormatItems]
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (b.score !== a.score) {
|
||||||
|
return b.score - a.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.name.localeCompare(b.name, undefined, { numeric: true });
|
||||||
|
})
|
||||||
|
.map((x) => items[x.format]);
|
||||||
|
}, [profileFormatItems]);
|
||||||
|
|
||||||
|
if (profileFormatItems.length < 1) {
|
||||||
|
return (
|
||||||
|
<InlineMarkdown
|
||||||
|
className={styles.addCustomFormatMessage}
|
||||||
|
data={translate('WantMoreControlAddACustomFormat')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>{translate('CustomFormats')}</FormLabel>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormInputHelpText text={translate('CustomFormatHelpText')} />
|
||||||
|
|
||||||
|
{errors.map((error, index) => {
|
||||||
|
return (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={error.message}
|
||||||
|
isError={true}
|
||||||
|
isCheckInput={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{warnings.map((warning, index) => {
|
||||||
|
return (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={warning.message}
|
||||||
|
isWarning={true}
|
||||||
|
isCheckInput={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className={styles.formats}>
|
||||||
|
<div className={styles.headerContainer}>
|
||||||
|
<div className={styles.headerTitle}>
|
||||||
|
{translate('CustomFormat')}
|
||||||
|
</div>
|
||||||
|
<div className={styles.headerScore}>{translate('Score')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{order.map((index) => {
|
||||||
|
const { format, name, score } = profileFormatItems[index];
|
||||||
|
return (
|
||||||
|
<QualityProfileFormatItem
|
||||||
|
key={format}
|
||||||
|
formatId={format}
|
||||||
|
name={name}
|
||||||
|
score={score}
|
||||||
|
onScoreChange={onQualityProfileFormatItemScoreChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityProfileFormatItems;
|
|
@ -1,162 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QualityProfileItemSize from './QualityProfileItemSize';
|
|
||||||
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 {
|
|
||||||
mode,
|
|
||||||
isPreview,
|
|
||||||
qualityId,
|
|
||||||
groupId,
|
|
||||||
name,
|
|
||||||
allowed,
|
|
||||||
minSize,
|
|
||||||
maxSize,
|
|
||||||
preferredSize,
|
|
||||||
isDragging,
|
|
||||||
isOverCurrent,
|
|
||||||
connectDragSource,
|
|
||||||
onSizeChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.qualityProfileItem,
|
|
||||||
mode === 'editSizes' && styles.editSizes,
|
|
||||||
isDragging && styles.isDragging,
|
|
||||||
isPreview && styles.isPreview,
|
|
||||||
isOverCurrent && styles.isOverCurrent,
|
|
||||||
groupId && styles.isInGroup
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
className={classNames(
|
|
||||||
styles.qualityNameContainer,
|
|
||||||
mode === 'editSizes' && styles.editSizes
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
mode === 'editGroups' && !groupId && !isPreview &&
|
|
||||||
<IconButton
|
|
||||||
className={styles.createGroupButton}
|
|
||||||
name={icons.GROUP}
|
|
||||||
title={translate('Group')}
|
|
||||||
onPress={this.onCreateGroupPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
mode === 'default' &&
|
|
||||||
<CheckInput
|
|
||||||
className={styles.checkInput}
|
|
||||||
containerClassName={styles.checkInputContainer}
|
|
||||||
name={name}
|
|
||||||
value={allowed}
|
|
||||||
isDisabled={!!groupId}
|
|
||||||
onChange={this.onAllowedChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={classNames(
|
|
||||||
styles.qualityName,
|
|
||||||
groupId && mode !== 'editSizes' && styles.isInGroup,
|
|
||||||
!allowed && styles.notAllowed
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{
|
|
||||||
mode === 'editSizes' && qualityId != null ?
|
|
||||||
<div>
|
|
||||||
<QualityProfileItemSize
|
|
||||||
id={qualityId}
|
|
||||||
minSize={minSize}
|
|
||||||
maxSize={maxSize}
|
|
||||||
preferredSize={preferredSize}
|
|
||||||
onSizeChange={onSizeChange}
|
|
||||||
/>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
mode === 'editSizes' ? null :
|
|
||||||
connectDragSource(
|
|
||||||
<div className={styles.dragHandle}>
|
|
||||||
<Icon
|
|
||||||
className={styles.dragIcon}
|
|
||||||
title={translate('CreateGroup')}
|
|
||||||
name={icons.REORDER}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityProfileItem.propTypes = {
|
|
||||||
mode: PropTypes.string.isRequired,
|
|
||||||
isPreview: PropTypes.bool,
|
|
||||||
groupId: PropTypes.number,
|
|
||||||
qualityId: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
allowed: PropTypes.bool.isRequired,
|
|
||||||
minSize: PropTypes.number,
|
|
||||||
maxSize: PropTypes.number,
|
|
||||||
preferredSize: PropTypes.number,
|
|
||||||
isDragging: PropTypes.bool.isRequired,
|
|
||||||
isOverCurrent: PropTypes.bool.isRequired,
|
|
||||||
isInGroup: PropTypes.bool,
|
|
||||||
connectDragSource: PropTypes.func,
|
|
||||||
onCreateGroupPress: PropTypes.func,
|
|
||||||
onQualityProfileItemAllowedChange: PropTypes.func,
|
|
||||||
onSizeChange: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
QualityProfileItem.defaultProps = {
|
|
||||||
mode: 'default',
|
|
||||||
isPreview: false,
|
|
||||||
isOverCurrent: false,
|
|
||||||
// The drag preview will not connect the drag handle.
|
|
||||||
connectDragSource: (node) => node
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QualityProfileItem;
|
|
129
frontend/src/Settings/Profiles/Quality/QualityProfileItem.tsx
Normal file
129
frontend/src/Settings/Profiles/Quality/QualityProfileItem.tsx
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { ConnectDragSource } from 'react-dnd';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QualityProfileItemSize, { SizeChanged } from './QualityProfileItemSize';
|
||||||
|
import styles from './QualityProfileItem.css';
|
||||||
|
|
||||||
|
interface QualityProfileItemProps {
|
||||||
|
dragRef: ConnectDragSource;
|
||||||
|
mode: string;
|
||||||
|
isPreview?: boolean;
|
||||||
|
groupId?: number;
|
||||||
|
qualityId: number;
|
||||||
|
name: string;
|
||||||
|
allowed: boolean;
|
||||||
|
minSize: number | null;
|
||||||
|
maxSize: number | null;
|
||||||
|
preferredSize: number | null;
|
||||||
|
isDragging: boolean;
|
||||||
|
onCreateGroupPress?: (qualityId: number) => void;
|
||||||
|
onItemAllowedChange: (qualityId: number, allowed: boolean) => void;
|
||||||
|
onSizeChange: (change: SizeChanged) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityProfileItem({
|
||||||
|
dragRef,
|
||||||
|
mode = 'default',
|
||||||
|
isPreview = false,
|
||||||
|
qualityId,
|
||||||
|
groupId,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
minSize,
|
||||||
|
maxSize,
|
||||||
|
isDragging,
|
||||||
|
preferredSize,
|
||||||
|
onCreateGroupPress,
|
||||||
|
onItemAllowedChange,
|
||||||
|
onSizeChange,
|
||||||
|
}: QualityProfileItemProps) {
|
||||||
|
const handleAllowedChange = useCallback(
|
||||||
|
({ value }: InputChanged<boolean>) => {
|
||||||
|
onItemAllowedChange?.(qualityId, value);
|
||||||
|
},
|
||||||
|
[qualityId, onItemAllowedChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateGroupPress = useCallback(() => {
|
||||||
|
onCreateGroupPress?.(qualityId);
|
||||||
|
}, [qualityId, onCreateGroupPress]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileItem,
|
||||||
|
mode === 'editSizes' && styles.editSizes,
|
||||||
|
isDragging && styles.isDragging,
|
||||||
|
isPreview && styles.isPreview,
|
||||||
|
groupId && styles.isInGroup
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityNameContainer,
|
||||||
|
mode === 'editSizes' && styles.editSizes
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{mode === 'editGroups' && !groupId && !isPreview && (
|
||||||
|
<IconButton
|
||||||
|
className={styles.createGroupButton}
|
||||||
|
name={icons.GROUP}
|
||||||
|
title={translate('Group')}
|
||||||
|
onPress={handleCreateGroupPress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mode === 'default' && (
|
||||||
|
<CheckInput
|
||||||
|
className={styles.checkInput}
|
||||||
|
containerClassName={styles.checkInputContainer}
|
||||||
|
name={name}
|
||||||
|
value={allowed}
|
||||||
|
isDisabled={!!groupId}
|
||||||
|
onChange={handleAllowedChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityName,
|
||||||
|
groupId && mode !== 'editSizes' && styles.isInGroup,
|
||||||
|
!allowed && styles.notAllowed
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{mode === 'editSizes' && qualityId != null ? (
|
||||||
|
<div>
|
||||||
|
<QualityProfileItemSize
|
||||||
|
id={qualityId}
|
||||||
|
minSize={minSize}
|
||||||
|
maxSize={maxSize}
|
||||||
|
preferredSize={preferredSize}
|
||||||
|
onSizeChange={onSizeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mode === 'editSizes' ? null : (
|
||||||
|
<div ref={dragRef} className={styles.dragHandle}>
|
||||||
|
<Icon
|
||||||
|
className={styles.dragIcon}
|
||||||
|
title={translate('CreateGroup')}
|
||||||
|
name={icons.REORDER}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityProfileItem;
|
|
@ -1,4 +0,0 @@
|
||||||
.dragPreview {
|
|
||||||
width: 380px;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'dragPreview': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,92 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { DragLayer } from 'react-dnd';
|
|
||||||
import DragPreviewLayer from 'Components/DragPreviewLayer';
|
|
||||||
import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions.js';
|
|
||||||
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);
|
|
|
@ -1,10 +1,10 @@
|
||||||
.qualityProfileItemDragSource {
|
.qualityProfileItemDragSource {
|
||||||
padding: $qualityProfileItemDragSourcePadding 0;
|
margin: $qualityProfileItemDragSourcePadding 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qualityProfileItemPlaceholder {
|
.qualityProfileItemPlaceholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: $qualityProfileItemHeight;
|
/* height: $qualityProfileItemHeight; */
|
||||||
border: 1px dotted #aaa;
|
border: 1px dotted #aaa;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,254 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { DragSource, DropTarget } from 'react-dnd';
|
|
||||||
import { findDOMNode } from 'react-dom';
|
|
||||||
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 {
|
|
||||||
mode,
|
|
||||||
qualityIndex,
|
|
||||||
groupId,
|
|
||||||
qualityId,
|
|
||||||
name,
|
|
||||||
allowed
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return {
|
|
||||||
mode,
|
|
||||||
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 {
|
|
||||||
mode,
|
|
||||||
groupId,
|
|
||||||
qualityId,
|
|
||||||
name,
|
|
||||||
allowed,
|
|
||||||
items,
|
|
||||||
minSize,
|
|
||||||
maxSize,
|
|
||||||
preferredSize,
|
|
||||||
qualityIndex,
|
|
||||||
isDragging,
|
|
||||||
isDraggingUp,
|
|
||||||
isDraggingDown,
|
|
||||||
isOverCurrent,
|
|
||||||
connectDragSource,
|
|
||||||
connectDropTarget,
|
|
||||||
onCreateGroupPress,
|
|
||||||
onDeleteGroupPress,
|
|
||||||
onQualityProfileItemAllowedChange,
|
|
||||||
onItemGroupAllowedChange,
|
|
||||||
onItemGroupNameChange,
|
|
||||||
onQualityProfileItemDragMove,
|
|
||||||
onQualityProfileItemDragEnd,
|
|
||||||
onSizeChange
|
|
||||||
} = 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
|
|
||||||
mode={mode}
|
|
||||||
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}
|
|
||||||
onSizeChange={onSizeChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
qualityId != null &&
|
|
||||||
<QualityProfileItem
|
|
||||||
mode={mode}
|
|
||||||
groupId={groupId}
|
|
||||||
qualityId={qualityId}
|
|
||||||
name={name}
|
|
||||||
allowed={allowed}
|
|
||||||
minSize={minSize}
|
|
||||||
maxSize={maxSize}
|
|
||||||
preferredSize={preferredSize}
|
|
||||||
qualityIndex={qualityIndex}
|
|
||||||
isDragging={isDragging}
|
|
||||||
isOverCurrent={isOverCurrent}
|
|
||||||
connectDragSource={connectDragSource}
|
|
||||||
onCreateGroupPress={onCreateGroupPress}
|
|
||||||
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
|
||||||
onSizeChange={onSizeChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAfter &&
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.qualityProfileItemPlaceholder,
|
|
||||||
styles.qualityProfileItemPlaceholderAfter
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityProfileItemDragSource.propTypes = {
|
|
||||||
mode: PropTypes.string.isRequired,
|
|
||||||
groupId: PropTypes.number,
|
|
||||||
qualityId: PropTypes.number,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
allowed: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
minSize: PropTypes.number,
|
|
||||||
maxSize: PropTypes.number,
|
|
||||||
preferredSize: PropTypes.number,
|
|
||||||
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,
|
|
||||||
onSizeChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DropTarget(
|
|
||||||
QUALITY_PROFILE_ITEM,
|
|
||||||
qualityProfileItemDropTarget,
|
|
||||||
collectDropTarget
|
|
||||||
)(DragSource(
|
|
||||||
QUALITY_PROFILE_ITEM,
|
|
||||||
qualityProfileItemDragSource,
|
|
||||||
collectDragSource
|
|
||||||
)(QualityProfileItemDragSource));
|
|
|
@ -0,0 +1,251 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { DragSourceMonitor, useDrag, useDrop, XYCoord } from 'react-dnd';
|
||||||
|
import DragType from 'Helpers/DragType';
|
||||||
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
|
import { qualityProfileItemHeight } from 'Styles/Variables/dimensions';
|
||||||
|
import { QualityProfileQualityItem } from 'typings/QualityProfile';
|
||||||
|
import QualityProfileItem from './QualityProfileItem';
|
||||||
|
import QualityProfileItemGroup from './QualityProfileItemGroup';
|
||||||
|
import { SizeChanged } from './QualityProfileItemSize';
|
||||||
|
import styles from './QualityProfileItemDragSource.css';
|
||||||
|
|
||||||
|
export interface DragMoveState {
|
||||||
|
dragQualityIndex: string | null;
|
||||||
|
dropQualityIndex: string | null;
|
||||||
|
dropPosition: 'above' | 'below' | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DragItem {
|
||||||
|
mode: string;
|
||||||
|
qualityIndex: string;
|
||||||
|
groupId: number | undefined;
|
||||||
|
qualityId: number | undefined;
|
||||||
|
isGroup: boolean;
|
||||||
|
name: string;
|
||||||
|
allowed: boolean;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ItemProps {
|
||||||
|
groupId: number | undefined;
|
||||||
|
qualityId: number;
|
||||||
|
name: string;
|
||||||
|
minSize: number | null;
|
||||||
|
maxSize: number | null;
|
||||||
|
preferredSize: number | null;
|
||||||
|
isInGroup?: boolean;
|
||||||
|
onCreateGroupPress?: (qualityId: number) => void;
|
||||||
|
onItemAllowedChange: (id: number, allowd: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupProps {
|
||||||
|
groupId: number;
|
||||||
|
qualityId: undefined;
|
||||||
|
items: QualityProfileQualityItem[];
|
||||||
|
qualityIndex: string;
|
||||||
|
onDeleteGroupPress: (groupId: number) => void;
|
||||||
|
onItemAllowedChange: (id: number, allowd: boolean) => void;
|
||||||
|
onGroupAllowedChange: (id: number, allowd: boolean) => void;
|
||||||
|
onItemGroupNameChange: (groupId: number, name: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommonProps {
|
||||||
|
mode: string;
|
||||||
|
name: string;
|
||||||
|
allowed: boolean;
|
||||||
|
qualityIndex: string;
|
||||||
|
isDraggingUp: boolean;
|
||||||
|
isDraggingDown: boolean;
|
||||||
|
onDragMove: (move: DragMoveState) => void;
|
||||||
|
onDragEnd: (didDrop: boolean) => void;
|
||||||
|
onSizeChange: (sizeChange: SizeChanged) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QualityProfileItemDragSourceProps = CommonProps &
|
||||||
|
(ItemProps | GroupProps);
|
||||||
|
|
||||||
|
export interface QualityProfileItemDragSourceActionProps {
|
||||||
|
onCreateGroupPress?: (qualityId: number) => void;
|
||||||
|
onItemAllowedChange: (id: number, allowd: boolean) => void;
|
||||||
|
onDeleteGroupPress: (groupId: number) => void;
|
||||||
|
onGroupAllowedChange: (id: number, allowd: boolean) => void;
|
||||||
|
onItemGroupNameChange: (groupId: number, name: string) => void;
|
||||||
|
onDragMove: (move: DragMoveState) => void;
|
||||||
|
onDragEnd: (didDrop: boolean) => void;
|
||||||
|
onSizeChange: (sizeChange: SizeChanged) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityProfileItemDragSource({
|
||||||
|
mode,
|
||||||
|
groupId,
|
||||||
|
qualityId,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
qualityIndex,
|
||||||
|
isDraggingDown,
|
||||||
|
isDraggingUp,
|
||||||
|
onDragMove,
|
||||||
|
onDragEnd,
|
||||||
|
...otherProps
|
||||||
|
}: QualityProfileItemDragSourceProps) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [measureRef, { height }] = useMeasure();
|
||||||
|
|
||||||
|
const [{ isOver, dragHeight }, dropRef] = useDrop<
|
||||||
|
DragItem,
|
||||||
|
void,
|
||||||
|
{ isOver: boolean; dragHeight: number }
|
||||||
|
>({
|
||||||
|
accept: DragType.QualityProfileItem,
|
||||||
|
collect(monitor) {
|
||||||
|
return {
|
||||||
|
isOver: monitor.isOver(),
|
||||||
|
dragHeight: monitor.getItem()?.height ?? qualityProfileItemHeight,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hover(item: DragItem, monitor) {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { qualityIndex: dragQualityIndex, isGroup: isDragGroup } = item;
|
||||||
|
|
||||||
|
const dropQualityIndex = qualityIndex;
|
||||||
|
const isDropGroupItem = !!(qualityId && groupId);
|
||||||
|
|
||||||
|
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||||
|
const hoverHeight = hoverBoundingRect.bottom - hoverBoundingRect.top;
|
||||||
|
|
||||||
|
// Smooth out updates when dragging down and the size grows to avoid flickering
|
||||||
|
const hoverMiddleY = Math.max(hoverHeight - height, height) / 2;
|
||||||
|
|
||||||
|
const clientOffset = monitor.getClientOffset();
|
||||||
|
const hoverClientY = (clientOffset as XYCoord).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: 'above' | 'below' | null = null;
|
||||||
|
|
||||||
|
// Determine drop position based on position over target
|
||||||
|
if (hoverClientY > hoverMiddleY) {
|
||||||
|
dropPosition = 'below';
|
||||||
|
} else if (hoverClientY < hoverMiddleY) {
|
||||||
|
dropPosition = 'above';
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragMove({
|
||||||
|
dragQualityIndex,
|
||||||
|
dropQualityIndex,
|
||||||
|
dropPosition,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [{ isDragging }, dragRef, previewRef] = useDrag<
|
||||||
|
DragItem,
|
||||||
|
unknown,
|
||||||
|
{ isDragging: boolean }
|
||||||
|
>({
|
||||||
|
type: DragType.QualityProfileItem,
|
||||||
|
item: () => {
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
qualityIndex,
|
||||||
|
groupId,
|
||||||
|
qualityId,
|
||||||
|
isGroup: !qualityId,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
collect: (monitor: DragSourceMonitor<unknown, unknown>) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
end: (_item: DragItem, monitor) => {
|
||||||
|
onDragEnd(monitor.didDrop());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dropRef(previewRef(ref));
|
||||||
|
|
||||||
|
const isBefore = !isDragging && isDraggingUp && isOver;
|
||||||
|
const isAfter = !isDragging && isDraggingDown && isOver;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={classNames(styles.qualityProfileItemDragSource)}>
|
||||||
|
{isBefore ? (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileItemPlaceholder,
|
||||||
|
styles.qualityProfileItemPlaceholderBefore
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
height: dragHeight,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div ref={measureRef}>
|
||||||
|
{'items' in otherProps && groupId ? (
|
||||||
|
<QualityProfileItemGroup
|
||||||
|
{...otherProps}
|
||||||
|
dragRef={dragRef}
|
||||||
|
mode={mode}
|
||||||
|
groupId={groupId}
|
||||||
|
name={name}
|
||||||
|
allowed={allowed}
|
||||||
|
qualityIndex={qualityIndex}
|
||||||
|
isDragging={isDragging}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onDragMove={onDragMove}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!('items' in otherProps) && qualityId ? (
|
||||||
|
<QualityProfileItem
|
||||||
|
{...otherProps}
|
||||||
|
dragRef={dragRef}
|
||||||
|
mode={mode}
|
||||||
|
qualityId={qualityId}
|
||||||
|
name={name}
|
||||||
|
allowed={allowed}
|
||||||
|
isDragging={isDragging}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAfter ? (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileItemPlaceholder,
|
||||||
|
styles.qualityProfileItemPlaceholderAfter
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
height: dragHeight,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityProfileItemDragSource;
|
|
@ -1,228 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
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 {
|
|
||||||
mode,
|
|
||||||
groupId,
|
|
||||||
name,
|
|
||||||
allowed,
|
|
||||||
items,
|
|
||||||
qualityIndex,
|
|
||||||
isDragging,
|
|
||||||
isDraggingUp,
|
|
||||||
isDraggingDown,
|
|
||||||
connectDragSource,
|
|
||||||
onQualityProfileItemAllowedChange,
|
|
||||||
onQualityProfileItemDragMove,
|
|
||||||
onQualityProfileItemDragEnd,
|
|
||||||
onSizeChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.qualityProfileItemGroup,
|
|
||||||
mode === 'editGroups' && styles.editGroups,
|
|
||||||
mode === 'editSizes' && styles.editSizes,
|
|
||||||
isDragging && styles.isDragging
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={styles.qualityProfileItemGroupInfo}>
|
|
||||||
{
|
|
||||||
mode === 'editGroups' &&
|
|
||||||
<div className={styles.qualityNameContainer}>
|
|
||||||
<IconButton
|
|
||||||
className={styles.deleteGroupButton}
|
|
||||||
name={icons.UNGROUP}
|
|
||||||
title={translate('Ungroup')}
|
|
||||||
onPress={this.onDeleteGroupPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
className={styles.nameInput}
|
|
||||||
name="name"
|
|
||||||
value={name}
|
|
||||||
onChange={this.onNameChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
mode === 'default' &&
|
|
||||||
<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>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
mode === 'editSizes' &&
|
|
||||||
<label
|
|
||||||
className={styles.editSizesQualityNameLabel}
|
|
||||||
>
|
|
||||||
<div className={styles.nameContainer}>
|
|
||||||
<div className={classNames(
|
|
||||||
styles.name,
|
|
||||||
!allowed && styles.notAllowed
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
mode === 'editSizes' ? null :
|
|
||||||
connectDragSource(
|
|
||||||
<div className={styles.dragHandle}>
|
|
||||||
<Icon
|
|
||||||
className={styles.dragIcon}
|
|
||||||
name={icons.REORDER}
|
|
||||||
title={translate('Reorder')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
mode === 'default' ?
|
|
||||||
null :
|
|
||||||
<div className={mode === 'editGroups' ? styles.items : undefined}>
|
|
||||||
{
|
|
||||||
items.map(({ quality }, index) => {
|
|
||||||
return (
|
|
||||||
<QualityProfileItemDragSource
|
|
||||||
key={quality.id}
|
|
||||||
mode={mode}
|
|
||||||
groupId={groupId}
|
|
||||||
qualityId={quality.id}
|
|
||||||
name={quality.name}
|
|
||||||
allowed={allowed}
|
|
||||||
minSize={quality.minSize}
|
|
||||||
maxSize={quality.maxSize}
|
|
||||||
preferredSize={quality.preferredSize}
|
|
||||||
items={items}
|
|
||||||
qualityIndex={`${qualityIndex}.${index + 1}`}
|
|
||||||
isDragging={isDragging}
|
|
||||||
isDraggingUp={isDraggingUp}
|
|
||||||
isDraggingDown={isDraggingDown}
|
|
||||||
isInGroup={true}
|
|
||||||
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
|
||||||
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
|
|
||||||
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
|
|
||||||
onSizeChange={onSizeChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}).reverse()
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityProfileItemGroup.propTypes = {
|
|
||||||
mode: PropTypes.string.isRequired,
|
|
||||||
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,
|
|
||||||
onSizeChange: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
QualityProfileItemGroup.defaultProps = {
|
|
||||||
mode: 'default',
|
|
||||||
// The drag preview will not connect the drag handle.
|
|
||||||
connectDragSource: (node) => node
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QualityProfileItemGroup;
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { ConnectDragSource } from 'react-dnd';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import { QualityProfileQualityItem } from 'typings/QualityProfile';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QualityProfileItemDragSource, {
|
||||||
|
DragMoveState,
|
||||||
|
} from './QualityProfileItemDragSource';
|
||||||
|
import { SizeChanged } from './QualityProfileItemSize';
|
||||||
|
import styles from './QualityProfileItemGroup.css';
|
||||||
|
|
||||||
|
interface QualityProfileItemGroupProps {
|
||||||
|
dragRef: ConnectDragSource;
|
||||||
|
mode?: string;
|
||||||
|
groupId: number;
|
||||||
|
name: string;
|
||||||
|
allowed: boolean;
|
||||||
|
items: QualityProfileQualityItem[];
|
||||||
|
qualityIndex: string;
|
||||||
|
isDragging: boolean;
|
||||||
|
isDraggingUp: boolean;
|
||||||
|
isDraggingDown: boolean;
|
||||||
|
onGroupAllowedChange: (groupId: number, allowed: boolean) => void;
|
||||||
|
onItemAllowedChange: (groupId: number, allowed: boolean) => void;
|
||||||
|
onItemGroupNameChange: (groupId: number, name: string) => void;
|
||||||
|
onDeleteGroupPress: (groupId: number) => void;
|
||||||
|
onDragMove: (drag: DragMoveState) => void;
|
||||||
|
onDragEnd: (didDrop: boolean) => void;
|
||||||
|
onSizeChange: (sizeChange: SizeChanged) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityProfileItemGroup({
|
||||||
|
dragRef,
|
||||||
|
mode = 'default',
|
||||||
|
groupId,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
items,
|
||||||
|
qualityIndex,
|
||||||
|
isDragging,
|
||||||
|
isDraggingUp,
|
||||||
|
isDraggingDown,
|
||||||
|
onDeleteGroupPress,
|
||||||
|
onGroupAllowedChange,
|
||||||
|
onItemAllowedChange,
|
||||||
|
onItemGroupNameChange,
|
||||||
|
onDragMove,
|
||||||
|
onDragEnd,
|
||||||
|
onSizeChange,
|
||||||
|
}: QualityProfileItemGroupProps) {
|
||||||
|
const handleAllowedChange = useCallback(
|
||||||
|
({ value }: InputChanged<boolean>) => {
|
||||||
|
onGroupAllowedChange?.(groupId, value);
|
||||||
|
},
|
||||||
|
[groupId, onGroupAllowedChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNameChange = useCallback(
|
||||||
|
({ value }: InputChanged<string>) => {
|
||||||
|
onItemGroupNameChange?.(groupId, value);
|
||||||
|
},
|
||||||
|
[groupId, onItemGroupNameChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteGroupPress = useCallback(() => {
|
||||||
|
onDeleteGroupPress?.(groupId);
|
||||||
|
}, [groupId, onDeleteGroupPress]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileItemGroup,
|
||||||
|
mode === 'editGroups' && styles.editGroups,
|
||||||
|
mode === 'editSizes' && styles.editSizes,
|
||||||
|
isDragging && styles.isDragging
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.qualityProfileItemGroupInfo}>
|
||||||
|
{mode === 'editGroups' ? (
|
||||||
|
<div className={styles.qualityNameContainer}>
|
||||||
|
<IconButton
|
||||||
|
className={styles.deleteGroupButton}
|
||||||
|
name={icons.UNGROUP}
|
||||||
|
title={translate('Ungroup')}
|
||||||
|
onPress={handleDeleteGroupPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
className={styles.nameInput}
|
||||||
|
name="name"
|
||||||
|
value={name}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mode === 'default' ? (
|
||||||
|
<label className={styles.qualityNameLabel}>
|
||||||
|
<CheckInput
|
||||||
|
className={styles.checkInput}
|
||||||
|
containerClassName={styles.checkInputContainer}
|
||||||
|
name="allowed"
|
||||||
|
value={allowed}
|
||||||
|
onChange={handleAllowedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mode === 'editSizes' ? (
|
||||||
|
<label className={styles.editSizesQualityNameLabel}>
|
||||||
|
<div className={styles.nameContainer}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.name,
|
||||||
|
!allowed && styles.notAllowed
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mode === 'editSizes' ? null : (
|
||||||
|
<div ref={dragRef} className={styles.dragHandle}>
|
||||||
|
<Icon
|
||||||
|
className={styles.dragIcon}
|
||||||
|
name={icons.REORDER}
|
||||||
|
title={translate('Reorder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mode === 'default' ? null : (
|
||||||
|
<div className={mode === 'editGroups' ? styles.items : undefined}>
|
||||||
|
{items
|
||||||
|
.map(({ quality }, index) => {
|
||||||
|
return (
|
||||||
|
<QualityProfileItemDragSource
|
||||||
|
key={quality.id}
|
||||||
|
mode={mode}
|
||||||
|
groupId={groupId}
|
||||||
|
qualityId={quality.id}
|
||||||
|
name={quality.name}
|
||||||
|
allowed={allowed}
|
||||||
|
minSize={quality.minSize}
|
||||||
|
maxSize={quality.maxSize}
|
||||||
|
preferredSize={quality.preferredSize}
|
||||||
|
qualityIndex={`${qualityIndex}.${index + 1}`}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
isInGroup={true}
|
||||||
|
onItemAllowedChange={onItemAllowedChange}
|
||||||
|
onDragMove={onDragMove}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onSizeChange={onSizeChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.reverse()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityProfileItemGroup;
|
|
@ -23,12 +23,16 @@ interface SizeProps {
|
||||||
maxSize: number | null;
|
maxSize: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnSizeChangeArguments extends SizeProps {
|
export interface SizeChanged extends SizeProps {
|
||||||
id: number;
|
qualityId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QualityProfileItemSizeProps extends OnSizeChangeArguments {
|
export interface QualityProfileItemSizeProps {
|
||||||
onSizeChange: (props: OnSizeChangeArguments) => void;
|
id: number;
|
||||||
|
minSize: number | null;
|
||||||
|
preferredSize: number | null;
|
||||||
|
maxSize: number | null;
|
||||||
|
onSizeChange: (props: SizeChanged) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function trackRenderer(props: HTMLProps<HTMLDivElement>) {
|
function trackRenderer(props: HTMLProps<HTMLDivElement>) {
|
||||||
|
@ -45,10 +49,13 @@ function getSliderValue(value: number | null, defaultValue: number): number {
|
||||||
return roundNumber(sliderValue);
|
return roundNumber(sliderValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QualityProfileItemSize(
|
export default function QualityProfileItemSize({
|
||||||
props: QualityProfileItemSizeProps
|
id,
|
||||||
) {
|
minSize,
|
||||||
const { id, minSize, maxSize, preferredSize, onSizeChange } = props;
|
maxSize,
|
||||||
|
preferredSize,
|
||||||
|
onSizeChange,
|
||||||
|
}: QualityProfileItemSizeProps) {
|
||||||
const [sizes, setSizes] = useState<SizeProps>({
|
const [sizes, setSizes] = useState<SizeProps>({
|
||||||
minSize: getSliderValue(minSize, MIN),
|
minSize: getSliderValue(minSize, MIN),
|
||||||
preferredSize: getSliderValue(preferredSize, SLIDER_MAX - MIN_DISTANCE),
|
preferredSize: getSliderValue(preferredSize, SLIDER_MAX - MIN_DISTANCE),
|
||||||
|
@ -61,21 +68,14 @@ export default function QualityProfileItemSize(
|
||||||
number,
|
number,
|
||||||
number
|
number
|
||||||
]) => {
|
]) => {
|
||||||
// console.log('Sizes:', sliderMinSize, sliderPreferredSize, sliderMaxSize);
|
|
||||||
console.log(
|
|
||||||
'Min Sizes: ',
|
|
||||||
sliderMinSize,
|
|
||||||
roundNumber(Math.pow(sliderMinSize, 1.1))
|
|
||||||
);
|
|
||||||
|
|
||||||
setSizes({
|
setSizes({
|
||||||
minSize: sliderMinSize,
|
minSize: sliderMinSize,
|
||||||
preferredSize: sliderPreferredSize,
|
preferredSize: sliderPreferredSize,
|
||||||
maxSize: sliderMaxSize,
|
maxSize: sliderMaxSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
onSizeChange({
|
onSizeChange?.({
|
||||||
id,
|
qualityId: id,
|
||||||
minSize: roundNumber(Math.pow(sliderMinSize, 1.1)),
|
minSize: roundNumber(Math.pow(sliderMinSize, 1.1)),
|
||||||
preferredSize:
|
preferredSize:
|
||||||
sliderPreferredSize === MAX - MIN_DISTANCE
|
sliderPreferredSize === MAX - MIN_DISTANCE
|
||||||
|
@ -98,8 +98,8 @@ export default function QualityProfileItemSize(
|
||||||
maxSize: sizes.maxSize,
|
maxSize: sizes.maxSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
onSizeChange({
|
onSizeChange?.({
|
||||||
id,
|
qualityId: id,
|
||||||
minSize: value,
|
minSize: value,
|
||||||
preferredSize: sizes.preferredSize,
|
preferredSize: sizes.preferredSize,
|
||||||
maxSize: sizes.maxSize,
|
maxSize: sizes.maxSize,
|
||||||
|
@ -116,8 +116,8 @@ export default function QualityProfileItemSize(
|
||||||
maxSize: sizes.maxSize,
|
maxSize: sizes.maxSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
onSizeChange({
|
onSizeChange?.({
|
||||||
id,
|
qualityId: id,
|
||||||
minSize: sizes.minSize,
|
minSize: sizes.minSize,
|
||||||
preferredSize: value,
|
preferredSize: value,
|
||||||
maxSize: sizes.maxSize,
|
maxSize: sizes.maxSize,
|
||||||
|
@ -134,8 +134,8 @@ export default function QualityProfileItemSize(
|
||||||
maxSize: value,
|
maxSize: value,
|
||||||
});
|
});
|
||||||
|
|
||||||
onSizeChange({
|
onSizeChange?.({
|
||||||
id,
|
qualityId: id,
|
||||||
minSize: sizes.minSize,
|
minSize: sizes.minSize,
|
||||||
preferredSize: sizes.preferredSize,
|
preferredSize: sizes.preferredSize,
|
||||||
maxSize: value,
|
maxSize: value,
|
||||||
|
|
|
@ -1,204 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QualityProfileItemDragPreview from './QualityProfileItemDragPreview';
|
|
||||||
import QualityProfileItemDragSource from './QualityProfileItemDragSource';
|
|
||||||
import styles from './QualityProfileItems.css';
|
|
||||||
|
|
||||||
class QualityProfileItems extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
defaultHeight: 0,
|
|
||||||
editGroupsHeight: 0,
|
|
||||||
editSizesHeight: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMeasure = ({ height }) => {
|
|
||||||
const heightKey = `${this.props.mode}Height`;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
[heightKey]: height
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditGroupsPress = () => {
|
|
||||||
this.props.onChangeMode('editGroups');
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditSizesPress = () => {
|
|
||||||
this.props.onChangeMode('editSizes');
|
|
||||||
};
|
|
||||||
|
|
||||||
onDefaultModePress = () => {
|
|
||||||
this.props.onChangeMode('default');
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
mode,
|
|
||||||
dropQualityIndex,
|
|
||||||
dropPosition,
|
|
||||||
qualityProfileItems,
|
|
||||||
errors,
|
|
||||||
warnings,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isDragging = dropQualityIndex !== null;
|
|
||||||
const isDraggingUp = isDragging && dropPosition === 'above';
|
|
||||||
const isDraggingDown = isDragging && dropPosition === 'below';
|
|
||||||
const height = this.state[`${mode}Height`];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
|
||||||
<FormLabel size={sizes.SMALL}>
|
|
||||||
{translate('Qualities')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<FormInputHelpText
|
|
||||||
text={translate('QualitiesHelpText')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
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={mode === 'editGroups' ? this.onDefaultModePress : this.onEditGroupsPress}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Icon
|
|
||||||
className={styles.editButtonIcon}
|
|
||||||
name={mode === 'editGroups' ? icons.REORDER : icons.GROUP}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
mode === 'editGroups' ? translate('DoneEditingGroups') : translate('EditGroups')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className={styles.editSizesButton}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
onPress={mode === 'editSizes' ? this.onDefaultModePress : this.onEditSizesPress}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Icon
|
|
||||||
className={styles.editButtonIcon}
|
|
||||||
name={mode === 'editSizes' ? icons.REORDER : icons.FILE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
mode === 'editSizes' ? translate('DoneEditingSizes') : translate('EditSizes')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Measure
|
|
||||||
whitelist={['height']}
|
|
||||||
includeMargin={false}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={styles.qualities}
|
|
||||||
style={{ height: `${height}px` }}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
qualityProfileItems.map(({ id, name, allowed, quality, items, minSize, maxSize, preferredSize }, index) => {
|
|
||||||
const identifier = quality ? quality.id : id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QualityProfileItemDragSource
|
|
||||||
key={identifier}
|
|
||||||
mode={mode}
|
|
||||||
groupId={id}
|
|
||||||
qualityId={quality && quality.id}
|
|
||||||
name={quality ? quality.name : name}
|
|
||||||
allowed={allowed}
|
|
||||||
items={items}
|
|
||||||
minSize={minSize}
|
|
||||||
maxSize={maxSize}
|
|
||||||
preferredSize={preferredSize}
|
|
||||||
qualityIndex={`${index + 1}`}
|
|
||||||
isInGroup={false}
|
|
||||||
isDragging={isDragging}
|
|
||||||
isDraggingUp={isDraggingUp}
|
|
||||||
isDraggingDown={isDraggingDown}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}).reverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
<QualityProfileItemDragPreview />
|
|
||||||
</div>
|
|
||||||
</Measure>
|
|
||||||
</div>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityProfileItems.propTypes = {
|
|
||||||
mode: PropTypes.string.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),
|
|
||||||
onChangeMode: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QualityProfileItems.defaultProps = {
|
|
||||||
errors: [],
|
|
||||||
warnings: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QualityProfileItems;
|
|
209
frontend/src/Settings/Profiles/Quality/QualityProfileItems.tsx
Normal file
209
frontend/src/Settings/Profiles/Quality/QualityProfileItems.tsx
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import useMeasure from 'react-use-measure';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import { Failure } from 'typings/pending';
|
||||||
|
import { QualityProfileItems as Items } from 'typings/QualityProfile';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QualityProfileItemDragSource, {
|
||||||
|
QualityProfileItemDragSourceActionProps,
|
||||||
|
} from './QualityProfileItemDragSource';
|
||||||
|
import styles from './QualityProfileItems.css';
|
||||||
|
|
||||||
|
export type EditQualityProfileMode = 'default' | 'editGroups' | 'editSizes';
|
||||||
|
|
||||||
|
interface QualityProfileItemsProps
|
||||||
|
extends QualityProfileItemDragSourceActionProps {
|
||||||
|
mode: EditQualityProfileMode;
|
||||||
|
dragQualityIndex: string | null;
|
||||||
|
dropQualityIndex: string | null;
|
||||||
|
dropPosition: string | null;
|
||||||
|
qualityProfileItems: Items;
|
||||||
|
errors?: Failure[];
|
||||||
|
warnings?: Failure[];
|
||||||
|
onChangeMode: (mode: EditQualityProfileMode) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityProfileItems({
|
||||||
|
mode,
|
||||||
|
dropQualityIndex,
|
||||||
|
dropPosition,
|
||||||
|
qualityProfileItems,
|
||||||
|
errors = [],
|
||||||
|
warnings = [],
|
||||||
|
onChangeMode,
|
||||||
|
...otherProps
|
||||||
|
}: QualityProfileItemsProps) {
|
||||||
|
const [measureRef, { height: measuredHeight }] = useMeasure();
|
||||||
|
const [defaultHeight, setDefaultHeight] = useState(0);
|
||||||
|
const [editGroupsHeight, setEditGroupsHeight] = useState(0);
|
||||||
|
const [editSizesHeight, setEditSizesHeight] = useState(0);
|
||||||
|
|
||||||
|
const isDragging = dropQualityIndex !== null;
|
||||||
|
const isDraggingUp = isDragging && dropPosition === 'above';
|
||||||
|
const isDraggingDown = isDragging && dropPosition === 'below';
|
||||||
|
|
||||||
|
const height = useMemo(() => {
|
||||||
|
if (mode === 'default' && defaultHeight > 0) {
|
||||||
|
return defaultHeight;
|
||||||
|
} else if (mode === 'editGroups' && editGroupsHeight > 0) {
|
||||||
|
return editGroupsHeight;
|
||||||
|
} else if (mode === 'editSizes' && editSizesHeight > 0) {
|
||||||
|
return editSizesHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'auto';
|
||||||
|
}, [mode, defaultHeight, editGroupsHeight, editSizesHeight]);
|
||||||
|
|
||||||
|
const handleEditGroupsPress = useCallback(() => {
|
||||||
|
onChangeMode('editGroups');
|
||||||
|
}, [onChangeMode]);
|
||||||
|
|
||||||
|
const handleEditSizesPress = useCallback(() => {
|
||||||
|
onChangeMode('editSizes');
|
||||||
|
}, [onChangeMode]);
|
||||||
|
|
||||||
|
const handleDefaultModePress = useCallback(() => {
|
||||||
|
onChangeMode('default');
|
||||||
|
}, [onChangeMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === 'default') {
|
||||||
|
setDefaultHeight(measuredHeight);
|
||||||
|
} else if (mode === 'editGroups') {
|
||||||
|
setEditGroupsHeight(measuredHeight);
|
||||||
|
} else if (mode === 'editSizes') {
|
||||||
|
setEditSizesHeight(measuredHeight);
|
||||||
|
}
|
||||||
|
}, [mode, measuredHeight]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>{translate('Qualities')}</FormLabel>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormInputHelpText text={translate('QualitiesHelpText')} />
|
||||||
|
|
||||||
|
{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={
|
||||||
|
mode === 'editGroups'
|
||||||
|
? handleDefaultModePress
|
||||||
|
: handleEditGroupsPress
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Icon
|
||||||
|
className={styles.editButtonIcon}
|
||||||
|
name={mode === 'editGroups' ? icons.REORDER : icons.GROUP}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{mode === 'editGroups'
|
||||||
|
? translate('DoneEditingGroups')
|
||||||
|
: translate('EditGroups')}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={styles.editSizesButton}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
onPress={
|
||||||
|
mode === 'editSizes' ? handleDefaultModePress : handleEditSizesPress
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Icon
|
||||||
|
className={styles.editButtonIcon}
|
||||||
|
name={mode === 'editSizes' ? icons.REORDER : icons.FILE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{mode === 'editSizes'
|
||||||
|
? translate('DoneEditingSizes')
|
||||||
|
: translate('EditSizes')}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={measureRef}
|
||||||
|
className={styles.qualities}
|
||||||
|
style={{ minHeight: `${height}px` }}
|
||||||
|
>
|
||||||
|
{qualityProfileItems
|
||||||
|
.map((item, index) => {
|
||||||
|
if ('quality' in item) {
|
||||||
|
const { quality, allowed, minSize, maxSize, preferredSize } =
|
||||||
|
item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QualityProfileItemDragSource
|
||||||
|
key={item.quality.id}
|
||||||
|
{...otherProps}
|
||||||
|
mode={mode}
|
||||||
|
groupId={undefined}
|
||||||
|
qualityId={quality.id}
|
||||||
|
name={quality.name}
|
||||||
|
allowed={allowed}
|
||||||
|
minSize={minSize}
|
||||||
|
maxSize={maxSize}
|
||||||
|
preferredSize={preferredSize}
|
||||||
|
qualityIndex={`${index + 1}`}
|
||||||
|
isInGroup={false}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, name, allowed, items } = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QualityProfileItemDragSource
|
||||||
|
key={id}
|
||||||
|
{...otherProps}
|
||||||
|
mode={mode}
|
||||||
|
groupId={id}
|
||||||
|
qualityId={undefined}
|
||||||
|
name={name}
|
||||||
|
allowed={allowed}
|
||||||
|
items={items}
|
||||||
|
qualityIndex={`${index + 1}`}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.reverse()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityProfileItems;
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
interface QualityProfileNameProps {
|
||||||
|
qualityProfileId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityProfileName({ qualityProfileId }: QualityProfileNameProps) {
|
||||||
|
const qualityProfile = useSelector(
|
||||||
|
createQualityProfileSelectorForHook(qualityProfileId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return <span>{qualityProfile?.name ?? translate('Unknown')}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityProfileName;
|
|
@ -1,31 +0,0 @@
|
||||||
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);
|
|
|
@ -1,107 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Card from 'Components/Card';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EditQualityProfileModalConnector from './EditQualityProfileModalConnector';
|
|
||||||
import QualityProfile from './QualityProfile';
|
|
||||||
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={translate('QualityProfiles')}>
|
|
||||||
<PageSectionContent
|
|
||||||
errorMessage={translate('QualityProfilesLoadError')}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.qualityProfiles}>
|
|
||||||
{
|
|
||||||
items.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;
|
|
93
frontend/src/Settings/Profiles/Quality/QualityProfiles.tsx
Normal file
93
frontend/src/Settings/Profiles/Quality/QualityProfiles.tsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { QualityProfilesAppState } from 'App/State/SettingsAppState';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
cloneQualityProfile,
|
||||||
|
fetchQualityProfiles,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
|
import QualityProfileModel from 'typings/QualityProfile';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EditQualityProfileModal from './EditQualityProfileModal';
|
||||||
|
import QualityProfile from './QualityProfile';
|
||||||
|
import styles from './QualityProfiles.css';
|
||||||
|
|
||||||
|
function QualityProfiles() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { error, isFetching, isPopulated, isDeleting, items } = useSelector(
|
||||||
|
createSortedSectionSelector<QualityProfileModel, QualityProfilesAppState>(
|
||||||
|
'settings.qualityProfiles',
|
||||||
|
sortByProp('name')
|
||||||
|
)
|
||||||
|
) as QualityProfilesAppState;
|
||||||
|
|
||||||
|
const [isQualityProfileModalOpen, setIsQualityProfileModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const handleEditQualityProfilePress = useCallback(() => {
|
||||||
|
setIsQualityProfileModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEditQualityProfileClosePress = useCallback(() => {
|
||||||
|
setIsQualityProfileModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCloneQualityProfilePress = useCallback(
|
||||||
|
(id: number) => {
|
||||||
|
dispatch(cloneQualityProfile({ id }));
|
||||||
|
setIsQualityProfileModalOpen(true);
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchQualityProfiles());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('QualityProfiles')}>
|
||||||
|
<PageSectionContent
|
||||||
|
errorMessage={translate('QualityProfilesLoadError')}
|
||||||
|
error={error}
|
||||||
|
isFetching={isFetching}
|
||||||
|
isPopulated={isPopulated}
|
||||||
|
>
|
||||||
|
<div className={styles.qualityProfiles}>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<QualityProfile
|
||||||
|
key={item.id}
|
||||||
|
{...item}
|
||||||
|
isDeleting={isDeleting}
|
||||||
|
onCloneQualityProfilePress={handleCloneQualityProfilePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={styles.addQualityProfile}
|
||||||
|
onPress={handleEditQualityProfilePress}
|
||||||
|
>
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon name={icons.ADD} size={45} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditQualityProfileModal
|
||||||
|
isOpen={isQualityProfileModalOpen}
|
||||||
|
onModalClose={handleEditQualityProfileClosePress}
|
||||||
|
/>
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityProfiles;
|
|
@ -1,63 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { cloneQualityProfile, deleteQualityProfile, fetchQualityProfiles } from 'Store/Actions/settingsActions';
|
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import QualityProfiles from './QualityProfiles';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')),
|
|
||||||
(qualityProfiles) => 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);
|
|
|
@ -3,7 +3,7 @@ import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
interface QualityDefinitionLimitsProps {
|
interface QualityDefinitionLimitsProps {
|
||||||
bytes?: number;
|
bytes: number | null;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import AppState from 'App/State/AppState';
|
||||||
import FieldSet from 'Components/FieldSet';
|
import FieldSet from 'Components/FieldSet';
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
|
|
||||||
import {
|
import {
|
||||||
fetchQualityDefinitions,
|
fetchQualityDefinitions,
|
||||||
saveQualityDefinitions,
|
saveQualityDefinitions,
|
||||||
|
@ -16,7 +15,7 @@ import {
|
||||||
SetChildSave,
|
SetChildSave,
|
||||||
} from 'typings/Settings/SettingsState';
|
} from 'typings/Settings/SettingsState';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QualityDefinitionConnector from './QualityDefinitionConnector';
|
import QualityDefinition from './QualityDefinition';
|
||||||
import styles from './QualityDefinitions.css';
|
import styles from './QualityDefinitions.css';
|
||||||
|
|
||||||
function createQualityDefinitionsSelector() {
|
function createQualityDefinitionsSelector() {
|
||||||
|
@ -50,7 +49,6 @@ function QualityDefinitions({
|
||||||
onChildStateChange,
|
onChildStateChange,
|
||||||
}: QualityDefinitionsProps) {
|
}: QualityDefinitionsProps) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const showAdvancedSettings = useShowAdvancedSettings();
|
|
||||||
const { items, isFetching, isPopulated, isSaving, error, hasPendingChanges } =
|
const { items, isFetching, isPopulated, isSaving, error, hasPendingChanges } =
|
||||||
useSelector(createQualityDefinitionsSelector());
|
useSelector(createQualityDefinitionsSelector());
|
||||||
|
|
||||||
|
@ -90,32 +88,13 @@ function QualityDefinitions({
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.quality}>{translate('Quality')}</div>
|
<div className={styles.quality}>{translate('Quality')}</div>
|
||||||
<div className={styles.title}>{translate('Title')}</div>
|
<div className={styles.title}>{translate('Title')}</div>
|
||||||
<div className={styles.sizeLimit}>{translate('SizeLimit')}</div>
|
|
||||||
|
|
||||||
{showAdvancedSettings ? (
|
|
||||||
<div className={styles.megabytesPerMinute}>
|
|
||||||
{translate('MegabytesPerMinute')}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.definitions}>
|
<div className={styles.definitions}>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
return (
|
return <QualityDefinition key={item.id} {...item} />;
|
||||||
<QualityDefinitionConnector
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
advancedSettings={showAdvancedSettings}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.sizeLimitHelpTextContainer}>
|
|
||||||
<div className={styles.sizeLimitHelpText}>
|
|
||||||
{translate('QualityLimitsSeriesRuntimeHelpText')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageSectionContent>
|
</PageSectionContent>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component, Fragment } from 'react';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QualityDefinitions from './Definition/QualityDefinitions';
|
|
||||||
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
|
|
||||||
|
|
||||||
class Quality extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._saveCallback = null;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isSaving: false,
|
|
||||||
hasPendingChanges: false,
|
|
||||||
isConfirmQualityDefinitionResetModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChildMounted = (saveCallback) => {
|
|
||||||
this._saveCallback = saveCallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
onChildStateChange = (payload) => {
|
|
||||||
this.setState(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
onResetQualityDefinitionsPress = () => {
|
|
||||||
this.setState({ isConfirmQualityDefinitionResetModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onCloseResetQualityDefinitionsModal = () => {
|
|
||||||
this.setState({ isConfirmQualityDefinitionResetModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
if (this._saveCallback) {
|
|
||||||
this._saveCallback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isSaving,
|
|
||||||
isResettingQualityDefinitions,
|
|
||||||
hasPendingChanges
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('QualitySettings')}>
|
|
||||||
<SettingsToolbarConnector
|
|
||||||
isSaving={isSaving}
|
|
||||||
hasPendingChanges={hasPendingChanges}
|
|
||||||
additionalButtons={
|
|
||||||
<Fragment>
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('ResetDefinitions')}
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
isSpinning={isResettingQualityDefinitions}
|
|
||||||
onPress={this.onResetQualityDefinitionsPress}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
<QualityDefinitions
|
|
||||||
onChildMounted={this.onChildMounted}
|
|
||||||
onChildStateChange={this.onChildStateChange}
|
|
||||||
/>
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
<ResetQualityDefinitionsModal
|
|
||||||
isOpen={this.state.isConfirmQualityDefinitionResetModalOpen}
|
|
||||||
onModalClose={this.onCloseResetQualityDefinitionsModal}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Quality.propTypes = {
|
|
||||||
isResettingQualityDefinitions: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Quality;
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import Series from 'Series/Series';
|
|
||||||
import ImportList from 'typings/ImportList';
|
|
||||||
import createAllSeriesSelector from './createAllSeriesSelector';
|
|
||||||
|
|
||||||
function createProfileInUseSelector(profileProp: string) {
|
|
||||||
return createSelector(
|
|
||||||
(_: AppState, { id }: { id: number }) => id,
|
|
||||||
createAllSeriesSelector(),
|
|
||||||
(state: AppState) => state.settings.importLists.items,
|
|
||||||
(id, series, lists) => {
|
|
||||||
if (!id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
series.some((s) => s[profileProp as keyof Series] === id) ||
|
|
||||||
lists.some((list) => list[profileProp as keyof ImportList] === id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createProfileInUseSelector;
|
|
|
@ -1,7 +1,7 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
import {
|
import {
|
||||||
|
AppSectionItemSchemaState,
|
||||||
AppSectionProviderState,
|
AppSectionProviderState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
|
@ -11,30 +11,26 @@ import selectSettings, {
|
||||||
} from 'Store/Selectors/selectSettings';
|
} from 'Store/Selectors/selectSettings';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
|
||||||
|
type SchemaState<T> = AppSectionSchemaState<T> | AppSectionItemSchemaState<T>;
|
||||||
|
|
||||||
function selector<
|
function selector<
|
||||||
T extends ModelBaseSetting,
|
T extends ModelBaseSetting,
|
||||||
S extends AppSectionProviderState<T> & AppSectionSchemaState<T>
|
S extends AppSectionProviderState<T> & SchemaState<T>
|
||||||
>(id: number | undefined, section: S) {
|
>(id: number | undefined, section: S) {
|
||||||
if (!id) {
|
if (id) {
|
||||||
const item = _.isArray(section.schema)
|
|
||||||
? section.selectedSchema
|
|
||||||
: section.schema;
|
|
||||||
const settings = selectSettings(
|
|
||||||
Object.assign({ name: '' }, item),
|
|
||||||
section.pendingChanges ?? {},
|
|
||||||
section.saveError
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isSchemaFetching: isFetching,
|
isFetching,
|
||||||
isSchemaPopulated: isPopulated,
|
isPopulated,
|
||||||
schemaError: error,
|
error,
|
||||||
isSaving,
|
isSaving,
|
||||||
saveError,
|
saveError,
|
||||||
isTesting,
|
isTesting,
|
||||||
pendingChanges,
|
pendingChanges,
|
||||||
} = section;
|
} = section;
|
||||||
|
|
||||||
|
const item = section.items.find((i) => i.id === id)!;
|
||||||
|
const settings = selectSettings<T>(item, pendingChanges, saveError);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
|
@ -43,24 +39,31 @@ function selector<
|
||||||
saveError,
|
saveError,
|
||||||
isTesting,
|
isTesting,
|
||||||
...settings,
|
...settings,
|
||||||
pendingChanges,
|
|
||||||
item: settings.settings,
|
item: settings.settings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const item =
|
||||||
|
'selectedSchema' in section
|
||||||
|
? section.selectedSchema
|
||||||
|
: (section.schema as T);
|
||||||
|
|
||||||
|
const settings = selectSettings(
|
||||||
|
Object.assign({ name: '' }, item),
|
||||||
|
section.pendingChanges ?? {},
|
||||||
|
section.saveError
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isFetching,
|
isSchemaFetching: isFetching,
|
||||||
isPopulated,
|
isSchemaPopulated: isPopulated,
|
||||||
error,
|
schemaError: error,
|
||||||
isSaving,
|
isSaving,
|
||||||
saveError,
|
saveError,
|
||||||
isTesting,
|
isTesting,
|
||||||
pendingChanges,
|
pendingChanges,
|
||||||
} = section;
|
} = section;
|
||||||
|
|
||||||
const item = section.items.find((i) => i.id === id)!;
|
|
||||||
const settings = selectSettings<T>(item, pendingChanges, saveError);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
|
@ -69,13 +72,14 @@ function selector<
|
||||||
saveError,
|
saveError,
|
||||||
isTesting,
|
isTesting,
|
||||||
...settings,
|
...settings,
|
||||||
|
pendingChanges,
|
||||||
item: settings.settings,
|
item: settings.settings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createProviderSettingsSelector<
|
export default function createProviderSettingsSelector<
|
||||||
T extends ModelBase,
|
T extends ModelBase,
|
||||||
S extends AppSectionProviderState<T> & AppSectionSchemaState<T>
|
S extends AppSectionProviderState<T> & SchemaState<T>
|
||||||
>(sectionName: string) {
|
>(sectionName: string) {
|
||||||
// @ts-expect-error - This isn't fully typed
|
// @ts-expect-error - This isn't fully typed
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
@ -87,7 +91,7 @@ export default function createProviderSettingsSelector<
|
||||||
|
|
||||||
export function createProviderSettingsSelectorHook<
|
export function createProviderSettingsSelectorHook<
|
||||||
T extends ModelBaseSetting,
|
T extends ModelBaseSetting,
|
||||||
S extends AppSectionProviderState<T> & AppSectionSchemaState<T>
|
S extends AppSectionProviderState<T> & SchemaState<T>
|
||||||
>(sectionName: string, id: number | undefined) {
|
>(sectionName: string, id: number | undefined) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.settings,
|
(state: AppState) => state.settings,
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import createAllSeriesSelector from './createAllSeriesSelector';
|
||||||
|
|
||||||
|
function createQualityProfileInUseSelector(id: number | undefined) {
|
||||||
|
return createSelector(
|
||||||
|
createAllSeriesSelector(),
|
||||||
|
(state: AppState) => state.settings.importLists.items,
|
||||||
|
(series, lists) => {
|
||||||
|
if (!id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
series.some((s) => s.qualityProfileId === id) ||
|
||||||
|
lists.some((list) => list.qualityProfileId === id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createQualityProfileInUseSelector;
|
|
@ -1,13 +1,13 @@
|
||||||
import Quality from 'Quality/Quality';
|
import Quality from 'Quality/Quality';
|
||||||
import { QualityProfileQualityItem } from 'typings/QualityProfile';
|
import { QualityProfileItems } from 'typings/QualityProfile';
|
||||||
|
|
||||||
export default function getQualities(qualities?: QualityProfileQualityItem[]) {
|
export default function getQualities(qualities?: QualityProfileItems) {
|
||||||
if (!qualities) {
|
if (!qualities) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return qualities.reduce<Quality[]>((acc, item) => {
|
return qualities.reduce<Quality[]>((acc, item) => {
|
||||||
if (item.quality) {
|
if ('quality' in item) {
|
||||||
acc.push(item.quality);
|
acc.push(item.quality);
|
||||||
} else {
|
} else {
|
||||||
const groupQualities = item.items.reduce<Quality[]>((acc, i) => {
|
const groupQualities = item.items.reduce<Quality[]>((acc, i) => {
|
||||||
|
|
|
@ -2,18 +2,30 @@ import Quality from 'Quality/Quality';
|
||||||
import { QualityProfileFormatItem } from './CustomFormat';
|
import { QualityProfileFormatItem } from './CustomFormat';
|
||||||
|
|
||||||
export interface QualityProfileQualityItem {
|
export interface QualityProfileQualityItem {
|
||||||
id?: number;
|
quality: Quality;
|
||||||
quality?: Quality;
|
allowed: boolean;
|
||||||
|
minSize: number | null;
|
||||||
|
maxSize: number | null;
|
||||||
|
preferredSize: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QualityProfileGroup {
|
||||||
|
id: number;
|
||||||
items: QualityProfileQualityItem[];
|
items: QualityProfileQualityItem[];
|
||||||
allowed: boolean;
|
allowed: boolean;
|
||||||
name?: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QualityProfileItems = (
|
||||||
|
| QualityProfileQualityItem
|
||||||
|
| QualityProfileGroup
|
||||||
|
)[];
|
||||||
|
|
||||||
interface QualityProfile {
|
interface QualityProfile {
|
||||||
name: string;
|
name: string;
|
||||||
upgradeAllowed: boolean;
|
upgradeAllowed: boolean;
|
||||||
cutoff: number;
|
cutoff: number;
|
||||||
items: QualityProfileQualityItem[];
|
items: QualityProfileItems;
|
||||||
minFormatScore: number;
|
minFormatScore: number;
|
||||||
cutoffFormatScore: number;
|
cutoffFormatScore: number;
|
||||||
minUpgradeFormatScore: number;
|
minUpgradeFormatScore: number;
|
||||||
|
|
|
@ -2,5 +2,5 @@ import Column from 'Components/Table/Column';
|
||||||
|
|
||||||
export interface TableOptionsChangePayload {
|
export interface TableOptionsChangePayload {
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
columns: Column[];
|
columns?: Column[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,15 +49,16 @@
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"qs": "6.13.0",
|
"qs": "6.13.0",
|
||||||
|
"rdndmb-html5-to-touch": "8.1.2",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-addons-shallow-compare": "15.6.3",
|
"react-addons-shallow-compare": "15.6.3",
|
||||||
"react-async-script": "1.2.0",
|
"react-async-script": "1.2.0",
|
||||||
"react-autosuggest": "10.1.0",
|
"react-autosuggest": "10.1.0",
|
||||||
"react-custom-scrollbars-2": "4.5.0",
|
"react-custom-scrollbars-2": "4.5.0",
|
||||||
"react-dnd": "14.0.4",
|
"react-dnd": "16.0.1",
|
||||||
"react-dnd-html5-backend": "14.0.2",
|
"react-dnd-html5-backend": "16.0.1",
|
||||||
"react-dnd-multi-backend": "6.0.2",
|
"react-dnd-multi-backend": "8.1.2",
|
||||||
"react-dnd-touch-backend": "14.1.1",
|
"react-dnd-touch-backend": "16.0.1",
|
||||||
"react-document-title": "2.0.3",
|
"react-document-title": "2.0.3",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-focus-lock": "2.9.4",
|
"react-focus-lock": "2.9.4",
|
||||||
|
|
116
yarn.lock
116
yarn.lock
|
@ -1186,20 +1186,20 @@
|
||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
"@react-dnd/asap@^4.0.0":
|
"@react-dnd/asap@^5.0.1":
|
||||||
version "4.0.1"
|
version "5.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
|
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488"
|
||||||
integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==
|
integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==
|
||||||
|
|
||||||
"@react-dnd/invariant@^2.0.0":
|
"@react-dnd/invariant@^4.0.1":
|
||||||
version "2.0.0"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e"
|
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df"
|
||||||
integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==
|
integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==
|
||||||
|
|
||||||
"@react-dnd/shallowequal@^2.0.0":
|
"@react-dnd/shallowequal@^4.0.1":
|
||||||
version "2.0.0"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
|
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4"
|
||||||
integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
|
integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==
|
||||||
|
|
||||||
"@rtsao/scc@^1.1.0":
|
"@rtsao/scc@^1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
|
@ -2768,19 +2768,19 @@ dir-glob@^3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-type "^4.0.0"
|
path-type "^4.0.0"
|
||||||
|
|
||||||
dnd-core@14.0.1:
|
dnd-core@^16.0.1:
|
||||||
version "14.0.1"
|
version "16.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e"
|
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19"
|
||||||
integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==
|
integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@react-dnd/asap" "^4.0.0"
|
"@react-dnd/asap" "^5.0.1"
|
||||||
"@react-dnd/invariant" "^2.0.0"
|
"@react-dnd/invariant" "^4.0.1"
|
||||||
redux "^4.1.1"
|
redux "^4.2.0"
|
||||||
|
|
||||||
dnd-multi-backend@^6.0.0:
|
dnd-multi-backend@^8.1.2:
|
||||||
version "6.0.0"
|
version "8.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/dnd-multi-backend/-/dnd-multi-backend-6.0.0.tgz#4ed68229a3f6f1fb9e9bc45b4034e8330005280d"
|
resolved "https://registry.yarnpkg.com/dnd-multi-backend/-/dnd-multi-backend-8.1.2.tgz#bf6a6ea9f6a9f5d58cabe12fd927753a753aeb92"
|
||||||
integrity sha512-qfUO4V0IACs24xfE9m9OUnwIzoL+SWzSiFbKVIHE0pFddJeZ93BZOdHS1XEYr8X3HNh+CfnfjezXgOMgjvh74g==
|
integrity sha512-KPDVEsiM+6gNEegqZYTWJQgJxYV4vB91tUrvoKJjaS0wwWqT/jNU0P7xJAwCue/cbasJNvk2dFZH7tC+bjX1Rg==
|
||||||
|
|
||||||
doctrine@^2.1.0:
|
doctrine@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
|
@ -5414,6 +5414,15 @@ raw-body@~1.1.0:
|
||||||
bytes "1"
|
bytes "1"
|
||||||
string_decoder "0.10"
|
string_decoder "0.10"
|
||||||
|
|
||||||
|
rdndmb-html5-to-touch@8.1.2:
|
||||||
|
version "8.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/rdndmb-html5-to-touch/-/rdndmb-html5-to-touch-8.1.2.tgz#ab5379974e66e57624a95a79632248b2d32bc354"
|
||||||
|
integrity sha512-efi3MaXYxWaLMd5xzF1bVvmX8erTMhYHSlaMjQe+tynf4IdtgRYfKLwYg+4Z5eq4k7idrjKHQOIMDE6D8LjnOA==
|
||||||
|
dependencies:
|
||||||
|
dnd-multi-backend "^8.1.2"
|
||||||
|
react-dnd-html5-backend "^16.0.1"
|
||||||
|
react-dnd-touch-backend "^16.0.1"
|
||||||
|
|
||||||
react-addons-shallow-compare@15.6.3:
|
react-addons-shallow-compare@15.6.3:
|
||||||
version "15.6.3"
|
version "15.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.3.tgz#28a94b0dfee71530852c66a69053d59a1baf04cb"
|
resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.3.tgz#28a94b0dfee71530852c66a69053d59a1baf04cb"
|
||||||
|
@ -5456,45 +5465,42 @@ react-custom-scrollbars-2@4.5.0:
|
||||||
prop-types "^15.5.10"
|
prop-types "^15.5.10"
|
||||||
raf "^3.1.0"
|
raf "^3.1.0"
|
||||||
|
|
||||||
react-dnd-html5-backend@14.0.2:
|
react-dnd-html5-backend@16.0.1, react-dnd-html5-backend@^16.0.1:
|
||||||
version "14.0.2"
|
version "16.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz#25019388f6abdeeda3a6fea835dff155abb2085c"
|
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6"
|
||||||
integrity sha512-QgN6rYrOm4UUj6tIvN8ovImu6uP48xBXF2rzVsp6tvj6d5XQ7OjHI4SJ/ZgGobOneRAU3WCX4f8DGCYx0tuhlw==
|
integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==
|
||||||
dependencies:
|
dependencies:
|
||||||
dnd-core "14.0.1"
|
dnd-core "^16.0.1"
|
||||||
|
|
||||||
react-dnd-multi-backend@6.0.2:
|
react-dnd-multi-backend@8.1.2:
|
||||||
version "6.0.2"
|
version "8.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dnd-multi-backend/-/react-dnd-multi-backend-6.0.2.tgz#485878014dfbac46fcc898961871be6e5277c3f2"
|
resolved "https://registry.yarnpkg.com/react-dnd-multi-backend/-/react-dnd-multi-backend-8.1.2.tgz#2be039e33d98d063d1f3d89d1a8ce1f487b900aa"
|
||||||
integrity sha512-SwpqRv0HkJYu244FbHf9NbvGzGy14Ir9wIAhm909uvOVaHgsOq6I1THMSWSgpwUI31J3Bo5uS19tuvGpVPjzZw==
|
integrity sha512-Ecj+gwr5B7zRiWqkDU5sUvUmufcu97WnsZFHnqHrWFJhTXAXQnhrperHLFktNP2CnQYtAgbucodr1if0MWpEaA==
|
||||||
dependencies:
|
dependencies:
|
||||||
dnd-multi-backend "^6.0.0"
|
dnd-multi-backend "^8.1.2"
|
||||||
prop-types "^15.7.2"
|
react-dnd-preview "^8.1.2"
|
||||||
react-dnd-preview "^6.0.2"
|
|
||||||
|
|
||||||
react-dnd-preview@^6.0.2:
|
react-dnd-preview@^8.1.2:
|
||||||
version "6.0.2"
|
version "8.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dnd-preview/-/react-dnd-preview-6.0.2.tgz#dd34931c270853c80438e1275e6c9e77174f8afe"
|
resolved "https://registry.yarnpkg.com/react-dnd-preview/-/react-dnd-preview-8.1.2.tgz#a679f62a7bdec30b167ed5a10c7f7ed58095b167"
|
||||||
integrity sha512-F2+uK4Be+q+7mZfNh9kaZols7wp1hX6G7UBTVaTpDsBpMhjFvY7/v7odxYSerSFBShh23MJl33a4XOVRFj1zoQ==
|
integrity sha512-j5M1NcQBItOCYXONRbCNs6MzW7u4KygeOGZlztNNguTs1/f2d7q1fRnQjFLjCpgeg5Gy/JrTFrbRThZglJP5dg==
|
||||||
dependencies:
|
|
||||||
prop-types "^15.7.2"
|
|
||||||
|
|
||||||
react-dnd-touch-backend@14.1.1:
|
react-dnd-touch-backend@16.0.1, react-dnd-touch-backend@^16.0.1:
|
||||||
version "14.1.1"
|
version "16.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-dnd-touch-backend/-/react-dnd-touch-backend-14.1.1.tgz#d8875ef1cf8dcbf1741a4e03dd5b147c4fbda5e4"
|
resolved "https://registry.yarnpkg.com/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz#e73f8169e2b9fac0f687970f875cac0a4d02d6e2"
|
||||||
integrity sha512-ITmfzn3fJrkUBiVLO6aJZcnu7T8C+GfwZitFryGsXKn5wYcUv+oQBeh9FYcMychmVbDdeUCfvEtTk9O+DKmAaw==
|
integrity sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@react-dnd/invariant" "^2.0.0"
|
"@react-dnd/invariant" "^4.0.1"
|
||||||
dnd-core "14.0.1"
|
dnd-core "^16.0.1"
|
||||||
|
|
||||||
react-dnd@14.0.4:
|
react-dnd@16.0.1:
|
||||||
version "14.0.4"
|
version "16.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.4.tgz#ffb4ea0e2a3a5532f9c6294d565742008a52b8b0"
|
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37"
|
||||||
integrity sha512-AFJJXzUIWp5WAhgvI85ESkDCawM0lhoVvfo/lrseLXwFdH3kEO3v8I2C81QPqBW2UEyJBIPStOhPMGYGFtq/bg==
|
integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@react-dnd/invariant" "^2.0.0"
|
"@react-dnd/invariant" "^4.0.1"
|
||||||
"@react-dnd/shallowequal" "^2.0.0"
|
"@react-dnd/shallowequal" "^4.0.1"
|
||||||
dnd-core "14.0.1"
|
dnd-core "^16.0.1"
|
||||||
fast-deep-equal "^3.1.3"
|
fast-deep-equal "^3.1.3"
|
||||||
hoist-non-react-statics "^3.3.2"
|
hoist-non-react-statics "^3.3.2"
|
||||||
|
|
||||||
|
@ -5787,7 +5793,7 @@ redux-thunk@2.4.2:
|
||||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"
|
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"
|
||||||
integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
|
integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
|
||||||
|
|
||||||
redux@4.2.1, redux@^4.0.0, redux@^4.1.1:
|
redux@4.2.1, redux@^4.0.0, redux@^4.2.0:
|
||||||
version "4.2.1"
|
version "4.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
|
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
|
||||||
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
|
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue