Webatrice: Registration toasts (#4566)
* wip * Registration Success Toast * remove debugging code * remove unused field * Show toast on successful password reset * Toast on account activation success * lint and PR feedback * Rework interface names to avoid collision * Move CssBaseline to sibling of ToastProvider Co-authored-by: Brent Clark <brent@backboneiq.com>
This commit is contained in:
parent
88b861d632
commit
4c04b4ef5a
11 changed files with 164 additions and 13 deletions
71
webclient/src/components/Toast/ToastContext.tsx
Normal file
71
webclient/src/components/Toast/ToastContext.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { createContext, FC, ReactChild, ReactNode, useContext, useEffect, useReducer, ContextType, Context } from 'react'
|
||||
|
||||
import { ACTIONS, initialState, reducer } from './reducer';
|
||||
import Toast from './Toast'
|
||||
|
||||
interface ToastEntry {
|
||||
isOpen: boolean,
|
||||
children: ReactChild,
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
toasts: Map<string, ToastEntry>,
|
||||
addToast: (key, children) => void,
|
||||
openToast: (key) => void,
|
||||
closeToast: (key) => void,
|
||||
removeToast: (key) => void,
|
||||
}
|
||||
|
||||
const ToastContext: Context<any> = createContext<ToastState>({
|
||||
toasts: new Map<string, ToastEntry>(),
|
||||
addToast: (key, children) => {},
|
||||
openToast: (key) => {},
|
||||
closeToast: (key) => {},
|
||||
removeToast: (key) => {},
|
||||
});
|
||||
|
||||
export const ToastProvider: FC<ReactNode> = (props) => {
|
||||
const { children } = props
|
||||
const [state, dispatch] = useReducer(reducer, initialState)
|
||||
const providerState = {
|
||||
toasts: state.toasts,
|
||||
addToast: (key, children) => dispatch({ type: ACTIONS.ADD_TOAST, payload: { key, children } }),
|
||||
openToast: key => dispatch({ type: ACTIONS.OPEN_TOAST, payload: key }),
|
||||
closeToast: key => dispatch({ type: ACTIONS.CLOSE_TOAST, payload: key }),
|
||||
removeToast: key => dispatch({ type: ACTIONS.REMOVE_TOAST, payload: key }),
|
||||
}
|
||||
return (
|
||||
<ToastContext.Provider value={providerState}>
|
||||
{children}
|
||||
<div>
|
||||
{Array.from(state.toasts).map(([key, value]) => {
|
||||
const { isOpen, children } = value;
|
||||
return (
|
||||
<Toast key={key} open={isOpen} onClose={() => dispatch({ type: ACTIONS.CLOSE_TOAST, payload: key })}>
|
||||
{children}
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ToastHookOptions {
|
||||
key: string,
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function useToast<ToastHookOptions>({ key, children }) {
|
||||
const { addToast, openToast, closeToast, removeToast } = useContext(ToastContext)
|
||||
|
||||
useEffect(() => {
|
||||
addToast(key, children)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
openToast: () => openToast(key),
|
||||
closeToast: () => closeToast(key),
|
||||
removeToast: () => removeToast(key),
|
||||
}
|
||||
}
|
8
webclient/src/components/Toast/index.ts
Normal file
8
webclient/src/components/Toast/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { useToast, ToastProvider } from './ToastContext';
|
||||
import Toast from './Toast';
|
||||
|
||||
export {
|
||||
Toast as default,
|
||||
useToast,
|
||||
ToastProvider,
|
||||
}
|
48
webclient/src/components/Toast/reducer.ts
Normal file
48
webclient/src/components/Toast/reducer.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
export const ACTIONS = {
|
||||
ADD_TOAST: 'ADD_TOAST',
|
||||
OPEN_TOAST: 'OPEN_TOAST',
|
||||
CLOSE_TOAST: 'CLOSE_TOAST',
|
||||
REMOVE_TOAST: 'REMOVE_TOAST',
|
||||
}
|
||||
|
||||
export const initialState = {
|
||||
toasts: new Map()
|
||||
}
|
||||
|
||||
export function reducer(state, action) {
|
||||
const { type, payload } = action
|
||||
switch (type) {
|
||||
case ACTIONS.ADD_TOAST: {
|
||||
const newState = { ...state }
|
||||
newState.toasts = new Map(Array.from(state.toasts))
|
||||
const { toasts } = newState;
|
||||
const { key, children } = payload
|
||||
toasts.set(key, { isOpen: false, children })
|
||||
return newState
|
||||
}
|
||||
case ACTIONS.OPEN_TOAST: {
|
||||
const newState = { ...state }
|
||||
newState.toasts = new Map(Array.from(state.toasts))
|
||||
const { toasts } = newState;
|
||||
const toast = toasts.get(payload)
|
||||
toasts.set(payload, { isOpen: true, children: toast.children })
|
||||
return newState
|
||||
}
|
||||
case ACTIONS.CLOSE_TOAST: {
|
||||
const newState = { ...state }
|
||||
newState.toasts = new Map(Array.from(state.toasts))
|
||||
const { toasts } = newState;
|
||||
const toast = toasts.get(payload)
|
||||
toasts.set(payload, { isOpen: false, children: toast.children })
|
||||
return newState
|
||||
}
|
||||
case ACTIONS.REMOVE_TOAST: {
|
||||
const newState = { ...state }
|
||||
newState.toasts = new Map(Array.from(state.toasts))
|
||||
newState.toasts.delete(payload)
|
||||
return newState
|
||||
}
|
||||
default:
|
||||
throw Error('Please pick an available action')
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
import { Component } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter as Router } from 'react-router-dom';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
|
@ -10,6 +9,8 @@ import FeatureDetection from './FeatureDetection';
|
|||
|
||||
import './AppShell.css';
|
||||
|
||||
import { ToastProvider } from 'components/Toast'
|
||||
|
||||
class AppShell extends Component {
|
||||
componentDidMount() {
|
||||
// @TODO (1)
|
||||
|
@ -24,14 +25,16 @@ class AppShell extends Component {
|
|||
return (
|
||||
<Provider store={store}>
|
||||
<CssBaseline />
|
||||
<div className="AppShell" onContextMenu={this.handleContextMenu}>
|
||||
<Router>
|
||||
<Header />
|
||||
<ToastProvider>
|
||||
<div className="AppShell" onContextMenu={this.handleContextMenu}>
|
||||
<Router>
|
||||
<Header />
|
||||
|
||||
<FeatureDetection />
|
||||
<Routes />
|
||||
</Router>
|
||||
</div>
|
||||
<FeatureDetection />
|
||||
<Routes />
|
||||
</Router>
|
||||
</div>
|
||||
</ToastProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { RouteEnum, WebSocketConnectOptions, getHostPort } from 'types';
|
|||
import { ServerSelectors, ServerTypes } from 'store';
|
||||
|
||||
import './Login.css';
|
||||
import { useToast } from 'components/Toast';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
|
@ -68,16 +69,21 @@ const Login = ({ state, description }: LoginProps) => {
|
|||
});
|
||||
const [userToResetPassword, setUserToResetPassword] = useState(null);
|
||||
|
||||
const passwordResetToast = useToast({ key: 'password-reset-success', children: 'Password Reset Successfully' })
|
||||
const accountActivatedToast = useToast({ key: 'account-activation-success', children: 'Account Activated Successfully' })
|
||||
|
||||
useReduxEffect(() => {
|
||||
closeRequestPasswordResetDialog();
|
||||
openResetPasswordDialog();
|
||||
}, ServerTypes.RESET_PASSWORD_REQUESTED, []);
|
||||
|
||||
useReduxEffect(() => {
|
||||
passwordResetToast.openToast()
|
||||
closeResetPasswordDialog();
|
||||
}, ServerTypes.RESET_PASSWORD_SUCCESS, []);
|
||||
|
||||
useReduxEffect(() => {
|
||||
accountActivatedToast.openToast()
|
||||
closeActivateAccountDialog();
|
||||
}, ServerTypes.ACCOUNT_ACTIVATION_SUCCESS, []);
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useState } from 'react';
|
||||
import { Form, Field } from 'react-final-form';
|
||||
import { OnChange } from 'react-final-form-listeners';
|
||||
import setFieldTouched from 'final-form-set-field-touched'
|
||||
|
@ -11,9 +9,9 @@ import Typography from '@material-ui/core/Typography';
|
|||
import { CountryDropdown, InputField, KnownHosts } from 'components';
|
||||
import { useReduxEffect } from 'hooks';
|
||||
import { ServerTypes } from 'store';
|
||||
import { FormKey } from 'types';
|
||||
|
||||
import './RegisterForm.css';
|
||||
import { useToast } from 'components/Toast';
|
||||
|
||||
const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
|
||||
const [emailRequired, setEmailRequired] = useState(false);
|
||||
|
@ -21,6 +19,7 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
|
|||
const [emailError, setEmailError] = useState(null);
|
||||
const [passwordError, setPasswordError] = useState(null);
|
||||
const [userNameError, setUserNameError] = useState(null);
|
||||
const { openToast } = useToast({ key: 'registration-success', children: 'Registration Successful!' })
|
||||
|
||||
const onHostChange = (host) => setEmailRequired(false);
|
||||
const onEmailChange = () => emailError && setEmailError(null);
|
||||
|
@ -35,6 +34,10 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
|
|||
setError(error);
|
||||
}, ServerTypes.REGISTRATION_FAILED);
|
||||
|
||||
useReduxEffect(() => {
|
||||
openToast()
|
||||
}, ServerTypes.REGISTRATION_SUCCES);
|
||||
|
||||
useReduxEffect(({ error }) => {
|
||||
setEmailError(error);
|
||||
}, ServerTypes.REGISTRATION_EMAIL_ERROR);
|
||||
|
|
|
@ -86,6 +86,9 @@ export const Actions = {
|
|||
registrationRequiresEmail: () => ({
|
||||
type: Types.REGISTRATION_REQUIRES_EMAIL,
|
||||
}),
|
||||
registrationSuccess: () => ({
|
||||
type: Types.REGISTRATION_SUCCES,
|
||||
}),
|
||||
registrationFailed: (error) => ({
|
||||
type: Types.REGISTRATION_FAILED,
|
||||
error
|
||||
|
|
|
@ -77,6 +77,9 @@ export const Dispatch = {
|
|||
registrationRequiresEmail: () => {
|
||||
store.dispatch(Actions.registrationRequiresEmail());
|
||||
},
|
||||
registrationSuccess: () => {
|
||||
store.dispatch(Actions.registrationSuccess())
|
||||
},
|
||||
registrationFailed: (error) => {
|
||||
store.dispatch(Actions.registrationFailed(error));
|
||||
},
|
||||
|
|
|
@ -21,6 +21,7 @@ export const Types = {
|
|||
VIEW_LOGS: '[Server] View Logs',
|
||||
CLEAR_LOGS: '[Server] Clear Logs',
|
||||
REGISTRATION_REQUIRES_EMAIL: '[Server] Registration Requires Email',
|
||||
REGISTRATION_SUCCES: '[Server] Registration Success',
|
||||
REGISTRATION_FAILED: '[Server] Registration Failed',
|
||||
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
|
||||
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
|
||||
|
|
|
@ -216,6 +216,7 @@ export class SessionCommands {
|
|||
webClient.protobuf.sendSessionCommand(sc, raw => {
|
||||
if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAccepted) {
|
||||
SessionCommands.login(passwordSalt);
|
||||
SessionPersistence.registrationSuccess()
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -105,6 +105,10 @@ export class SessionPersistence {
|
|||
ServerDispatch.registrationRequiresEmail();
|
||||
}
|
||||
|
||||
static registrationSuccess() {
|
||||
ServerDispatch.registrationSuccess();
|
||||
}
|
||||
|
||||
static registrationFailed(error: string) {
|
||||
ServerDispatch.registrationFailed(error);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue