Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add useToast hook for toast notifications #3593

Merged
merged 9 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions editor.planx.uk/src/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Alert from "@mui/material/Alert";
import Snackbar from "@mui/material/Snackbar";
import { useToast } from "hooks/useToast";
import React from "react";

import { Toast as ToastProps } from "./types";

const Toast = ({ message, type = "success", id }: ToastProps) => {
const toast = useToast();

const handleCloseToast = () => {
if (toast) {
toast.remove(id);
} else {
console.warn("ToastContext is not provided.");
}
};

return (
<Snackbar onClose={handleCloseToast} autoHideDuration={6000} open={true}>
<Alert onClose={handleCloseToast} severity={type} sx={{ width: "100%" }}>
{message}
</Alert>
</Snackbar>
);
};

export default Toast;
16 changes: 16 additions & 0 deletions editor.planx.uk/src/components/Toast/ToastContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";

import Toast from "./Toast";
import { Toast as ToastComponent } from "./types";

const ToastContainer = ({ toasts }: { toasts: ToastComponent[] }) => {
return (
<div className="toasts-container">
{toasts.map((toast) => (
<Toast key={toast.id} {...toast} />
))}
</div>
);
};

export default ToastContainer;
31 changes: 31 additions & 0 deletions editor.planx.uk/src/components/Toast/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export interface Toast {
message: string;
type: ToastType;
id: number;
}
export type ToastType = "success" | "warning" | "info" | "error";

export type ToastState = {
toasts: Toast[];
};

export type ToastAction = AddToast | DeleteToast;

type AddToast = {
type: "ADD_TOAST";
payload: Toast;
};

type DeleteToast = {
type: "DELETE_TOAST";
payload: { id: number };
};

export type ToastContextType = {
addToast: (type: ToastType, message: string) => void;
remove: (id: number) => void;
success: (message: string) => void;
warning: (message: string) => void;
info: (message: string) => void;
error: (message: string) => void;
};
70 changes: 70 additions & 0 deletions editor.planx.uk/src/contexts/ToastContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import ToastContainer from "components/Toast/ToastContainer";
import {
ToastContextType,
ToastState,
ToastType,
} from "components/Toast/types";
import React, { createContext, ReactNode, useReducer } from "react";
import { toastReducer } from "reducers/toastReducer";

const defaultCreateContextValue = {
remove: (_id: number) => {},
addToast: (_type: ToastType, _message: string) => {},
success: (_message: string) => {},
warning: (_message: string) => {},
info: (_message: string) => {},
error: (_message: string) => {},
};

export const ToastContext = createContext<ToastContextType>(
defaultCreateContextValue,
);

const initialState: ToastState = {
toasts: [],
};

export const ToastContextProvider = ({
children,
}: Readonly<{ children: ReactNode }>) => {
const [state, dispatch] = useReducer(toastReducer, initialState);
const addToast = (type: ToastType, message: string) => {
const id = Math.floor(Math.random() * 10_000_000);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we also have the uuid package available on the frontend which would let you generate a unique uuid id like const id = uuidV4()(consistent with how we generate session ids) - happy for this to work either way though / chances of unintentionally colliding/non-unique IDs seems veryyy low here 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice tip thanks! I'll include that in the next PR 👍

dispatch({ type: "ADD_TOAST", payload: { id, message, type } });
};
const remove = (id: number) => {
dispatch({ type: "DELETE_TOAST", payload: { id } });
};
const success = (message: string) => {
addToast("success", message);
};

const warning = (message: string) => {
addToast("warning", message);
};

const info = (message: string) => {
addToast("info", message);
};

const error = (message: string) => {
addToast("error", message);
};

const value: ToastContextType = {
success,
warning,
info,
error,
remove,
addToast,
};

return (
<ToastContext.Provider value={value}>
<ToastContainer toasts={state.toasts} />
{children}
</ToastContext.Provider>
);
};
5 changes: 5 additions & 0 deletions editor.planx.uk/src/hooks/useToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useContext } from "react";

import { ToastContext } from "../contexts/ToastContext";

export const useToast = () => useContext(ToastContext);
5 changes: 3 additions & 2 deletions editor.planx.uk/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ApolloProvider } from "@apollo/client";
import CssBaseline from "@mui/material/CssBaseline";
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles";
import { MyMap } from "@opensystemslab/map";
import { ToastContextProvider } from "contexts/ToastContext";
import { getCookie, setCookie } from "lib/cookie";
import ErrorPage from "pages/ErrorPage";
import { AnalyticsProvider } from "pages/FlowEditor/lib/analytics/provider";
Expand Down Expand Up @@ -93,7 +94,7 @@ const Layout: React.FC<{
};

