Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>pull/6/head
parent
e41f884153
commit
23bc5b11cf
@ -0,0 +1,8 @@
|
|||||||
|
.deviceInputWrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
composes: inputContainer from './TagInput.css';
|
||||||
|
composes: hasButton from 'Components/Form/Input.css';
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import FormInputButton from './FormInputButton';
|
||||||
|
import TagInput, { tagShape } from './TagInput';
|
||||||
|
import styles from './DeviceInput.css';
|
||||||
|
|
||||||
|
class DeviceInput extends Component {
|
||||||
|
|
||||||
|
onTagAdd = (device) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// New tags won't have an ID, only a name.
|
||||||
|
const deviceId = device.id || device.name;
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: [...value, deviceId]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onTagDelete = ({ index }) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = value.slice();
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
items,
|
||||||
|
selectedDevices,
|
||||||
|
hasError,
|
||||||
|
hasWarning,
|
||||||
|
isFetching,
|
||||||
|
onRefreshPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<TagInput
|
||||||
|
className={styles.inputContainer}
|
||||||
|
tags={selectedDevices}
|
||||||
|
tagList={items}
|
||||||
|
allowNew={true}
|
||||||
|
minQueryLength={0}
|
||||||
|
hasError={hasError}
|
||||||
|
hasWarning={hasWarning}
|
||||||
|
onTagAdd={this.onTagAdd}
|
||||||
|
onTagDelete={this.onTagDelete}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputButton
|
||||||
|
onPress={onRefreshPress}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.REFRESH}
|
||||||
|
isSpinning={isFetching}
|
||||||
|
/>
|
||||||
|
</FormInputButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceInput.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
selectedDevices: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
hasWarning: PropTypes.bool,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onRefreshPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceInput.defaultProps = {
|
||||||
|
className: styles.deviceInputWrapper,
|
||||||
|
inputClassName: styles.input
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeviceInput;
|
@ -0,0 +1,99 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchDevices, clearDevices } from 'Store/Actions/deviceActions';
|
||||||
|
import DeviceInput from './DeviceInput';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { value }) => value,
|
||||||
|
(state) => state.devices,
|
||||||
|
(value, devices) => {
|
||||||
|
|
||||||
|
return {
|
||||||
|
...devices,
|
||||||
|
selectedDevices: value.map((valueDevice) => {
|
||||||
|
// Disable equality ESLint rule so we don't need to worry about
|
||||||
|
// a type mismatch between the value items and the device ID.
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
const device = devices.items.find((d) => d.id == valueDevice);
|
||||||
|
|
||||||
|
if (device) {
|
||||||
|
return {
|
||||||
|
id: device.id,
|
||||||
|
name: `${device.name} (${device.id})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: valueDevice,
|
||||||
|
name: `Unknown (${valueDevice})`
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchDevices: fetchDevices,
|
||||||
|
dispatchClearDevices: clearDevices
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeviceInputConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
this._populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
// this.props.dispatchClearDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
_populate() {
|
||||||
|
const {
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
dispatchFetchDevices
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
dispatchFetchDevices({ provider, providerData });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onRefreshPress = () => {
|
||||||
|
this._populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<DeviceInput
|
||||||
|
{...this.props}
|
||||||
|
onRefreshPress={this.onRefreshPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceInputConnector.propTypes = {
|
||||||
|
provider: PropTypes.string.isRequired,
|
||||||
|
providerData: PropTypes.object.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchDevices: PropTypes.func.isRequired,
|
||||||
|
dispatchClearDevices: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector);
|
@ -0,0 +1,83 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import requestAction from 'Utilities/requestAction';
|
||||||
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import { set } from './baseActions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'devices';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
items: [],
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: false
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_DEVICES = 'devices/fetchDevices';
|
||||||
|
export const CLEAR_DEVICES = 'devices/clearDevices';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchDevices = createThunk(FETCH_DEVICES);
|
||||||
|
export const clearDevices = createAction(CLEAR_DEVICES);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
|
[FETCH_DEVICES]: function(getState, payload, dispatch) {
|
||||||
|
const actionPayload = {
|
||||||
|
action: 'getDevices',
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isFetching: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promise = requestAction(actionPayload);
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: true,
|
||||||
|
error: null,
|
||||||
|
items: data.devices || []
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
|
[CLEAR_DEVICES]: function(state) {
|
||||||
|
return updateSectionState(state, section, defaultState);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, defaultState, section);
|
@ -0,0 +1,14 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
|
{
|
||||||
|
public class PushBulletDevice
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "Iden")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string Nickname { get; set; }
|
||||||
|
public string Manufacturer { get; set; }
|
||||||
|
public string Model { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
|
{
|
||||||
|
public class PushBulletDevicesResponse
|
||||||
|
{
|
||||||
|
public List<PushBulletDevice> Devices { get; set; }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue