-
- {
- values.map((v, index) => {
- return (
-
- {v.value}
-
- );
- })
- }
+ renderTarget={
+ (ref) => {
+ this._buttonRef = ref;
+
+ return (
+
+
+
+
+ {selectedOption ? selectedOption.value : null}
+
+
+
+
+
+
+
+
+ );
+ }
+ }
+ renderElement={
+ (ref) => {
+ this._optionsRef = ref;
+
+ if (!isOpen || isMobile) {
+ return;
+ }
+
+ return (
+
+
+ {
+ values.map((v, index) => {
+ return (
+
+ {v.value}
+
+ );
+ })
+ }
+
-
+ );
+ }
}
-
+ />
{
isMobile &&
diff --git a/frontend/src/Components/Form/FormInputButton.css b/frontend/src/Components/Form/FormInputButton.css
index 27a1923be..da4888f09 100644
--- a/frontend/src/Components/Form/FormInputButton.css
+++ b/frontend/src/Components/Form/FormInputButton.css
@@ -1,5 +1,5 @@
.button {
- composes: button from 'Components/Link/Button.css';
+ composes: button from '~Components/Link/Button.css';
border-left: none;
border-top-left-radius: 0;
diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js
index 96af58af2..c90ba3da8 100644
--- a/frontend/src/Components/Form/FormInputGroup.js
+++ b/frontend/src/Components/Form/FormInputGroup.js
@@ -6,6 +6,7 @@ import AutoCompleteInput from './AutoCompleteInput';
import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
+import KeyValueListInput from './KeyValueListInput';
import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput';
@@ -34,6 +35,9 @@ function getComponent(type) {
case inputTypes.DEVICE:
return DeviceInputConnector;
+ case inputTypes.KEY_VALUE_LIST:
+ return KeyValueListInput;
+
case inputTypes.NUMBER:
return NumberInput;
diff --git a/frontend/src/Components/Form/FormInputHelpText.css b/frontend/src/Components/Form/FormInputHelpText.css
index c760d957c..7fd957233 100644
--- a/frontend/src/Components/Form/FormInputHelpText.css
+++ b/frontend/src/Components/Form/FormInputHelpText.css
@@ -33,7 +33,7 @@
}
.link {
- composes: link from 'Components/Link/Link.css';
+ composes: link from '~Components/Link/Link.css';
margin-left: 5px;
}
diff --git a/frontend/src/Components/Form/KeyValueListInput.css b/frontend/src/Components/Form/KeyValueListInput.css
new file mode 100644
index 000000000..8bf23610b
--- /dev/null
+++ b/frontend/src/Components/Form/KeyValueListInput.css
@@ -0,0 +1,21 @@
+.inputContainer {
+ composes: input from '~Components/Form/Input.css';
+
+ position: relative;
+ min-height: 35px;
+ height: auto;
+
+ &.isFocused {
+ outline: 0;
+ border-color: $inputFocusBorderColor;
+ box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
+ }
+}
+
+.hasError {
+ composes: hasError from '~Components/Form/Input.css';
+}
+
+.hasWarning {
+ composes: hasWarning from '~Components/Form/Input.css';
+}
diff --git a/frontend/src/Components/Form/KeyValueListInput.js b/frontend/src/Components/Form/KeyValueListInput.js
new file mode 100644
index 000000000..a52c76f70
--- /dev/null
+++ b/frontend/src/Components/Form/KeyValueListInput.js
@@ -0,0 +1,152 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import classNames from 'classnames';
+import KeyValueListInputItem from './KeyValueListInputItem';
+import styles from './KeyValueListInput.css';
+
+class KeyValueListInput extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isFocused: false
+ };
+ }
+
+ //
+ // Listeners
+
+ onItemChange = (index, itemValue) => {
+ const {
+ name,
+ value,
+ onChange
+ } = this.props;
+
+ const newValue = [...value];
+
+ if (index == null) {
+ newValue.push(itemValue);
+ } else {
+ newValue.splice(index, 1, itemValue);
+ }
+
+ onChange({
+ name,
+ value: newValue
+ });
+ }
+
+ onRemoveItem = (index) => {
+ const {
+ name,
+ value,
+ onChange
+ } = this.props;
+
+ const newValue = [...value];
+ newValue.splice(index, 1);
+
+ onChange({
+ name,
+ value: newValue
+ });
+ }
+
+ onFocus = () => {
+ this.setState({
+ isFocused: true
+ });
+ }
+
+ onBlur = () => {
+ this.setState({
+ isFocused: false
+ });
+
+ const {
+ name,
+ value,
+ onChange
+ } = this.props;
+
+ const newValue = value.reduce((acc, v) => {
+ if (v.key || v.value) {
+ acc.push(v);
+ }
+
+ return acc;
+ }, []);
+
+ if (newValue.length !== value.length) {
+ onChange({
+ name,
+ value: newValue
+ });
+ }
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ className,
+ value,
+ keyPlaceholder,
+ valuePlaceholder
+ } = this.props;
+
+ const { isFocused } = this.state;
+
+ return (
+
+ {
+ [...value, { key: '', value: '' }].map((v, index) => {
+ return (
+
+ );
+ })
+ }
+
+ );
+ }
+}
+
+KeyValueListInput.propTypes = {
+ className: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ value: PropTypes.arrayOf(PropTypes.object).isRequired,
+ hasError: PropTypes.bool,
+ hasWarning: PropTypes.bool,
+ keyPlaceholder: PropTypes.string,
+ valuePlaceholder: PropTypes.string,
+ onChange: PropTypes.func.isRequired
+};
+
+KeyValueListInput.defaultProps = {
+ className: styles.inputContainer,
+ value: []
+};
+
+export default KeyValueListInput;
diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css b/frontend/src/Components/Form/KeyValueListInputItem.css
new file mode 100644
index 000000000..f77ea3470
--- /dev/null
+++ b/frontend/src/Components/Form/KeyValueListInputItem.css
@@ -0,0 +1,14 @@
+.itemContainer {
+ display: flex;
+ margin-bottom: 3px;
+ border-bottom: 1px solid $inputBorderColor;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.keyInput,
+.valueInput {
+ border: none;
+}
diff --git a/frontend/src/Components/Form/KeyValueListInputItem.js b/frontend/src/Components/Form/KeyValueListInputItem.js
new file mode 100644
index 000000000..4e465f3a9
--- /dev/null
+++ b/frontend/src/Components/Form/KeyValueListInputItem.js
@@ -0,0 +1,117 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { icons } from 'Helpers/Props';
+import IconButton from 'Components/Link/IconButton';
+import TextInput from './TextInput';
+import styles from './KeyValueListInputItem.css';
+
+class KeyValueListInputItem extends Component {
+
+ //
+ // Listeners
+
+ onKeyChange = ({ value: keyValue }) => {
+ const {
+ index,
+ value,
+ onChange
+ } = this.props;
+
+ onChange(index, { key: keyValue, value });
+ }
+
+ onValueChange = ({ value }) => {
+ // TODO: Validate here or validate at a lower level component
+
+ const {
+ index,
+ keyValue,
+ onChange
+ } = this.props;
+
+ onChange(index, { key: keyValue, value });
+ }
+
+ onRemovePress = () => {
+ const {
+ index,
+ onRemove
+ } = this.props;
+
+ onRemove(index);
+ }
+
+ onFocus = () => {
+ this.props.onFocus();
+ }
+
+ onBlur = () => {
+ this.props.onBlur();
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ keyValue,
+ value,
+ keyPlaceholder,
+ valuePlaceholder,
+ isNew
+ } = this.props;
+
+ return (
+
+
+
+
+
+ {
+ !isNew &&
+
+ }
+
+ );
+ }
+}
+
+KeyValueListInputItem.propTypes = {
+ index: PropTypes.number,
+ keyValue: PropTypes.string.isRequired,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+ keyPlaceholder: PropTypes.string.isRequired,
+ valuePlaceholder: PropTypes.string.isRequired,
+ isNew: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onRemove: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ onBlur: PropTypes.func.isRequired
+};
+
+KeyValueListInputItem.defaultProps = {
+ keyPlaceholder: 'Key',
+ valuePlaceholder: 'Value'
+};
+
+export default KeyValueListInputItem;
diff --git a/frontend/src/Components/Form/PasswordInput.css b/frontend/src/Components/Form/PasswordInput.css
index fca96bea9..6cb162784 100644
--- a/frontend/src/Components/Form/PasswordInput.css
+++ b/frontend/src/Components/Form/PasswordInput.css
@@ -1,5 +1,5 @@
.input {
- composes: input from 'Components/Form/TextInput.css';
+ composes: input from '~Components/Form/TextInput.css';
font-family: $passwordFamily;
}
diff --git a/frontend/src/Components/Form/PathInput.css b/frontend/src/Components/Form/PathInput.css
index ce9fd8ebe..94d1b1c62 100644
--- a/frontend/src/Components/Form/PathInput.css
+++ b/frontend/src/Components/Form/PathInput.css
@@ -1,17 +1,17 @@
.path {
- composes: input from 'Components/Form/Input.css';
+ composes: input from '~Components/Form/Input.css';
}
.hasError {
- composes: hasError from 'Components/Form/Input.css';
+ composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
- composes: hasWarning from 'Components/Form/Input.css';
+ composes: hasWarning from '~Components/Form/Input.css';
}
.hasFileBrowser {
- composes: hasButton from 'Components/Form/Input.css';
+ composes: hasButton from '~Components/Form/Input.css';
}
.pathInputWrapper {
@@ -62,7 +62,7 @@
}
.fileBrowserButton {
- composes: button from './FormInputButton.css';
+ composes: button from '~./FormInputButton.css';
height: 35px;
}
diff --git a/frontend/src/Components/Form/PathInput.js b/frontend/src/Components/Form/PathInput.js
index b62ba0555..5451844cf 100644
--- a/frontend/src/Components/Form/PathInput.js
+++ b/frontend/src/Components/Form/PathInput.js
@@ -111,6 +111,7 @@ class PathInput extends Component {
value,
placeholder,
paths,
+ includeFiles,
hasError,
hasWarning,
hasFileBrowser,
@@ -171,6 +172,7 @@ class PathInput extends Component {
isOpen={this.state.isFileBrowserModalOpen}
name={name}
value={value}
+ includeFiles={includeFiles}
onChange={onChange}
onModalClose={this.onFileBrowserModalClose}
/>
@@ -188,6 +190,7 @@ PathInput.propTypes = {
value: PropTypes.string,
placeholder: PropTypes.string,
paths: PropTypes.array.isRequired,
+ includeFiles: PropTypes.bool.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
hasFileBrowser: PropTypes.bool,
diff --git a/frontend/src/Components/Form/PathInputConnector.js b/frontend/src/Components/Form/PathInputConnector.js
index 4916daec8..38ea37065 100644
--- a/frontend/src/Components/Form/PathInputConnector.js
+++ b/frontend/src/Components/Form/PathInputConnector.js
@@ -28,8 +28,8 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
- fetchPaths,
- clearPaths
+ dispatchFetchPaths: fetchPaths,
+ dispatchClearPaths: clearPaths
};
class PathInputConnector extends Component {
@@ -38,11 +38,19 @@ class PathInputConnector extends Component {
// Listeners
onFetchPaths = (path) => {
- this.props.fetchPaths({ path });
+ const {
+ includeFiles,
+ dispatchFetchPaths
+ } = this.props;
+
+ dispatchFetchPaths({
+ path,
+ includeFiles
+ });
}
onClearPaths = () => {
- this.props.clearPaths();
+ this.props.dispatchClearPaths();
}
//
@@ -60,8 +68,13 @@ class PathInputConnector extends Component {
}
PathInputConnector.propTypes = {
- fetchPaths: PropTypes.func.isRequired,
- clearPaths: PropTypes.func.isRequired
+ includeFiles: PropTypes.bool.isRequired,
+ dispatchFetchPaths: PropTypes.func.isRequired,
+ dispatchClearPaths: PropTypes.func.isRequired
+};
+
+PathInputConnector.defaultProps = {
+ includeFiles: false
};
export default connect(createMapStateToProps, mapDispatchToProps)(PathInputConnector);
diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js
index 98922dae4..84268806f 100644
--- a/frontend/src/Components/Form/ProviderFieldFormGroup.js
+++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js
@@ -20,6 +20,8 @@ function getType(type) {
return inputTypes.NUMBER;
case 'path':
return inputTypes.PATH;
+ case 'filepath':
+ return inputTypes.PATH;
case 'select':
return inputTypes.SELECT;
case 'tag':
@@ -84,7 +86,7 @@ function ProviderFieldFormGroup(props) {
errors={errors}
warnings={warnings}
pending={pending}
- hasFileBrowser={false}
+ includeFiles={type === 'filepath' ? true : undefined}
onChange={onChange}
{...otherProps}
/>
diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css
index 0a8fa6ffe..6b0cf9e4f 100644
--- a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css
+++ b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css
@@ -1,5 +1,5 @@
.selectedValue {
- composes: selectedValue from './EnhancedSelectInputSelectedValue.css';
+ composes: selectedValue from '~./EnhancedSelectInputSelectedValue.css';
display: flex;
align-items: center;
diff --git a/frontend/src/Components/Form/SelectInput.css b/frontend/src/Components/Form/SelectInput.css
index 5f1c10e83..aa1dfc79b 100644
--- a/frontend/src/Components/Form/SelectInput.css
+++ b/frontend/src/Components/Form/SelectInput.css
@@ -1,15 +1,15 @@
.select {
- composes: input from 'Components/Form/Input.css';
+ composes: input from '~Components/Form/Input.css';
padding: 0 11px;
}
.hasError {
- composes: hasError from 'Components/Form/Input.css';
+ composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
- composes: hasWarning from 'Components/Form/Input.css';
+ composes: hasWarning from '~Components/Form/Input.css';
}
.isDisabled {
diff --git a/frontend/src/Components/Form/TagInput.css b/frontend/src/Components/Form/TagInput.css
index e22109368..5cf0bca8a 100644
--- a/frontend/src/Components/Form/TagInput.css
+++ b/frontend/src/Components/Form/TagInput.css
@@ -1,5 +1,5 @@
.inputContainer {
- composes: input from 'Components/Form/Input.css';
+ composes: input from '~Components/Form/Input.css';
position: relative;
padding: 0;
@@ -14,11 +14,11 @@
}
.hasError {
- composes: hasError from 'Components/Form/Input.css';
+ composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
- composes: hasWarning from 'Components/Form/Input.css';
+ composes: hasWarning from '~Components/Form/Input.css';
}
.tags {
diff --git a/frontend/src/Components/Form/TagInput.js b/frontend/src/Components/Form/TagInput.js
index 81682f842..fa7ec9dc6 100644
--- a/frontend/src/Components/Form/TagInput.js
+++ b/frontend/src/Components/Form/TagInput.js
@@ -4,6 +4,7 @@ import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import classNames from 'classnames';
import { kinds } from 'Helpers/Props';
+import tagShape from 'Helpers/Props/Shapes/tagShape';
import TagInputInput from './TagInputInput';
import TagInputTag from './TagInputTag';
import styles from './TagInput.css';
@@ -266,11 +267,6 @@ class TagInput extends Component {
}
}
-export const tagShape = {
- id: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]).isRequired,
- name: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired
-};
-
TagInput.propTypes = {
className: PropTypes.string.isRequired,
inputClassName: PropTypes.string.isRequired,
diff --git a/frontend/src/Components/Form/TagInputInput.js b/frontend/src/Components/Form/TagInputInput.js
index 8bd075774..6d5dff2f8 100644
--- a/frontend/src/Components/Form/TagInputInput.js
+++ b/frontend/src/Components/Form/TagInputInput.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
-import { tagShape } from './TagInput';
+import tagShape from 'Helpers/Props/Shapes/tagShape';
import styles from './TagInputInput.css';
class TagInputInput extends Component {
diff --git a/frontend/src/Components/Form/TagInputTag.js b/frontend/src/Components/Form/TagInputTag.js
index ff1e0e2db..8d650702b 100644
--- a/frontend/src/Components/Form/TagInputTag.js
+++ b/frontend/src/Components/Form/TagInputTag.js
@@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
+import tagShape from 'Helpers/Props/Shapes/tagShape';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
-import { tagShape } from './TagInput';
class TagInputTag extends Component {
diff --git a/frontend/src/Components/Form/TextInput.css b/frontend/src/Components/Form/TextInput.css
index 7fb9f68cc..80503704d 100644
--- a/frontend/src/Components/Form/TextInput.css
+++ b/frontend/src/Components/Form/TextInput.css
@@ -1,5 +1,5 @@
.input {
- composes: input from 'Components/Form/Input.css';
+ composes: input from '~Components/Form/Input.css';
}
.readOnly {
@@ -7,13 +7,13 @@
}
.hasError {
- composes: hasError from 'Components/Form/Input.css';
+ composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
- composes: hasWarning from 'Components/Form/Input.css';
+ composes: hasWarning from '~Components/Form/Input.css';
}
.hasButton {
- composes: hasButton from 'Components/Form/Input.css';
+ composes: hasButton from '~Components/Form/Input.css';
}
diff --git a/frontend/src/Components/Icon.js b/frontend/src/Components/Icon.js
index d1005c6e8..d7748d2e7 100644
--- a/frontend/src/Components/Icon.js
+++ b/frontend/src/Components/Icon.js
@@ -1,11 +1,15 @@
import PropTypes from 'prop-types';
-import React from 'react';
+import React, { PureComponent } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { kinds } from 'Helpers/Props';
import classNames from 'classnames';
import styles from './Icon.css';
-class Icon extends React.PureComponent {
+class Icon extends PureComponent {
+
+ //
+ // Render
+
render() {
const {
containerClassName,
diff --git a/frontend/src/Components/InfoLabel.css b/frontend/src/Components/InfoLabel.css
new file mode 100644
index 000000000..c75044b66
--- /dev/null
+++ b/frontend/src/Components/InfoLabel.css
@@ -0,0 +1,40 @@
+.label {
+ display: inline-block;
+ margin: 2px;
+ color: $white;
+ /** text-align: center; **/
+ white-space: nowrap;
+ line-height: 1;
+ cursor: default;
+}
+
+.title {
+ margin-bottom: 2px;
+ color: $helpTextColor;
+ font-size: 10px;
+}
+
+/** Kinds **/
+
+/** Sizes **/
+
+.small {
+ padding: 1px 3px;
+ font-size: 11px;
+}
+
+.medium {
+ padding: 2px 5px;
+ font-size: 12px;
+}
+
+.large {
+ padding: 3px 7px;
+ font-size: 14px;
+}
+
+/** Outline **/
+
+.outline {
+ background-color: $white;
+}
diff --git a/frontend/src/Components/InfoLabel.js b/frontend/src/Components/InfoLabel.js
new file mode 100644
index 000000000..0ded28d84
--- /dev/null
+++ b/frontend/src/Components/InfoLabel.js
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import classNames from 'classnames';
+import { kinds, sizes } from 'Helpers/Props';
+import styles from './InfoLabel.css';
+
+function InfoLabel(props) {
+ const {
+ className,
+ title,
+ kind,
+ size,
+ outline,
+ children,
+ ...otherProps
+ } = props;
+
+ return (
+
+
+ {title}
+
+
+ {children}
+
+
+ );
+}
+
+InfoLabel.propTypes = {
+ className: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ kind: PropTypes.oneOf(kinds.all).isRequired,
+ size: PropTypes.oneOf(sizes.all).isRequired,
+ outline: PropTypes.bool.isRequired,
+ children: PropTypes.node.isRequired
+};
+
+InfoLabel.defaultProps = {
+ className: styles.label,
+ kind: kinds.DEFAULT,
+ size: sizes.SMALL,
+ outline: false
+};
+
+export default InfoLabel;
diff --git a/frontend/src/Components/Link/Button.css b/frontend/src/Components/Link/Button.css
index 8913c4ab8..d5b7e8200 100644
--- a/frontend/src/Components/Link/Button.css
+++ b/frontend/src/Components/Link/Button.css
@@ -1,5 +1,5 @@
.button {
- composes: link from './Link.css';
+ composes: link from '~./Link.css';
overflow: hidden;
border: 1px solid;
diff --git a/frontend/src/Components/Link/ClipboardButton.css b/frontend/src/Components/Link/ClipboardButton.css
index 09ed883cb..438489155 100644
--- a/frontend/src/Components/Link/ClipboardButton.css
+++ b/frontend/src/Components/Link/ClipboardButton.css
@@ -1,5 +1,5 @@
.button {
- composes: button from 'Components/Form/FormInputButton.css';
+ composes: button from '~Components/Form/FormInputButton.css';
position: relative;
}
diff --git a/frontend/src/Components/Link/IconButton.css b/frontend/src/Components/Link/IconButton.css
index 2c85173a1..2061243ee 100644
--- a/frontend/src/Components/Link/IconButton.css
+++ b/frontend/src/Components/Link/IconButton.css
@@ -1,5 +1,5 @@
.button {
- composes: link from 'Components/Link/Link.css';
+ composes: link from '~Components/Link/Link.css';
display: inline-block;
margin: 0 2px;
diff --git a/frontend/src/Components/Link/SpinnerButton.css b/frontend/src/Components/Link/SpinnerButton.css
index cfccd0f06..2a2044c25 100644
--- a/frontend/src/Components/Link/SpinnerButton.css
+++ b/frontend/src/Components/Link/SpinnerButton.css
@@ -1,5 +1,5 @@
.button {
- composes: button from 'Components/Link/Button.css';
+ composes: button from '~Components/Link/Button.css';
position: relative;
}
diff --git a/frontend/src/Components/Link/SpinnerErrorButton.css b/frontend/src/Components/Link/SpinnerErrorButton.css
index 5f4e68545..1671053f1 100644
--- a/frontend/src/Components/Link/SpinnerErrorButton.css
+++ b/frontend/src/Components/Link/SpinnerErrorButton.css
@@ -1,5 +1,5 @@
.iconContainer {
- composes: spinnerContainer from 'Components/Link/SpinnerButton.css';
+ composes: spinnerContainer from '~Components/Link/SpinnerButton.css';
}
.icon {
@@ -7,7 +7,7 @@
}
.label {
- composes: label from 'Components/Link/SpinnerButton.css';
+ composes: label from '~Components/Link/SpinnerButton.css';
}
.showIcon {
diff --git a/frontend/src/Components/Loading/LoadingMessage.js b/frontend/src/Components/Loading/LoadingMessage.js
index db6cb2b56..d3649b41a 100644
--- a/frontend/src/Components/Loading/LoadingMessage.js
+++ b/frontend/src/Components/Loading/LoadingMessage.js
@@ -6,9 +6,13 @@ const messages = [
// TODO Add some messages here
];
+let message = null;
+
function LoadingMessage() {
- const index = Math.floor(Math.random() * messages.length);
- const message = messages[index];
+ if (!message) {
+ const index = Math.floor(Math.random() * messages.length);
+ message = messages[index];
+ }
return (
diff --git a/frontend/src/Components/Menu/FilterMenu.css b/frontend/src/Components/Menu/FilterMenu.css
index 34991aed9..881dbe26c 100644
--- a/frontend/src/Components/Menu/FilterMenu.css
+++ b/frontend/src/Components/Menu/FilterMenu.css
@@ -1,5 +1,5 @@
.filterMenu {
- composes: menu from './Menu.css';
+ composes: menu from '~./Menu.css';
}
@media only screen and (max-width: $breakpointSmall) {
diff --git a/frontend/src/Components/Menu/Menu.js b/frontend/src/Components/Menu/Menu.js
index da778bb7a..946b7e0ec 100644
--- a/frontend/src/Components/Menu/Menu.js
+++ b/frontend/src/Components/Menu/Menu.js
@@ -38,6 +38,9 @@ class Menu extends Component {
constructor(props, context) {
super(props, context);
+ this._menuRef = {};
+ this._menuContentRef = {};
+
this.state = {
isMenuOpen: false,
maxHeight: 0
@@ -60,7 +63,7 @@ class Menu extends Component {
return;
}
- const menu = ReactDOM.findDOMNode(this.refs.menu);
+ const menu = ReactDOM.findDOMNode(this._menuRef.current);
if (!menu) {
return;
@@ -73,9 +76,13 @@ class Menu extends Component {
}
setMaxHeight() {
- this.setState({
- maxHeight: this.getMaxHeight()
- });
+ const maxHeight = this.getMaxHeight();
+
+ if (maxHeight !== this.state.maxHeight) {
+ this.setState({
+ maxHeight
+ });
+ }
}
_addListener() {
@@ -99,10 +106,10 @@ class Menu extends Component {
// Listeners
onWindowClick = (event) => {
- const menu = ReactDOM.findDOMNode(this.refs.menu);
- const menuContent = ReactDOM.findDOMNode(this.refs.menuContent);
+ const menu = ReactDOM.findDOMNode(this._menuRef.current);
+ const menuContent = ReactDOM.findDOMNode(this._menuContentRef.current);
- if (!menu) {
+ if (!menu || !menuContent) {
return;
}
@@ -116,7 +123,17 @@ class Menu extends Component {
this.setMaxHeight();
}
- onWindowScroll = () => {
+ onWindowScroll = (event) => {
+ if (!this._menuContentRef.current) {
+ return;
+ }
+
+ const menuContent = ReactDOM.findDOMNode(this._menuContentRef.current);
+
+ if (menuContent && menuContent.contains(event.target)) {
+ return;
+ }
+
this.setMaxHeight();
}
@@ -158,35 +175,46 @@ class Menu extends Component {
}
);
- const content = React.cloneElement(
- childrenArray[1],
- {
- ref: 'menuContent',
- alignMenu,
- maxHeight,
- isOpen: isMenuOpen
- }
- );
-
return (
-
- {button}
-
-
- {
- isMenuOpen &&
- content
+ renderTarget={
+ (ref) => {
+ this._menuRef = ref;
+
+ return (
+
+ {button}
+
+ );
+ }
+ }
+ renderElement={
+ (ref) => {
+ this._menuContentRef = ref;
+
+ if (!isMenuOpen) {
+ return null;
+ }
+
+ return React.cloneElement(
+ childrenArray[1],
+ {
+ ref,
+ alignMenu,
+ maxHeight,
+ isOpen: isMenuOpen
+ }
+ );
+ }
}
-
+ />
);
}
}
diff --git a/frontend/src/Components/Menu/PageMenuButton.css b/frontend/src/Components/Menu/PageMenuButton.css
index e6954f600..d979a1708 100644
--- a/frontend/src/Components/Menu/PageMenuButton.css
+++ b/frontend/src/Components/Menu/PageMenuButton.css
@@ -1,5 +1,5 @@
.menuButton {
- composes: menuButton from './MenuButton.css';
+ composes: menuButton from '~./MenuButton.css';
&:hover {
color: #666;
diff --git a/frontend/src/Components/Menu/ToolbarMenuButton.css b/frontend/src/Components/Menu/ToolbarMenuButton.css
index c8a905e17..71e966c71 100644
--- a/frontend/src/Components/Menu/ToolbarMenuButton.css
+++ b/frontend/src/Components/Menu/ToolbarMenuButton.css
@@ -1,11 +1,16 @@
.menuButton {
- composes: menuButton from './MenuButton.css';
+ composes: menuButton from '~./MenuButton.css';
+ padding-top: 4px;
width: $toolbarButtonWidth;
height: $toolbarHeight;
text-align: center;
}
+.labelContainer {
+ composes: labelContainer from '~Components/Page/Toolbar/PageToolbarButton.css';
+}
+
.label {
- composes: label from 'Components/Page/Toolbar/PageToolbarButton.css';
+ composes: label from '~Components/Page/Toolbar/PageToolbarButton.css';
}
diff --git a/frontend/src/Components/Menu/ToolbarMenuButton.js b/frontend/src/Components/Menu/ToolbarMenuButton.js
index b80d6eaa3..fe06793f6 100644
--- a/frontend/src/Components/Menu/ToolbarMenuButton.js
+++ b/frontend/src/Components/Menu/ToolbarMenuButton.js
@@ -22,8 +22,10 @@ function ToolbarMenuButton(props) {
size={21}
/>
-
diff --git a/frontend/src/Components/Modal/ModalError.css b/frontend/src/Components/Modal/ModalError.css
index 54dbdbc63..1556240c6 100644
--- a/frontend/src/Components/Modal/ModalError.css
+++ b/frontend/src/Components/Modal/ModalError.css
@@ -1,5 +1,5 @@
.message {
- composes: message from 'Components/Error/ErrorBoundaryError.css';
+ composes: message from '~Components/Error/ErrorBoundaryError.css';
margin: 0;
margin-bottom: 30px;
@@ -8,7 +8,7 @@
}
.details {
- composes: details from 'Components/Error/ErrorBoundaryError.css';
+ composes: details from '~Components/Error/ErrorBoundaryError.css';
margin: 0;
margin-top: 20px;
diff --git a/frontend/src/Components/MonitorToggleButton.css b/frontend/src/Components/MonitorToggleButton.css
index 794af1e98..09b64f1ab 100644
--- a/frontend/src/Components/MonitorToggleButton.css
+++ b/frontend/src/Components/MonitorToggleButton.css
@@ -1,5 +1,5 @@
.toggleButton {
- composes: button from 'Components/Link/IconButton.css';
+ composes: button from '~Components/Link/IconButton.css';
padding: 0;
font-size: inherit;
diff --git a/frontend/src/Components/Page/ErrorPage.css b/frontend/src/Components/Page/ErrorPage.css
index e62a82a6b..c72e73673 100644
--- a/frontend/src/Components/Page/ErrorPage.css
+++ b/frontend/src/Components/Page/ErrorPage.css
@@ -1,5 +1,5 @@
.page {
- composes: page from './Page.css';
+ composes: page from '~./Page.css';
margin-top: 20px;
text-align: center;
diff --git a/frontend/src/Components/Page/ErrorPage.js b/frontend/src/Components/Page/ErrorPage.js
index 018e98ca1..b19ec0888 100644
--- a/frontend/src/Components/Page/ErrorPage.js
+++ b/frontend/src/Components/Page/ErrorPage.js
@@ -11,7 +11,8 @@ function ErrorPage(props) {
customFiltersError,
tagsError,
qualityProfilesError,
- uiSettingsError
+ uiSettingsError,
+ systemStatusError
} = props;
let errorMessage = 'Failed to load Radarr';
@@ -28,6 +29,8 @@ function ErrorPage(props) {
errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API');
} else if (uiSettingsError) {
errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API');
+ } else if (systemStatusError) {
+ errorMessage = getErrorMessage(uiSettingsError, 'Failed to load system status from API');
}
return (
@@ -50,7 +53,8 @@ ErrorPage.propTypes = {
customFiltersError: PropTypes.object,
tagsError: PropTypes.object,
qualityProfilesError: PropTypes.object,
- uiSettingsError: PropTypes.object
+ uiSettingsError: PropTypes.object,
+ systemStatusError: PropTypes.object
};
export default ErrorPage;
diff --git a/frontend/src/Components/Page/Header/MovieSearchInput.js b/frontend/src/Components/Page/Header/MovieSearchInput.js
index fa49f0716..6246fd375 100644
--- a/frontend/src/Components/Page/Header/MovieSearchInput.js
+++ b/frontend/src/Components/Page/Header/MovieSearchInput.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
-import jdu from 'jdu';
+import Fuse from 'fuse.js';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
@@ -10,6 +10,21 @@ import styles from './MovieSearchInput.css';
const ADD_NEW_TYPE = 'addNew';
+const fuseOptions = {
+ shouldSort: true,
+ includeMatches: true,
+ threshold: 0.3,
+ location: 0,
+ distance: 100,
+ maxPatternLength: 32,
+ minMatchCharLength: 1,
+ keys: [
+ 'title',
+ 'alternateTitles.title',
+ 'tags.label'
+ ]
+};
+
class MovieSearchInput extends Component {
//
@@ -69,16 +84,15 @@ class MovieSearchInput extends Component {
return (
);
}
- goToMovie(movie) {
+ goToMovie(item) {
this.setState({ value: '' });
- this.props.onGoToMovie(movie.titleSlug);
+ this.props.onGoToMovie(item.item.titleSlug);
}
reset() {
@@ -140,26 +154,8 @@ class MovieSearchInput extends Component {
}
onSuggestionsFetchRequested = ({ value }) => {
- const lowerCaseValue = jdu.replace(value).toLowerCase();
-
- const suggestions = this.props.movie.filter((movie) => {
- // Check the title first and if there isn't a match fallback to
- // the alternate titles and finally the tags.
-
- if (value.length === 1) {
- return (
- movie.cleanTitle.startsWith(lowerCaseValue) ||
- movie.alternateTitles.some((alternateTitle) => alternateTitle.cleanTitle.startsWith(lowerCaseValue)) ||
- movie.tags.some((tag) => tag.cleanLabel.startsWith(lowerCaseValue))
- );
- }
-
- return (
- movie.cleanTitle.contains(lowerCaseValue) ||
- movie.alternateTitles.some((alternateTitle) => alternateTitle.cleanTitle.contains(lowerCaseValue)) ||
- movie.tags.some((tag) => tag.cleanLabel.contains(lowerCaseValue))
- );
- });
+ const fuse = new Fuse(this.props.movies, fuseOptions);
+ const suggestions = fuse.search(value);
this.setState({ suggestions });
}
@@ -209,7 +205,7 @@ class MovieSearchInput extends Component {
const inputProps = {
ref: this.setInputRef,
className: styles.input,
- name: 'seriesSearch',
+ name: 'movieSearch',
value,
placeholder: 'Search',
autoComplete: 'off',
@@ -255,7 +251,7 @@ class MovieSearchInput extends Component {
}
MovieSearchInput.propTypes = {
- movie: PropTypes.arrayOf(PropTypes.object).isRequired,
+ movies: PropTypes.arrayOf(PropTypes.object).isRequired,
onGoToMovie: PropTypes.func.isRequired,
onGoToAddNewMovie: PropTypes.func.isRequired,
bindShortcut: PropTypes.func.isRequired
diff --git a/frontend/src/Components/Page/Header/MovieSearchInputConnector.js b/frontend/src/Components/Page/Header/MovieSearchInputConnector.js
index 10f3f52d7..54482a6ab 100644
--- a/frontend/src/Components/Page/Header/MovieSearchInputConnector.js
+++ b/frontend/src/Components/Page/Header/MovieSearchInputConnector.js
@@ -1,35 +1,14 @@
import { connect } from 'react-redux';
-import { push } from 'react-router-redux';
+import { push } from 'connected-react-router';
import { createSelector } from 'reselect';
-import jdu from 'jdu';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import MovieSearchInput from './MovieSearchInput';
-function createCleanTagsSelector() {
- return createSelector(
- createTagsSelector(),
- (tags) => {
- return tags.map((tag) => {
- const {
- id,
- label
- } = tag;
-
- return {
- id,
- label,
- cleanLabel: jdu.replace(label).toLowerCase()
- };
- });
- }
- );
-}
-
function createCleanMovieSelector() {
return createSelector(
createAllMoviesSelector(),
- createCleanTagsSelector(),
+ createTagsSelector(),
(allMovies, allTags) => {
return allMovies.map((movie) => {
const {
@@ -46,27 +25,11 @@ function createCleanMovieSelector() {
titleSlug,
sortTitle,
images,
- cleanTitle: jdu.replace(title).toLowerCase(),
- alternateTitles: alternateTitles.map((alternateTitle) => {
- return {
- title: alternateTitle.title,
- sortTitle: alternateTitle.sortTitle,
- cleanTitle: jdu.replace(alternateTitle.title).toLowerCase()
- };
- }),
+ alternateTitles,
tags: tags.map((id) => {
return allTags.find((tag) => tag.id === id);
})
};
- }).sort((a, b) => {
- if (a.sortTitle < b.sortTitle) {
- return -1;
- }
- if (a.sortTitle > b.sortTitle) {
- return 1;
- }
-
- return 0;
});
}
);
@@ -75,9 +38,9 @@ function createCleanMovieSelector() {
function createMapStateToProps() {
return createSelector(
createCleanMovieSelector(),
- (movie) => {
+ (movies) => {
return {
- movie
+ movies
};
}
);
diff --git a/frontend/src/Components/Page/Header/MovieSearchResult.js b/frontend/src/Components/Page/Header/MovieSearchResult.js
index 83211a766..8708fb151 100644
--- a/frontend/src/Components/Page/Header/MovieSearchResult.js
+++ b/frontend/src/Components/Page/Header/MovieSearchResult.js
@@ -5,38 +5,22 @@ import Label from 'Components/Label';
import MoviePoster from 'Movie/MoviePoster';
import styles from './MovieSearchResult.css';
-function findMatchingAlternateTitle(alternateTitles, cleanQuery) {
- return alternateTitles.find((alternateTitle) => {
- return alternateTitle.cleanTitle.contains(cleanQuery);
- });
-}
-
-function getMatchingTag(tags, cleanQuery) {
- return tags.find((tag) => {
- return tag.cleanLabel.contains(cleanQuery);
- });
-}
-
function MovieSearchResult(props) {
const {
- cleanQuery,
+ match,
title,
- cleanTitle,
images,
alternateTitles,
tags
} = props;
- const titleContains = cleanTitle.contains(cleanQuery);
let alternateTitle = null;
let tag = null;
- if (!titleContains) {
- alternateTitle = findMatchingAlternateTitle(alternateTitles, cleanQuery);
- }
-
- if (!titleContains && !alternateTitle) {
- tag = getMatchingTag(tags, cleanQuery);
+ if (match.key === 'alternateTitles.cleanTitle') {
+ alternateTitle = alternateTitles[match.arrayIndex];
+ } else if (match.key === 'tags.label') {
+ tag = tags[match.arrayIndex];
}
return (
@@ -55,14 +39,15 @@ function MovieSearchResult(props) {
{
- !!alternateTitle &&
+ alternateTitle ?
{alternateTitle.title}
-
+
:
+ null
}
{
- !!tag &&
+ tag ?