Adding indication of autosaved text fields
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import Icon from '../Icon';
|
||||
|
||||
export default class TextField extends React.Component {
|
||||
constructor(props) {
|
||||
@ -8,6 +9,7 @@ export default class TextField extends React.Component {
|
||||
this.state = {
|
||||
in_focus: false,
|
||||
value: props.value || '',
|
||||
saved: false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -25,15 +27,16 @@ export default class TextField extends React.Component {
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ in_focus: true });
|
||||
this.setState({ in_focus: true, saved: false, });
|
||||
}
|
||||
|
||||
handleBlur = () => {
|
||||
const { value, onChange } = this.props;
|
||||
const { value, onChange, autosave } = this.props;
|
||||
const { value: stateValue } = this.state;
|
||||
this.setState({ in_focus: false });
|
||||
if (stateValue !== value) {
|
||||
onChange(stateValue);
|
||||
if (autosave) this.setState({ saved: true });
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,18 +46,20 @@ export default class TextField extends React.Component {
|
||||
type = 'text',
|
||||
placeholder,
|
||||
} = this.props;
|
||||
const { value } = this.state;
|
||||
const { value, saved } = this.state;
|
||||
|
||||
return (
|
||||
<input
|
||||
className={className}
|
||||
type={type}
|
||||
onChange={this.handleChange}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<div className={`text-field__wrapper ${className}`}>
|
||||
<input
|
||||
type={type}
|
||||
onChange={this.handleChange}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{saved && <Icon name="check" className="text-field__saved" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,6 +111,7 @@ const Servers = ({
|
||||
type="text"
|
||||
value={server.name}
|
||||
onChange={value => dispatch(mopidyActions.updateServer({ id: server.id, name: value }))}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
@ -120,6 +121,7 @@ const Servers = ({
|
||||
<TextField
|
||||
value={server.host}
|
||||
onChange={value => dispatch(mopidyActions.updateServer({ id: server.id, host: value }))}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
@ -130,6 +132,7 @@ const Servers = ({
|
||||
type="text"
|
||||
value={server.port}
|
||||
onChange={value => dispatch(mopidyActions.updateServer({ id: server.id, port: value }))}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@ -68,7 +68,14 @@ class Services extends React.Component {
|
||||
}
|
||||
|
||||
renderSpotify() {
|
||||
const user_object = (this.props.spotify.me && this.props.core.users[this.props.spotify.me.uri] ? this.props.core.users[this.props.spotify.me.uri] : null);
|
||||
const {
|
||||
core,
|
||||
spotify,
|
||||
mopidy,
|
||||
spotifyActions,
|
||||
} = this.props;
|
||||
const { country, locale } = this.state;
|
||||
const user_object = (spotify.me && core.users[spotify.me.uri] ?core.users[spotify.me.uri] : null);
|
||||
if (user_object) {
|
||||
var user = (
|
||||
<URILink className="user" type="user" uri={user_object.uri}>
|
||||
@ -92,7 +99,7 @@ class Services extends React.Component {
|
||||
|
||||
let not_installed = null;
|
||||
|
||||
if (!this.props.mopidy.uri_schemes || !this.props.mopidy.uri_schemes.includes('spotify:')) {
|
||||
if (!mopidy.uri_schemes || !mopidy.uri_schemes.includes('spotify:')) {
|
||||
not_installed = (
|
||||
<div>
|
||||
<p className="message warning">
|
||||
@ -111,8 +118,9 @@ class Services extends React.Component {
|
||||
<div className="name">Country</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
onChange={value => this.props.spotifyActions.set({ country: value })}
|
||||
value={this.state.country}
|
||||
onChange={(value) => spotifyActions.set({ country: value })}
|
||||
value={country}
|
||||
autosave
|
||||
/>
|
||||
<div className="description">
|
||||
{'An '}
|
||||
@ -126,12 +134,10 @@ class Services extends React.Component {
|
||||
<label className="field">
|
||||
<div className="name">Locale</div>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => this.setState({ locale: e.target.value })}
|
||||
onFocus={(e) => this.setState({ input_in_focus: 'locale' })}
|
||||
onBlur={(e) => this.props.spotifyActions.set({ locale: e.target.value })}
|
||||
value={this.state.locale}
|
||||
<TextField
|
||||
onChange={(value) => spotifyActions.set({ locale: value })}
|
||||
value={locale}
|
||||
autosave
|
||||
/>
|
||||
<div className="description">
|
||||
{'Lowercase '}
|
||||
@ -158,12 +164,12 @@ class Services extends React.Component {
|
||||
<div className="name">Authorization</div>
|
||||
<div className="input">
|
||||
<SpotifyAuthenticationFrame />
|
||||
{this.props.spotify.refreshing_token ? (
|
||||
{spotify.refreshing_token ? (
|
||||
<a className="button button--default button--working">Force token refres</a>
|
||||
) : (
|
||||
<a
|
||||
className="button button--default"
|
||||
onClick={(e) => this.props.spotifyActions.refreshingToken()}
|
||||
onClick={() => spotifyActions.refreshingToken()}
|
||||
>
|
||||
Force token refresh
|
||||
</a>
|
||||
@ -265,6 +271,7 @@ class Services extends React.Component {
|
||||
}
|
||||
|
||||
renderIcecast() {
|
||||
const { core, coreActions } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<div className="field checkbox">
|
||||
@ -274,8 +281,8 @@ class Services extends React.Component {
|
||||
<input
|
||||
type="checkbox"
|
||||
name="ssl"
|
||||
checked={this.props.core.http_streaming_enabled}
|
||||
onChange={(e) => this.props.coreActions.set({ http_streaming_enabled: !this.props.core.http_streaming_enabled })}
|
||||
checked={core.http_streaming_enabled}
|
||||
onChange={() => coreActions.set({ http_streaming_enabled: !core.http_streaming_enabled })}
|
||||
/>
|
||||
<span className="label">
|
||||
Stream audio to this browser
|
||||
@ -286,10 +293,10 @@ class Services extends React.Component {
|
||||
<label className="field">
|
||||
<div className="name">Location</div>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => this.props.coreActions.set({ http_streaming_url: e.target.value })}
|
||||
value={this.props.core.http_streaming_url}
|
||||
<TextField
|
||||
onChange={(value) => coreActions.set({ http_streaming_url: value })}
|
||||
value={core.http_streaming_url}
|
||||
autosave
|
||||
/>
|
||||
<div className="description">
|
||||
The full URL to your stream endpoint
|
||||
|
||||
@ -58,7 +58,8 @@ const Snapcast = (props) => {
|
||||
<div className="input">
|
||||
<TextField
|
||||
value={host}
|
||||
onChange={value => actions.setConnection({ host: value })}
|
||||
onChange={(value) => actions.setConnection({ host: value })}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,7 +69,9 @@ const Snapcast = (props) => {
|
||||
<div className="input">
|
||||
<TextField
|
||||
value={port}
|
||||
onChange={value => actions.setConnection({ port: value })}
|
||||
name="port"
|
||||
onChange={(value) => actions.setConnection({ port: value })}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -40,6 +40,7 @@ const SnapcastClients = ({ actions, group, groups, show_disconnected_clients })
|
||||
<TextField
|
||||
onChange={(value) => actions.setClientName(client.id, value)}
|
||||
value={client.name}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
@ -63,6 +64,7 @@ const SnapcastClients = ({ actions, group, groups, show_disconnected_clients })
|
||||
label: '+ New group',
|
||||
},
|
||||
]}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
@ -81,6 +83,7 @@ const SnapcastClients = ({ actions, group, groups, show_disconnected_clients })
|
||||
type="number"
|
||||
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
|
||||
value={String(client.latency)}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -49,6 +49,7 @@ const SnapcastGroups = (props) => {
|
||||
<TextField
|
||||
value={group.name}
|
||||
onChange={value => actions.setGroupName(group.id, value)}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,6 +68,7 @@ const SnapcastGroups = (props) => {
|
||||
label: `${stream.id} (${stream.status})`,
|
||||
}
|
||||
))}
|
||||
autosave
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -72,7 +72,7 @@ class Playlist extends React.Component {
|
||||
loadPlaylist(uri);
|
||||
} else if (!prev_mopidy_connected && mopidy_connected) {
|
||||
if (uriSource(uri) !== 'spotify') {
|
||||
loadPlaylist(nextProps.uri);
|
||||
loadPlaylist(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,6 +275,31 @@ Edited
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const {
|
||||
ui: {
|
||||
allow_reporting,
|
||||
slim_mode,
|
||||
theme,
|
||||
load_queue,
|
||||
} = {},
|
||||
core: {
|
||||
users,
|
||||
tracks,
|
||||
playlists,
|
||||
} = {},
|
||||
spotify: {
|
||||
library_playlists: spotify_library_playlists,
|
||||
authorization: spotify_authorized,
|
||||
me: {
|
||||
id: spotify_userid,
|
||||
} = {},
|
||||
} = {},
|
||||
mopidy: {
|
||||
connected: mopidy_connected,
|
||||
library_playlists: local_library_playlists,
|
||||
} = {},
|
||||
} = state;
|
||||
|
||||
// Decode the URI, and then re-encode selected characters
|
||||
// This is needed as Mopidy encodes *some* characters in playlist URIs (but not other characters)
|
||||
// We need to retain ":" because this a reserved URI separator
|
||||
@ -288,18 +313,18 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
return {
|
||||
uri,
|
||||
allow_reporting: state.ui.allow_reporting,
|
||||
slim_mode: state.ui.slim_mode,
|
||||
theme: state.ui.theme,
|
||||
load_queue: state.ui.load_queue,
|
||||
users: state.core.users,
|
||||
tracks: state.core.tracks,
|
||||
playlist: (state.core.playlists[uri] !== undefined ? state.core.playlists[uri] : false),
|
||||
spotify_library_playlists: state.spotify.library_playlists,
|
||||
local_library_playlists: state.mopidy.library_playlists,
|
||||
mopidy_connected: state.mopidy.connected,
|
||||
spotify_authorized: state.spotify.authorization,
|
||||
spotify_userid: (state.spotify.me && state.spotify.me.id ? state.spotify.me.id : null),
|
||||
allow_reporting,
|
||||
slim_mode,
|
||||
theme,
|
||||
load_queue,
|
||||
users,
|
||||
tracks,
|
||||
playlist: (playlists[uri] !== undefined ? playlists[uri] : false),
|
||||
spotify_library_playlists,
|
||||
local_library_playlists,
|
||||
mopidy_connected,
|
||||
spotify_authorized,
|
||||
spotify_userid,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -157,6 +157,7 @@ class Settings extends React.Component {
|
||||
<TextField
|
||||
onChange={(value) => pusherActions.setUsername(value.replace(/\W/g, ''))}
|
||||
value={this.state.pusher_username}
|
||||
autosave
|
||||
/>
|
||||
<div className="description">
|
||||
A non-unique string used to identify this client (no special characters)
|
||||
@ -331,11 +332,10 @@ class Settings extends React.Component {
|
||||
<div className="field">
|
||||
<div className="name">Artist library URI</div>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
<TextField
|
||||
value={this.state.mopidy_library_artists_uri}
|
||||
onChange={(e) => this.setState({ mopidy_library_artists_uri: e.target.value })}
|
||||
onBlur={(e) => this.handleBlur('mopidy', 'library_artists_uri', e.target.value)}
|
||||
onChange={(mopidy_library_artists_uri) => this.setState({ mopidy_library_artists_uri })}
|
||||
autosave
|
||||
/>
|
||||
<div className="description">
|
||||
URI used for collecting library artists
|
||||
@ -346,11 +346,11 @@ class Settings extends React.Component {
|
||||
<label className="field">
|
||||
<div className="name">Album library URI</div>
|
||||
<div className="input">
|
||||
<input
|
||||
<TextField
|
||||
type="text"
|
||||
value={this.state.mopidy_library_albums_uri}
|
||||
onChange={(e) => this.setState({ mopidy_library_albums_uri: e.target.value })}
|
||||
onBlur={(e) => this.handleBlur('mopidy', 'library_albums_uri', e.target.value)}
|
||||
onChange={(mopidy_library_albums_uri) => this.setState({ mopidy_library_albums_uri })}
|
||||
autosave
|
||||
/>
|
||||
<div className="description">
|
||||
URI used for collecting library albums
|
||||
|
||||
@ -64,6 +64,16 @@ class EditCommand extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { command } = this.props;
|
||||
const {
|
||||
name,
|
||||
colour,
|
||||
icon,
|
||||
url,
|
||||
method,
|
||||
post_data,
|
||||
additional_headers,
|
||||
} = this.state;
|
||||
const icons = [
|
||||
'power_settings_new',
|
||||
'eject',
|
||||
@ -110,9 +120,7 @@ class EditCommand extends React.Component {
|
||||
return (
|
||||
<Modal className="modal--create-command">
|
||||
<h1>
|
||||
{this.props.command ? 'Edit' : 'Create'}
|
||||
{' '}
|
||||
command
|
||||
{`${command ? 'Edit' : 'Create'} command`}
|
||||
</h1>
|
||||
<form onSubmit={(e) => this.handleSubmit(e)}>
|
||||
|
||||
@ -123,7 +131,7 @@ command
|
||||
<div className="input">
|
||||
<TextField
|
||||
name="name"
|
||||
value={this.state.name}
|
||||
value={name}
|
||||
onChange={(value) => this.setState({ name: value })}
|
||||
/>
|
||||
</div>
|
||||
@ -135,7 +143,7 @@ command
|
||||
</div>
|
||||
<div className="input">
|
||||
<ColourField
|
||||
colour={this.state.colour}
|
||||
colour={colour}
|
||||
onChange={(colour) => this.setState({ colour })}
|
||||
/>
|
||||
</div>
|
||||
@ -147,7 +155,7 @@ command
|
||||
</div>
|
||||
<div className="input">
|
||||
<IconField
|
||||
icon={this.state.icon}
|
||||
icon={icon}
|
||||
icons={icons}
|
||||
onChange={(icon) => this.setState({ icon })}
|
||||
/>
|
||||
@ -161,8 +169,8 @@ command
|
||||
<div className="input">
|
||||
<TextField
|
||||
name="url"
|
||||
value={this.state.url}
|
||||
onChange={(value) => this.setState({ url: value })}
|
||||
value={url}
|
||||
onChange={(url) => this.setState({ url })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -177,7 +185,7 @@ command
|
||||
type="radio"
|
||||
name="method"
|
||||
value="GET"
|
||||
checked={this.state.method == 'GET'}
|
||||
checked={method === 'GET'}
|
||||
onChange={(e) => this.setState({ method: e.target.value })}
|
||||
/>
|
||||
<span className="label">GET</span>
|
||||
@ -187,7 +195,7 @@ command
|
||||
type="radio"
|
||||
name="method"
|
||||
value="POST"
|
||||
checked={this.state.method == 'POST'}
|
||||
checked={method === 'POST'}
|
||||
onChange={(e) => this.setState({ method: e.target.value })}
|
||||
/>
|
||||
<span className="label">POST</span>
|
||||
@ -195,7 +203,7 @@ command
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.method == 'POST' && (
|
||||
{method === 'POST' && (
|
||||
<div className="field textarea white">
|
||||
<div className="name">
|
||||
Data
|
||||
@ -203,7 +211,7 @@ command
|
||||
<div className="input">
|
||||
<textarea
|
||||
name="command"
|
||||
value={this.state.post_data}
|
||||
value={post_data}
|
||||
onChange={(e) => this.setState({ post_data: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
@ -217,7 +225,7 @@ command
|
||||
<div className="input">
|
||||
<textarea
|
||||
name="headers"
|
||||
value={this.state.additional_headers}
|
||||
value={additional_headers}
|
||||
onChange={(e) => this.setState({ additional_headers: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
@ -225,7 +233,15 @@ command
|
||||
|
||||
|
||||
<div className="actions centered-text">
|
||||
{this.props.command ? <button type="button" className="button button--destructive button--large" onClick={(e) => this.handleDelete(e)}>Delete</button> : null}
|
||||
{command && (
|
||||
<button
|
||||
type="button"
|
||||
className="button button--destructive button--large"
|
||||
onClick={(e) => this.handleDelete(e)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
<button type="submit" className="button button--primary button--large">Save</button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -582,6 +582,24 @@ select {
|
||||
}
|
||||
}
|
||||
|
||||
.text-field {
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__saved {
|
||||
@include fadeout($delay: 1s);
|
||||
position: absolute;
|
||||
top: calc(50% - 0.5em);
|
||||
right: 10px;
|
||||
line-height: 1em;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: colour(green);
|
||||
}
|
||||
}
|
||||
|
||||
@include responsive($bp_medium) {
|
||||
.field {
|
||||
display: block;
|
||||
|
||||
Reference in New Issue
Block a user