Adding indication of autosaved text fields

This commit is contained in:
James Barnsley
2020-05-03 13:54:34 +12:00
parent d4e6757341
commit f1b6ada66b
15 changed files with 336 additions and 165 deletions

View File

@ -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>
);
}
}

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,
};
};

View File

@ -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

View File

@ -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>

View File

@ -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;