root.render(
<>
<ToastContextProvider>
<ApolloProvider client={client}>
<AnalyticsProvider>
<Router context={{ currentUser: hasJWT() }} navigation={navigation}>
Expand All @@ -109,5 +110,5 @@ root.render(
</AnalyticsProvider>
</ApolloProvider>
<ToastContainer icon={false} theme="colored" />
</>,
</ToastContextProvider>,
);
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import Snackbar from "@mui/material/Snackbar";
import { styled } from "@mui/material/styles";
import Typography from "@mui/material/Typography";
import { TeamTheme } from "@opensystemslab/planx-core/types";
import { FormikConfig } from "formik";
import { useToast } from "hooks/useToast";
import { useStore } from "pages/FlowEditor/lib/store";
import React, { useEffect, useState } from "react";
import SettingsSection from "ui/editor/SettingsSection";
Expand Down Expand Up @@ -34,7 +33,7 @@ const DesignSettings: React.FC = () => {
const [formikConfig, setFormikConfig] = useState<
FormikConfig<TeamTheme> | undefined
>(undefined);

const toast = useToast();
/**
* Fetch current team and setup shared form config
*/
Expand All @@ -60,20 +59,7 @@ const DesignSettings: React.FC = () => {
fetchTeam();
}, []);

const [open, setOpen] = useState(false);

const handleClose = (
_event?: React.SyntheticEvent | Event,
reason?: string,
) => {
if (reason === "clickaway") {
return;
}

setOpen(false);
};

const onSuccess = () => setOpen(true);
const onSuccess = () => toast.success("Theme updated successfully");

return (
<Container maxWidth="formWrap">
Expand All @@ -93,11 +79,6 @@ const DesignSettings: React.FC = () => {
<FaviconForm formikConfig={formikConfig} onSuccess={onSuccess} />
</>
)}
<Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="success" sx={{ width: "100%" }}>
Theme updated successfully
</Alert>
</Snackbar>
</Container>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import Alert from "@mui/material/Alert";
import Container from "@mui/material/Container";
import Snackbar from "@mui/material/Snackbar";
import Typography from "@mui/material/Typography";
import { TeamSettings } from "@opensystemslab/planx-core/types";
import { FormikConfig } from "formik";
import { useToast } from "hooks/useToast";
import { useStore } from "pages/FlowEditor/lib/store";
import React, { useEffect, useState } from "react";
import SettingsSection from "ui/editor/SettingsSection";
Expand All @@ -22,6 +21,7 @@ const GeneralSettings: React.FC = () => {
const [formikConfig, setFormikConfig] = useState<
FormikConfig<TeamSettings> | undefined
>(undefined);
const toast = useToast();

useEffect(() => {
const fetchTeam = async () => {
Expand All @@ -44,21 +44,7 @@ const GeneralSettings: React.FC = () => {
fetchTeam();
}, []);

const [open, setOpen] = useState(false);
const [updateMessage, _setUpdateMessage] = useState("Setting Updated");

const handleClose = (
_event?: React.SyntheticEvent | Event,
reason?: string,
) => {
if (reason === "clickaway") {
return;
}

setOpen(false);
};

const onSuccess = () => setOpen(true);
const onSuccess = () => toast.success("Setting Updated");

return (
<Container maxWidth="formWrap">
Expand All @@ -81,11 +67,6 @@ const GeneralSettings: React.FC = () => {
<SubmissionsForm formikConfig={formikConfig} onSuccess={onSuccess} />
</>
)}
<Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="success" sx={{ width: "100%" }}>
{updateMessage}
</Alert>
</Snackbar>
</Container>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Container from "@mui/material/Container";
import FormControlLabel, {
formControlLabelClasses,
} from "@mui/material/FormControlLabel";
import Link from "@mui/material/Link";
import Snackbar from "@mui/material/Snackbar";
import Switch, { SwitchProps } from "@mui/material/Switch";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { FlowStatus } from "@opensystemslab/planx-core/types";
import axios from "axios";
import { useFormik } from "formik";
import { useToast } from "hooks/useToast";
import React, { useState } from "react";
import { rootFlowPath } from "routes/utils";
import { FONT_WEIGHT_BOLD } from "theme";
Expand Down Expand Up @@ -228,19 +227,7 @@ const ServiceSettings: React.FC = () => {
state.teamDomain,
state.isFlowPublished,
]);

const [isAlertOpen, setIsAlertOpen] = useState(false);

const handleClose = (
_event?: React.SyntheticEvent | Event,
reason?: string,
) => {
if (reason === "clickaway") {
return;
}

setIsAlertOpen(false);
};
const toast = useToast();

const sendFlowStatusSlackNotification = async (status: FlowStatus) => {
const skipTeamSlugs = [
Expand Down Expand Up @@ -294,7 +281,7 @@ const ServiceSettings: React.FC = () => {
},
onSubmit: async (values) => {
await updateFlowSettings(values);
setIsAlertOpen(true);
toast.success("Service settings updated successfully");
},
validate: () => {},
});
Expand All @@ -306,7 +293,7 @@ const ServiceSettings: React.FC = () => {
onSubmit: async (values, { resetForm }) => {
const isSuccess = await updateFlowStatus(values.status);
if (isSuccess) {
setIsAlertOpen(true);
toast.success("Service settings updated successfully");
// Send a Slack notification to #planx-notifications
sendFlowStatusSlackNotification(values.status);
// Reset "dirty" status to disable Save & Reset buttons
Expand Down Expand Up @@ -487,15 +474,6 @@ const ServiceSettings: React.FC = () => {
</Box>
</SettingsSection>
</Box>
<Snackbar
open={isAlertOpen}
autoHideDuration={6000}
onClose={handleClose}
>
<Alert onClose={handleClose} severity="success" sx={{ width: "100%" }}>
Service settings updated successfully
</Alert>
</Snackbar>
</Container>
);
};
Expand Down
Loading
Loading