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:
		| @@ -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, | ||||
|   password: string, | ||||
|   processing: boolean | ||||
| } | ||||
|  | ||||
| class SignInPage extends Component<SignInPageProps, SignInPageState> { | ||||
| class SignIn extends Component<SignInProps, SignInState> { | ||||
|  | ||||
|   constructor(props: SignInPageProps) { | ||||
|   constructor(props: SignInProps) { | ||||
|     super(props); | ||||
|     this.state = { | ||||
|       username: '', | ||||
| @@ -115,6 +115,10 @@ class SignInPage extends Component<SignInPageProps, SignInPageState> { | ||||
|               value={username} | ||||
|               onChange={this.updateInputElement} | ||||
|               margin="normal" | ||||
|               inputProps={{ | ||||
|                 autoCapitalize: "none", | ||||
|                 autoCorrect: "off", | ||||
|               }} | ||||
|             /> | ||||
|             <PasswordValidator | ||||
|               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))); | ||||
|   | ||||
| @@ -7,21 +7,28 @@ export const ACCESS_TOKEN = 'access_token'; | ||||
| export const LOGIN_PATHNAME = 'loginPathname'; | ||||
| 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) { | ||||
|   if (location) { | ||||
|     localStorage.setItem(LOGIN_PATHNAME, location.pathname); | ||||
|     localStorage.setItem(LOGIN_SEARCH, location.search); | ||||
|     getStorage().setItem(LOGIN_PATHNAME, location.pathname); | ||||
|     getStorage().setItem(LOGIN_SEARCH, location.search); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function clearLoginRedirect() { | ||||
|   localStorage.removeItem(LOGIN_PATHNAME); | ||||
|   localStorage.removeItem(LOGIN_SEARCH); | ||||
|   getStorage().removeItem(LOGIN_PATHNAME); | ||||
|   getStorage().removeItem(LOGIN_SEARCH); | ||||
| } | ||||
|  | ||||
| export function fetchLoginRedirect(): H.LocationDescriptorObject { | ||||
|   const loginPathname = localStorage.getItem(LOGIN_PATHNAME); | ||||
|   const loginSearch = localStorage.getItem(LOGIN_SEARCH); | ||||
|   const loginPathname = getStorage().getItem(LOGIN_PATHNAME); | ||||
|   const loginSearch = getStorage().getItem(LOGIN_SEARCH); | ||||
|   clearLoginRedirect(); | ||||
|   return { | ||||
|     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. | ||||
|  */ | ||||
| export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> { | ||||
|   const accessToken = localStorage.getItem(ACCESS_TOKEN); | ||||
|   const accessToken = getStorage().getItem(ACCESS_TOKEN); | ||||
|   if (accessToken) { | ||||
|     params = params || {}; | ||||
|     params.credentials = 'include'; | ||||
| @@ -63,7 +70,7 @@ export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestIni | ||||
| } | ||||
|  | ||||
| export function addAccessTokenParameter(url: string) { | ||||
|   const accessToken = localStorage.getItem(ACCESS_TOKEN); | ||||
|   const accessToken = getStorage().getItem(ACCESS_TOKEN); | ||||
|   if (!accessToken) { | ||||
|     return url; | ||||
|   } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import { withStyles, Theme, createStyles, WithStyles } from '@material-ui/core/s | ||||
|  | ||||
| import history from '../history' | ||||
| import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api'; | ||||
| import { ACCESS_TOKEN, authorizedFetch } from './Authentication'; | ||||
| import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication'; | ||||
| import { AuthenticationContext, Me } from './AuthenticationContext'; | ||||
|  | ||||
| export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken); | ||||
| @@ -81,7 +81,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, | ||||
|   } | ||||
|  | ||||
|   refresh = () => { | ||||
|     const accessToken = localStorage.getItem(ACCESS_TOKEN) | ||||
|     const accessToken = getStorage().getItem(ACCESS_TOKEN) | ||||
|     if (accessToken) { | ||||
|       authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT) | ||||
|         .then(response => { | ||||
| @@ -100,7 +100,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, | ||||
|  | ||||
|   signIn = (accessToken: string) => { | ||||
|     try { | ||||
|       localStorage.setItem(ACCESS_TOKEN, accessToken); | ||||
|       getStorage().setItem(ACCESS_TOKEN, accessToken); | ||||
|       const me: Me = decodeMeJWT(accessToken); | ||||
|       this.setState({ context: { ...this.state.context, me } }); | ||||
|       this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' }); | ||||
| @@ -111,7 +111,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, | ||||
|   } | ||||
|  | ||||
|   signOut = () => { | ||||
|     localStorage.removeItem(ACCESS_TOKEN); | ||||
|     getStorage().removeItem(ACCESS_TOKEN); | ||||
|     this.setState({ | ||||
|       context: { | ||||
|         ...this.state.context, | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| 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'; | ||||
|  | ||||
| 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. | ||||
|         </Typography> | ||||
|         <Typography variant="body1" paragraph> | ||||
|           The demo project interface code stored in the interface/project directory: | ||||
|         </Typography> | ||||
|         <Table> | ||||
|           <TableHead> | ||||
|             <TableRow> | ||||
|               <TableCell> | ||||
|                 File | ||||
|               </TableCell> | ||||
|               <TableCell> | ||||
|                 Description | ||||
|               </TableCell> | ||||
|             </TableRow> | ||||
|           </TableHead> | ||||
|           <TableBody> | ||||
|             <TableRow> | ||||
|               <TableCell> | ||||
|                 ProjectMenu.tsx | ||||
|               </TableCell> | ||||
|               <TableCell> | ||||
|                 You can add your project's screens to the side bar here. | ||||
|               </TableCell> | ||||
|             </TableRow> | ||||
|             <TableRow> | ||||
|               <TableCell> | ||||
|                 ProjectRouting.tsx | ||||
|               </TableCell> | ||||
|               <TableCell> | ||||
|                 The routing which controls the screens of your project. | ||||
|               </TableCell> | ||||
|             </TableRow> | ||||
|             <TableRow> | ||||
|               <TableCell> | ||||
|                 DemoProject.tsx | ||||
|               </TableCell> | ||||
|               <TableCell> | ||||
|                 This screen, with tabs and tab routing. | ||||
|               </TableCell> | ||||
|             </TableRow> | ||||
|             <TableRow> | ||||
|               <TableCell> | ||||
|                 DemoInformation.tsx | ||||
|               </TableCell> | ||||
|               <TableCell> | ||||
|                 The demo information page. | ||||
|               </TableCell> | ||||
|             </TableRow> | ||||
|             <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> | ||||
|           The demo project interface code is stored in the interface/project directory: | ||||
|         </Typography>         | ||||
|         <List> | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="ProjectMenu.tsx" | ||||
|               secondary="You can add your project's screens to the side bar here." | ||||
|             /> | ||||
|           </ListItem> | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="ProjectRouting.tsx" | ||||
|               secondary="The routing which controls the screens of your project." | ||||
|             /> | ||||
|           </ListItem> | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="DemoProject.tsx" | ||||
|               secondary="This screen, with tabs and tab routing." | ||||
|             /> | ||||
|           </ListItem> | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="DemoInformation.tsx" | ||||
|               secondary="The demo information page." | ||||
|             /> | ||||
|           </ListItem> | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="LightStateRestController.tsx" | ||||
|               secondary="A form which lets the user control the LED over a REST service." | ||||
|             /> | ||||
|           </ListItem> | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="LightStateWebSocketController.tsx" | ||||
|               secondary="A form which lets the user control and monitor the status of the LED over WebSockets." | ||||
|             /> | ||||
|           </ListItem> | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="LightMqttSettingsController.tsx" | ||||
|               secondary="A form which lets the user change the MQTT settings for MQTT based control of the LED." | ||||
|             /> | ||||
|           </ListItem> | ||||
|         </List> | ||||
|         <Box mt={2}> | ||||
|           <Typography variant="body1"> | ||||
|             See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project. | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import React, { Fragment } from 'react'; | ||||
| 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 EditIcon from '@material-ui/icons/Edit'; | ||||
| @@ -28,7 +28,7 @@ function compareUsers(a: User, b: User) { | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps; | ||||
| type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps & WithWidthProps; | ||||
|  | ||||
| type ManageUsersFormState = { | ||||
|   creating: boolean; | ||||
| @@ -106,12 +106,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const { data, loadData } = this.props; | ||||
|     const { width, data, loadData } = this.props; | ||||
|     const { user, creating } = this.state; | ||||
|     return ( | ||||
|       <Fragment> | ||||
|         <ValidatorForm onSubmit={this.onSubmit}> | ||||
|           <Table size="small"> | ||||
|           <Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}> | ||||
|             <TableHead> | ||||
|               <TableRow> | ||||
|                 <TableCell>Username</TableCell> | ||||
| @@ -141,12 +141,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF | ||||
|                 </TableRow> | ||||
|               ))} | ||||
|             </TableBody> | ||||
|             <TableFooter> | ||||
|             <TableFooter > | ||||
|               <TableRow> | ||||
|                 <TableCell colSpan={2} /> | ||||
|                 <TableCell align="center"> | ||||
|                 <TableCell align="center" padding="default"> | ||||
|                   <Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}> | ||||
|                     Add User | ||||
|                     Add | ||||
|                   </Button> | ||||
|                 </TableCell> | ||||
|               </TableRow> | ||||
| @@ -188,4 +188,4 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF | ||||
|  | ||||
| } | ||||
|  | ||||
| export default withAuthenticatedContext(ManageUsersForm); | ||||
| export default withAuthenticatedContext(withWidth()(ManageUsersForm)); | ||||
|   | ||||
| @@ -79,7 +79,7 @@ class WiFiStatusForm extends Component<WiFiStatusFormProps> { | ||||
|                   <SettingsInputComponentIcon /> | ||||
|                 </Avatar> | ||||
|               </ListItemAvatar> | ||||
|               <ListItemText primary="Gateway IP" secondary={data.gateway_ip ? data.gateway_ip : "none"} /> | ||||
|               <ListItemText primary="Gateway IP" secondary={data.gateway_ip || "none"} /> | ||||
|             </ListItem> | ||||
|             <Divider variant="inset" component="li" /> | ||||
|             <ListItem> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user