feat: use react-toastify for notifications
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useConfig } from '../../providers/Config';
|
||||
@@ -8,7 +9,6 @@ import MinimalTemplate from '../../templates/Minimal';
|
||||
import { useForm } from '../../forms/Form/context';
|
||||
import useTitle from '../../../hooks/useTitle';
|
||||
import { requests } from '../../../api';
|
||||
import { useStatusList } from '../Status';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -30,7 +30,6 @@ const DeleteDocument = (props) => {
|
||||
} = props;
|
||||
|
||||
const { serverURL, routes: { api, admin } } = useConfig();
|
||||
const { replaceStatus } = useStatusList();
|
||||
const { setModified } = useForm();
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const { closeAll, toggle } = useModal();
|
||||
@@ -41,11 +40,8 @@ const DeleteDocument = (props) => {
|
||||
const modalSlug = `delete-${id}`;
|
||||
|
||||
const addDefaultError = useCallback(() => {
|
||||
replaceStatus([{
|
||||
message: `There was an error while deleting ${title}. Please check your connection and try again.`,
|
||||
type: 'error',
|
||||
}]);
|
||||
}, [replaceStatus, title]);
|
||||
toast.error(`There was an error while deleting ${title}. Please check your connection and try again.`);
|
||||
}, [title]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
setDeleting(true);
|
||||
@@ -72,7 +68,7 @@ const DeleteDocument = (props) => {
|
||||
closeAll();
|
||||
|
||||
if (json.errors) {
|
||||
replaceStatus(json.errors);
|
||||
toast.error(json.errors);
|
||||
}
|
||||
addDefaultError();
|
||||
return false;
|
||||
@@ -80,7 +76,7 @@ const DeleteDocument = (props) => {
|
||||
return addDefaultError();
|
||||
}
|
||||
});
|
||||
}, [addDefaultError, closeAll, history, id, replaceStatus, singular, slug, title, admin, api, serverURL, setModified]);
|
||||
}, [addDefaultError, closeAll, history, id, singular, slug, title, admin, api, serverURL, setModified]);
|
||||
|
||||
if (id) {
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import Button from '../Button';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import { useStatusList } from '../Status';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -16,17 +16,13 @@ const GenerateConfirmation = (props) => {
|
||||
} = props;
|
||||
|
||||
const { toggle } = useModal();
|
||||
const { replaceStatus } = useStatusList();
|
||||
|
||||
const modalSlug = 'generate-confirmation';
|
||||
|
||||
const handleGenerate = () => {
|
||||
setKey();
|
||||
toggle(modalSlug);
|
||||
replaceStatus([{
|
||||
message: 'New API Key Generated.',
|
||||
type: 'success',
|
||||
}]);
|
||||
toast.success('New API Key Generated.', { autoClose: 3000 });
|
||||
highlightField(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import React, {
|
||||
useReducer, createContext, useContext, useEffect, useCallback,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import X from '../../icons/X';
|
||||
import reducer from './reducer';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'status-list';
|
||||
|
||||
const Context = createContext({});
|
||||
|
||||
const useStatusList = () => useContext(Context);
|
||||
|
||||
const StatusListProvider = ({ children }) => {
|
||||
const [statusList, dispatchStatus] = useReducer(reducer, []);
|
||||
const { pathname, state } = useLocation();
|
||||
|
||||
const removeStatus = useCallback((i) => dispatchStatus({ type: 'REMOVE', payload: i }), []);
|
||||
const addStatus = useCallback((status) => dispatchStatus({ type: 'ADD', payload: status }), []);
|
||||
const clearStatus = useCallback(() => dispatchStatus({ type: 'CLEAR' }), []);
|
||||
const replaceStatus = useCallback((status) => dispatchStatus({ type: 'REPLACE', payload: status }), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (state && state.status) {
|
||||
if (Array.isArray(state.status)) {
|
||||
replaceStatus(state.status);
|
||||
} else {
|
||||
replaceStatus([state.status]);
|
||||
}
|
||||
} else {
|
||||
clearStatus();
|
||||
}
|
||||
}, [addStatus, replaceStatus, clearStatus, state, pathname]);
|
||||
|
||||
return (
|
||||
<Context.Provider value={{
|
||||
statusList,
|
||||
removeStatus,
|
||||
addStatus,
|
||||
clearStatus,
|
||||
replaceStatus,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
StatusListProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
const StatusList = () => {
|
||||
const { statusList, removeStatus } = useStatusList();
|
||||
|
||||
if (statusList.length > 0) {
|
||||
return (
|
||||
<ul className={baseClass}>
|
||||
{statusList.map((status, i) => {
|
||||
const classes = [
|
||||
`${baseClass}__status`,
|
||||
`${baseClass}__status--${status.type}`,
|
||||
].join(' ');
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classes}
|
||||
key={i}
|
||||
>
|
||||
{status.message}
|
||||
<button
|
||||
type="button"
|
||||
className="close"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
removeStatus(i);
|
||||
}}
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export {
|
||||
StatusListProvider,
|
||||
useStatusList,
|
||||
};
|
||||
|
||||
export default StatusList;
|
||||
@@ -1,47 +0,0 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.status-list {
|
||||
position: relative;
|
||||
z-index: $z-status;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
background: $color-green;
|
||||
color: $color-dark-gray;
|
||||
padding: base(.5) $baseline;
|
||||
margin-bottom: 1px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
button {
|
||||
@extend %btn-reset;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: base(1);
|
||||
height: base(1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
li.status-list__status--error {
|
||||
background: $color-red;
|
||||
color: white;
|
||||
|
||||
button svg {
|
||||
@include color-svg(white);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
const statusReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'ADD': {
|
||||
const newState = [
|
||||
...state,
|
||||
action.payload,
|
||||
];
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
|
||||
case 'REMOVE': {
|
||||
const statusList = [...state];
|
||||
statusList.splice(action.payload, 1);
|
||||
return statusList;
|
||||
}
|
||||
|
||||
case 'CLEAR': {
|
||||
return [];
|
||||
}
|
||||
|
||||
case 'REPLACE': {
|
||||
return action.payload;
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default statusReducer;
|
||||
@@ -4,8 +4,8 @@ import React, {
|
||||
import { objectToFormData } from 'object-to-formdata';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import { useStatusList } from '../../elements/Status';
|
||||
import { requests } from '../../../api';
|
||||
import useThrottledEffect from '../../../hooks/useThrottledEffect';
|
||||
import { useAuth } from '../../providers/Authentication';
|
||||
@@ -38,14 +38,12 @@ const Form = (props) => {
|
||||
disableSuccessStatus,
|
||||
initialState, // fully formed initial field state
|
||||
initialData, // values only, paths are required as key - form should build initial state as convenience
|
||||
disableScrollOnSuccess,
|
||||
waitForAutocomplete,
|
||||
log,
|
||||
} = props;
|
||||
|
||||
const history = useHistory();
|
||||
const locale = useLocale();
|
||||
const { replaceStatus, addStatus, clearStatus } = useStatusList();
|
||||
const { refreshCookie } = useAuth();
|
||||
|
||||
const [modified, setModified] = useState(false);
|
||||
@@ -111,17 +109,7 @@ const Form = (props) => {
|
||||
|
||||
// If not valid, prevent submission
|
||||
if (!isValid) {
|
||||
addStatus({
|
||||
message: 'Please correct the fields below.',
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
if (!disableScrollOnSuccess) {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
toast.error('Please correct invalid fields.');
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -131,13 +119,6 @@ const Form = (props) => {
|
||||
return onSubmit(fields, reduceFieldsToValues(fields));
|
||||
}
|
||||
|
||||
if (!disableScrollOnSuccess) {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
const formData = contextRef.current.createFormData();
|
||||
|
||||
try {
|
||||
@@ -151,7 +132,6 @@ const Form = (props) => {
|
||||
|
||||
|
||||
setProcessing(false);
|
||||
clearStatus();
|
||||
|
||||
const contentType = res.headers.get('content-type');
|
||||
const isJSON = contentType && contentType.indexOf('application/json') !== -1;
|
||||
@@ -182,20 +162,13 @@ const Form = (props) => {
|
||||
|
||||
history.push(destination);
|
||||
} else if (!disableSuccessStatus) {
|
||||
replaceStatus([{
|
||||
message: json.message || 'Submission successful.',
|
||||
type: 'success',
|
||||
disappear: 3000,
|
||||
}]);
|
||||
toast.success(json.message || 'Submission successful.', { autoClose: 3000 });
|
||||
}
|
||||
} else {
|
||||
contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form
|
||||
|
||||
if (json.message) {
|
||||
addStatus({
|
||||
message: json.message,
|
||||
type: 'error',
|
||||
});
|
||||
toast.error(json.message);
|
||||
|
||||
return json;
|
||||
}
|
||||
@@ -218,10 +191,7 @@ const Form = (props) => {
|
||||
});
|
||||
|
||||
nonFieldErrors.forEach((err) => {
|
||||
addStatus({
|
||||
message: err.message || 'An unknown error occurred.',
|
||||
type: 'error',
|
||||
});
|
||||
toast.error(err.message || 'An unknown error occurred.');
|
||||
});
|
||||
|
||||
return json;
|
||||
@@ -229,25 +199,17 @@ const Form = (props) => {
|
||||
|
||||
const message = errorMessages[res.status] || 'An unknown error occurrred.';
|
||||
|
||||
addStatus({
|
||||
message,
|
||||
type: 'error',
|
||||
});
|
||||
toast.error(message);
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (err) {
|
||||
setProcessing(false);
|
||||
|
||||
return addStatus({
|
||||
message: err,
|
||||
type: 'error',
|
||||
});
|
||||
toast.error(err);
|
||||
}
|
||||
}, [
|
||||
action,
|
||||
addStatus,
|
||||
clearStatus,
|
||||
disableSuccessStatus,
|
||||
disabled,
|
||||
fields,
|
||||
@@ -257,8 +219,6 @@ const Form = (props) => {
|
||||
onSubmit,
|
||||
onSuccess,
|
||||
redirect,
|
||||
replaceStatus,
|
||||
disableScrollOnSuccess,
|
||||
waitForAutocomplete,
|
||||
]);
|
||||
|
||||
@@ -366,7 +326,6 @@ Form.defaultProps = {
|
||||
disableSuccessStatus: false,
|
||||
disabled: false,
|
||||
initialState: undefined,
|
||||
disableScrollOnSuccess: false,
|
||||
waitForAutocomplete: false,
|
||||
initialData: undefined,
|
||||
log: false,
|
||||
@@ -387,7 +346,6 @@ Form.propTypes = {
|
||||
redirect: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
initialState: PropTypes.shape({}),
|
||||
disableScrollOnSuccess: PropTypes.bool,
|
||||
waitForAutocomplete: PropTypes.bool,
|
||||
initialData: PropTypes.shape({}),
|
||||
log: PropTypes.bool,
|
||||
|
||||
@@ -45,7 +45,6 @@ const AddUploadModal = (props) => {
|
||||
action={`${serverURL}${api}/${collection.slug}`}
|
||||
onSuccess={onSuccess}
|
||||
disableSuccessStatus
|
||||
disableScrollOnSuccess
|
||||
>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h1>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useConfig } from '../../providers/Config';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import StatusList, { useStatusList } from '../../elements/Status';
|
||||
import Form from '../../forms/Form';
|
||||
import Email from '../../forms/field-types/Email';
|
||||
import FormSubmit from '../../forms/Submit';
|
||||
@@ -15,7 +15,6 @@ import './index.scss';
|
||||
const baseClass = 'forgot-password';
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const { addStatus } = useStatusList();
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
@@ -32,10 +31,7 @@ const ForgotPassword = () => {
|
||||
.then(() => {
|
||||
setHasSubmitted(true);
|
||||
}, () => {
|
||||
addStatus({
|
||||
type: 'error',
|
||||
message: 'The email provided is not valid.',
|
||||
});
|
||||
toast.error('The email provided is not valid.');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -81,7 +77,6 @@ const ForgotPassword = () => {
|
||||
|
||||
return (
|
||||
<MinimalTemplate className={baseClass}>
|
||||
<StatusList />
|
||||
<Form
|
||||
novalidate
|
||||
handleResponse={handleResponse}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toast } from 'react-toastify';
|
||||
import Email from '../../../../forms/field-types/Email';
|
||||
import Password from '../../../../forms/field-types/Password';
|
||||
import Checkbox from '../../../../forms/field-types/Checkbox';
|
||||
@@ -7,7 +8,6 @@ import Button from '../../../../elements/Button';
|
||||
import ConfirmPassword from '../../../../forms/field-types/ConfirmPassword';
|
||||
import { useFormFields, useFormModified } from '../../../../forms/Form/context';
|
||||
import { useConfig } from '../../../../providers/Config';
|
||||
import { useStatusList } from '../../../../elements/Status';
|
||||
|
||||
import APIKey from './APIKey';
|
||||
|
||||
@@ -20,7 +20,6 @@ const Auth = (props) => {
|
||||
const [changingPassword, setChangingPassword] = useState(requirePassword);
|
||||
const { getField } = useFormFields();
|
||||
const modified = useFormModified();
|
||||
const { replaceStatus } = useStatusList();
|
||||
|
||||
const enableAPIKey = getField('enableAPIKey');
|
||||
|
||||
@@ -50,15 +49,9 @@ const Auth = (props) => {
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
replaceStatus([{
|
||||
message: 'Successfully unlocked',
|
||||
type: 'success',
|
||||
}]);
|
||||
toast.success('Successfully unlocked', { autoClose: 3000 });
|
||||
} else {
|
||||
replaceStatus([{
|
||||
message: 'Unable to unlock',
|
||||
type: 'error',
|
||||
}]);
|
||||
toast.error('Successfully unlocked');
|
||||
}
|
||||
}, [replaceStatus, serverURL, api, slug, email]);
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ const DefaultEditView = (props) => {
|
||||
admin: {
|
||||
useAsTitle,
|
||||
disableDuplicate,
|
||||
disableScrollOnSuccess,
|
||||
},
|
||||
timestamps,
|
||||
preview,
|
||||
@@ -68,7 +67,6 @@ const DefaultEditView = (props) => {
|
||||
onSuccess={onSave}
|
||||
disabled={!hasSavePermission}
|
||||
initialState={initialState}
|
||||
disableScrollOnSuccess={disableScrollOnSuccess}
|
||||
>
|
||||
<div className={`${baseClass}__main`}>
|
||||
<Meta
|
||||
@@ -232,7 +230,6 @@ DefaultEditView.propTypes = {
|
||||
admin: PropTypes.shape({
|
||||
useAsTitle: PropTypes.string,
|
||||
disableDuplicate: PropTypes.bool,
|
||||
disableScrollOnSuccess: PropTypes.bool,
|
||||
}),
|
||||
fields: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
preview: PropTypes.func,
|
||||
|
||||
@@ -4,9 +4,9 @@ import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { ScrollInfoProvider } from '@faceless-ui/scroll-info';
|
||||
import { WindowInfoProvider } from '@faceless-ui/window-info';
|
||||
import { ModalProvider, ModalContainer } from '@faceless-ui/modal';
|
||||
import { ToastContainer, Slide } from 'react-toastify';
|
||||
import { SearchParamsProvider } from './components/utilities/SearchParams';
|
||||
import { LocaleProvider } from './components/utilities/Locale';
|
||||
import StatusList, { StatusListProvider } from './components/elements/Status';
|
||||
import { AuthenticationProvider } from './components/providers/Authentication';
|
||||
import Routes from './components/Routes';
|
||||
import getCSSVariable from '../utilities/getCSSVariable';
|
||||
@@ -25,30 +25,34 @@ const Index = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<WindowInfoProvider {...windowInfoProps}>
|
||||
<ScrollInfoProvider>
|
||||
<Router>
|
||||
<ModalProvider
|
||||
classPrefix="payload"
|
||||
zIndex={parseInt(getCSSVariable('z-modal'), 10)}
|
||||
>
|
||||
<AuthenticationProvider>
|
||||
<StatusListProvider>
|
||||
<React.Fragment>
|
||||
<ConfigProvider>
|
||||
<WindowInfoProvider {...windowInfoProps}>
|
||||
<ScrollInfoProvider>
|
||||
<Router>
|
||||
<ModalProvider
|
||||
classPrefix="payload"
|
||||
zIndex={parseInt(getCSSVariable('z-modal'), 10)}
|
||||
>
|
||||
<AuthenticationProvider>
|
||||
<SearchParamsProvider>
|
||||
<LocaleProvider>
|
||||
<StatusList />
|
||||
<Routes />
|
||||
</LocaleProvider>
|
||||
</SearchParamsProvider>
|
||||
</StatusListProvider>
|
||||
<ModalContainer />
|
||||
</AuthenticationProvider>
|
||||
</ModalProvider>
|
||||
</Router>
|
||||
</ScrollInfoProvider>
|
||||
</WindowInfoProvider>
|
||||
</ConfigProvider>
|
||||
<ModalContainer />
|
||||
</AuthenticationProvider>
|
||||
</ModalProvider>
|
||||
</Router>
|
||||
</ScrollInfoProvider>
|
||||
</WindowInfoProvider>
|
||||
</ConfigProvider>
|
||||
<ToastContainer
|
||||
position="bottom-center"
|
||||
transition={Slide}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import 'fonts';
|
||||
@import 'styles';
|
||||
@import './toastify.scss';
|
||||
|
||||
:root {
|
||||
--breakpoint-xs-width : #{$breakpoint-xs-width};
|
||||
|
||||
37
src/admin/scss/toastify.scss
Normal file
37
src/admin/scss/toastify.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
@import 'vars';
|
||||
@import '~react-toastify/dist/ReactToastify.css';
|
||||
|
||||
.Toastify {
|
||||
.Toastify__toast-container {
|
||||
left: base(5);
|
||||
transform: none;
|
||||
right: base(5);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.Toastify__toast {
|
||||
padding: base(.5);
|
||||
border-radius: $style-radius-m;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.Toastify__close-button {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.Toastify__toast--success {
|
||||
color: $color-dark-gray;
|
||||
}
|
||||
|
||||
.Toastify__close-button--success {
|
||||
color: $color-dark-gray;
|
||||
}
|
||||
|
||||
.Toastify__toast--success {
|
||||
background: $color-green;
|
||||
}
|
||||
|
||||
.Toastify__toast--error {
|
||||
background: $color-red;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user