From 131dd51c39b08c2235582d23deb53188a04e5d80 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Thu, 19 Nov 2020 10:00:26 -0500 Subject: [PATCH] feat: use react-toastify for notifications --- package.json | 1 + payload.d.ts | 1 - .../elements/DeleteDocument/index.js | 14 +-- .../elements/GenerateConfirmation/index.js | 8 +- src/admin/components/elements/Status/index.js | 101 ------------------ .../components/elements/Status/index.scss | 47 -------- .../components/elements/Status/reducer.js | 32 ------ src/admin/components/forms/Form/index.js | 56 ++-------- .../forms/field-types/Upload/Add/index.js | 1 - .../components/views/ForgotPassword/index.js | 9 +- .../views/collections/Edit/Auth/index.js | 13 +-- .../views/collections/Edit/Default.js | 3 - src/admin/index.js | 44 ++++---- src/admin/scss/app.scss | 1 + src/admin/scss/toastify.scss | 37 +++++++ yarn.lock | 16 ++- 16 files changed, 97 insertions(+), 287 deletions(-) delete mode 100644 src/admin/components/elements/Status/index.js delete mode 100644 src/admin/components/elements/Status/index.scss delete mode 100644 src/admin/components/elements/Status/reducer.js create mode 100644 src/admin/scss/toastify.scss diff --git a/package.json b/package.json index 170122d7d0..c7af9d8b42 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "react-router-navigation-prompt": "^1.8.11", "react-select": "^3.0.8", "react-simple-code-editor": "^0.11.0", + "react-toastify": "^6.1.0", "sanitize-filename": "^1.6.3", "sass": "^1.27.0", "sass-loader": "7.1.0", diff --git a/payload.d.ts b/payload.d.ts index c937b0ef2c..a74058b574 100644 --- a/payload.d.ts +++ b/payload.d.ts @@ -130,7 +130,6 @@ declare module "@payloadcms/payload/types" { admin?: { useAsTitle?: string; defaultColumns?: string[]; - disableScrollOnSuccess?: boolean; components?: any; }, hooks?: { diff --git a/src/admin/components/elements/DeleteDocument/index.js b/src/admin/components/elements/DeleteDocument/index.js index 8016e6290f..3fd192b7ae 100644 --- a/src/admin/components/elements/DeleteDocument/index.js +++ b/src/admin/components/elements/DeleteDocument/index.js @@ -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 ( diff --git a/src/admin/components/elements/GenerateConfirmation/index.js b/src/admin/components/elements/GenerateConfirmation/index.js index 9704d1c6b2..091da4f424 100644 --- a/src/admin/components/elements/GenerateConfirmation/index.js +++ b/src/admin/components/elements/GenerateConfirmation/index.js @@ -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); }; diff --git a/src/admin/components/elements/Status/index.js b/src/admin/components/elements/Status/index.js deleted file mode 100644 index b15c7be7c0..0000000000 --- a/src/admin/components/elements/Status/index.js +++ /dev/null @@ -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 ( - - {children} - - ); -}; - -StatusListProvider.propTypes = { - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - ]).isRequired, -}; - -const StatusList = () => { - const { statusList, removeStatus } = useStatusList(); - - if (statusList.length > 0) { - return ( - - ); - } - - return null; -}; - -export { - StatusListProvider, - useStatusList, -}; - -export default StatusList; diff --git a/src/admin/components/elements/Status/index.scss b/src/admin/components/elements/Status/index.scss deleted file mode 100644 index 883e1c94de..0000000000 --- a/src/admin/components/elements/Status/index.scss +++ /dev/null @@ -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); - } - } -} diff --git a/src/admin/components/elements/Status/reducer.js b/src/admin/components/elements/Status/reducer.js deleted file mode 100644 index 47a05ccbc5..0000000000 --- a/src/admin/components/elements/Status/reducer.js +++ /dev/null @@ -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; diff --git a/src/admin/components/forms/Form/index.js b/src/admin/components/forms/Form/index.js index 9cce82bcf5..66749b033a 100644 --- a/src/admin/components/forms/Form/index.js +++ b/src/admin/components/forms/Form/index.js @@ -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, diff --git a/src/admin/components/forms/field-types/Upload/Add/index.js b/src/admin/components/forms/field-types/Upload/Add/index.js index 6e1580e700..9e6bf941ae 100644 --- a/src/admin/components/forms/field-types/Upload/Add/index.js +++ b/src/admin/components/forms/field-types/Upload/Add/index.js @@ -45,7 +45,6 @@ const AddUploadModal = (props) => { action={`${serverURL}${api}/${collection.slug}`} onSuccess={onSuccess} disableSuccessStatus - disableScrollOnSuccess >

diff --git a/src/admin/components/views/ForgotPassword/index.js b/src/admin/components/views/ForgotPassword/index.js index e8a49dd4c0..fec30ddd63 100644 --- a/src/admin/components/views/ForgotPassword/index.js +++ b/src/admin/components/views/ForgotPassword/index.js @@ -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 ( -
{ 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]); diff --git a/src/admin/components/views/collections/Edit/Default.js b/src/admin/components/views/collections/Edit/Default.js index bd7ee36141..9c73c37eb5 100644 --- a/src/admin/components/views/collections/Edit/Default.js +++ b/src/admin/components/views/collections/Edit/Default.js @@ -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} >
{ }; return ( - - - - - - - + + + + + + + - - - - - - - - - + + + + + + + + + + ); }; diff --git a/src/admin/scss/app.scss b/src/admin/scss/app.scss index 1165ecfdaa..f9b53ead92 100644 --- a/src/admin/scss/app.scss +++ b/src/admin/scss/app.scss @@ -1,5 +1,6 @@ @import 'fonts'; @import 'styles'; +@import './toastify.scss'; :root { --breakpoint-xs-width : #{$breakpoint-xs-width}; diff --git a/src/admin/scss/toastify.scss b/src/admin/scss/toastify.scss new file mode 100644 index 0000000000..c6dd3a5fe7 --- /dev/null +++ b/src/admin/scss/toastify.scss @@ -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; + } +} diff --git a/yarn.lock b/yarn.lock index ca132aa016..69ad596d20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3171,6 +3171,11 @@ clone-deep@^2.0.1: kind-of "^6.0.0" shallow-clone "^1.0.0" +clsx@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -9911,7 +9916,16 @@ react-simple-code-editor@^0.11.0: resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.11.0.tgz#bb57c7c29b570f2ab229872599eac184f5bc673c" integrity sha512-xGfX7wAzspl113ocfKQAR8lWPhavGWHL3xSzNLeseDRHysT+jzRBi/ExdUqevSMos+7ZtdfeuBOXtgk9HTwsrw== -react-transition-group@^4.3.0: +react-toastify@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-6.1.0.tgz#32ee52477530b9024553586b9072a3326f7618dc" + integrity sha512-Ne+wIoO9A+jZlaqGqgeuXDC/DQfqTuJdyoc7G5SsuCHsr8mNRx7W26417YKtHRH0LcnFFd5ii76tGnmm0cMlLg== + dependencies: + clsx "^1.1.1" + prop-types "^15.7.2" + react-transition-group "^4.4.1" + +react-transition-group@^4.3.0, react-transition-group@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==