UI Usability Fixes

* Fallback to sessionStorage if localStorage is absent
* Disable auto-correct and auto-capitalize on username field (SignIn)
* Fix SignIn component name
* Improve support for low screen widths

Co-authored-by: kasedy <kasedy@gmail.com>
This commit is contained in:
rjwats 2020-05-16 12:39:18 +01:00 committed by GitHub
parent a1f4e57a21
commit 7d3bbf4240
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 84 additions and 99 deletions

View File

@ -39,17 +39,17 @@ const styles = (theme: Theme) => createStyles({
} }
}); });
type SignInPageProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps; type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
interface SignInPageState { interface SignInState {
username: string, username: string,
password: string, password: string,
processing: boolean processing: boolean
} }
class SignInPage extends Component<SignInPageProps, SignInPageState> { class SignIn extends Component<SignInProps, SignInState> {
constructor(props: SignInPageProps) { constructor(props: SignInProps) {
super(props); super(props);
this.state = { this.state = {
username: '', username: '',
@ -115,6 +115,10 @@ class SignInPage extends Component<SignInPageProps, SignInPageState> {
value={username} value={username}
onChange={this.updateInputElement} onChange={this.updateInputElement}
margin="normal" margin="normal"
inputProps={{
autoCapitalize: "none",
autoCorrect: "off",
}}
/> />
<PasswordValidator <PasswordValidator
disabled={processing} disabled={processing}
@ -140,4 +144,4 @@ class SignInPage extends Component<SignInPageProps, SignInPageState> {
} }
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignInPage))); export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));

View File

@ -7,21 +7,28 @@ export const ACCESS_TOKEN = 'access_token';
export const LOGIN_PATHNAME = 'loginPathname'; export const LOGIN_PATHNAME = 'loginPathname';
export const LOGIN_SEARCH = 'loginSearch'; export const LOGIN_SEARCH = 'loginSearch';
/**
* Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
*/
export function getStorage() {
return localStorage || sessionStorage;
}
export function storeLoginRedirect(location?: H.Location) { export function storeLoginRedirect(location?: H.Location) {
if (location) { if (location) {
localStorage.setItem(LOGIN_PATHNAME, location.pathname); getStorage().setItem(LOGIN_PATHNAME, location.pathname);
localStorage.setItem(LOGIN_SEARCH, location.search); getStorage().setItem(LOGIN_SEARCH, location.search);
} }
} }
export function clearLoginRedirect() { export function clearLoginRedirect() {
localStorage.removeItem(LOGIN_PATHNAME); getStorage().removeItem(LOGIN_PATHNAME);
localStorage.removeItem(LOGIN_SEARCH); getStorage().removeItem(LOGIN_SEARCH);
} }
export function fetchLoginRedirect(): H.LocationDescriptorObject { export function fetchLoginRedirect(): H.LocationDescriptorObject {
const loginPathname = localStorage.getItem(LOGIN_PATHNAME); const loginPathname = getStorage().getItem(LOGIN_PATHNAME);
const loginSearch = localStorage.getItem(LOGIN_SEARCH); const loginSearch = getStorage().getItem(LOGIN_SEARCH);
clearLoginRedirect(); clearLoginRedirect();
return { return {
pathname: loginPathname || `/${PROJECT_PATH}/`, pathname: loginPathname || `/${PROJECT_PATH}/`,
@ -33,7 +40,7 @@ export function fetchLoginRedirect(): H.LocationDescriptorObject {
* Wraps the normal fetch routene with one with provides the access token if present. * Wraps the normal fetch routene with one with provides the access token if present.
*/ */
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> { export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
const accessToken = localStorage.getItem(ACCESS_TOKEN); const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) { if (accessToken) {
params = params || {}; params = params || {};
params.credentials = 'include'; params.credentials = 'include';
@ -63,7 +70,7 @@ export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestIni
} }
export function addAccessTokenParameter(url: string) { export function addAccessTokenParameter(url: string) {
const accessToken = localStorage.getItem(ACCESS_TOKEN); const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (!accessToken) { if (!accessToken) {
return url; return url;
} }

View File

@ -8,7 +8,7 @@ import { withStyles, Theme, createStyles, WithStyles } from '@material-ui/core/s
import history from '../history' import history from '../history'
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api'; import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
import { ACCESS_TOKEN, authorizedFetch } from './Authentication'; import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
import { AuthenticationContext, Me } from './AuthenticationContext'; import { AuthenticationContext, Me } from './AuthenticationContext';
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken); export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken);
@ -81,7 +81,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
} }
refresh = () => { refresh = () => {
const accessToken = localStorage.getItem(ACCESS_TOKEN) const accessToken = getStorage().getItem(ACCESS_TOKEN)
if (accessToken) { if (accessToken) {
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT) authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
.then(response => { .then(response => {
@ -100,7 +100,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
signIn = (accessToken: string) => { signIn = (accessToken: string) => {
try { try {
localStorage.setItem(ACCESS_TOKEN, accessToken); getStorage().setItem(ACCESS_TOKEN, accessToken);
const me: Me = decodeMeJWT(accessToken); const me: Me = decodeMeJWT(accessToken);
this.setState({ context: { ...this.state.context, me } }); this.setState({ context: { ...this.state.context, me } });
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' }); this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
@ -111,7 +111,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
} }
signOut = () => { signOut = () => {
localStorage.removeItem(ACCESS_TOKEN); getStorage().removeItem(ACCESS_TOKEN);
this.setState({ this.setState({
context: { context: {
...this.state.context, ...this.state.context,

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Typography, TableRow, TableBody, TableCell, TableHead, Table, Box } from '@material-ui/core'; import { Typography, Box, List, ListItem, ListItemText } from '@material-ui/core';
import { SectionContent } from '../components'; import { SectionContent } from '../components';
class DemoInformation extends Component { class DemoInformation extends Component {
@ -17,78 +17,52 @@ class DemoInformation extends Component {
simplify merges should you wish to update your project with future framework changes. simplify merges should you wish to update your project with future framework changes.
</Typography> </Typography>
<Typography variant="body1" paragraph> <Typography variant="body1" paragraph>
The demo project interface code stored in the interface/project directory: The demo project interface code is stored in the interface/project directory:
</Typography> </Typography>
<Table> <List>
<TableHead> <ListItem>
<TableRow> <ListItemText
<TableCell> primary="ProjectMenu.tsx"
File secondary="You can add your project's screens to the side bar here."
</TableCell> />
<TableCell> </ListItem>
Description <ListItem>
</TableCell> <ListItemText
</TableRow> primary="ProjectRouting.tsx"
</TableHead> secondary="The routing which controls the screens of your project."
<TableBody> />
<TableRow> </ListItem>
<TableCell> <ListItem>
ProjectMenu.tsx <ListItemText
</TableCell> primary="DemoProject.tsx"
<TableCell> secondary="This screen, with tabs and tab routing."
You can add your project's screens to the side bar here. />
</TableCell> </ListItem>
</TableRow> <ListItem>
<TableRow> <ListItemText
<TableCell> primary="DemoInformation.tsx"
ProjectRouting.tsx secondary="The demo information page."
</TableCell> />
<TableCell> </ListItem>
The routing which controls the screens of your project. <ListItem>
</TableCell> <ListItemText
</TableRow> primary="LightStateRestController.tsx"
<TableRow> secondary="A form which lets the user control the LED over a REST service."
<TableCell> />
DemoProject.tsx </ListItem>
</TableCell> <ListItem>
<TableCell> <ListItemText
This screen, with tabs and tab routing. primary="LightStateWebSocketController.tsx"
</TableCell> secondary="A form which lets the user control and monitor the status of the LED over WebSockets."
</TableRow> />
<TableRow> </ListItem>
<TableCell> <ListItem>
DemoInformation.tsx <ListItemText
</TableCell> primary="LightMqttSettingsController.tsx"
<TableCell> secondary="A form which lets the user change the MQTT settings for MQTT based control of the LED."
The demo information page. />
</TableCell> </ListItem>
</TableRow> </List>
<TableRow>
<TableCell>
LightStateRestController.tsx
</TableCell>
<TableCell>
A form which lets the user control the LED over a REST service.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
LightStateWebSocketController.tsx
</TableCell>
<TableCell>
A form which lets the user control and monitor the status of the LED over WebSockets.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
LightMqttSettingsController.tsx
</TableCell>
<TableCell>
A form which lets the user change the MQTT settings for MQTT based control of the LED.
</TableCell>
</TableRow>
</TableBody>
</Table>
<Box mt={2}> <Box mt={2}>
<Typography variant="body1"> <Typography variant="body1">
See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project. See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project.

View File

@ -1,7 +1,7 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { ValidatorForm } from 'react-material-ui-form-validator'; import { ValidatorForm } from 'react-material-ui-form-validator';
import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow } from '@material-ui/core'; import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow, withWidth, WithWidthProps, isWidthDown } from '@material-ui/core';
import { Box, Button, Typography, } from '@material-ui/core'; import { Box, Button, Typography, } from '@material-ui/core';
import EditIcon from '@material-ui/icons/Edit'; import EditIcon from '@material-ui/icons/Edit';
@ -28,7 +28,7 @@ function compareUsers(a: User, b: User) {
return 0; return 0;
} }
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps; type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps & WithWidthProps;
type ManageUsersFormState = { type ManageUsersFormState = {
creating: boolean; creating: boolean;
@ -106,12 +106,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
} }
render() { render() {
const { data, loadData } = this.props; const { width, data, loadData } = this.props;
const { user, creating } = this.state; const { user, creating } = this.state;
return ( return (
<Fragment> <Fragment>
<ValidatorForm onSubmit={this.onSubmit}> <ValidatorForm onSubmit={this.onSubmit}>
<Table size="small"> <Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>Username</TableCell> <TableCell>Username</TableCell>
@ -141,12 +141,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
<TableFooter> <TableFooter >
<TableRow> <TableRow>
<TableCell colSpan={2} /> <TableCell colSpan={2} />
<TableCell align="center"> <TableCell align="center" padding="default">
<Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}> <Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}>
Add User Add
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -188,4 +188,4 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
} }
export default withAuthenticatedContext(ManageUsersForm); export default withAuthenticatedContext(withWidth()(ManageUsersForm));

View File

@ -79,7 +79,7 @@ class WiFiStatusForm extends Component<WiFiStatusFormProps> {
<SettingsInputComponentIcon /> <SettingsInputComponentIcon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary="Gateway IP" secondary={data.gateway_ip ? data.gateway_ip : "none"} /> <ListItemText primary="Gateway IP" secondary={data.gateway_ip || "none"} />
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
<ListItem> <ListItem>