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, |   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))); | ||||||
|   | |||||||
| @@ -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; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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. | ||||||
|   | |||||||
| @@ -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)); | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user