Skip to content

Commit

Permalink
#4020 Differentiate warning toast from error toast style (#4081)
Browse files Browse the repository at this point in the history
* 4020 notify warning

* 4020 PR comments
  • Loading branch information
BALEHOK authored Aug 22, 2022
1 parent 25e1eeb commit 6bad05d
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 30 deletions.
2 changes: 1 addition & 1 deletion src/blocks/effects/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class AlertEffect extends Effect {
type: "string",
description:
"The alert type/style. 'window' uses the browser's native window alert dialog, which the user must dismiss",
enum: ["window", "info", "success", "error"],
enum: ["window", "info", "success", "warning", "error"],
default: "info",
},
duration: {
Expand Down
56 changes: 56 additions & 0 deletions src/utils/notify.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2022 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Button } from "react-bootstrap";
import { initToaster, NotificationType, showNotification } from "./notify";

const notificationTypes = ["info", "success", "error", "warning", "loading"];
initToaster();

const NotifyButton = ({ type }: { type: NotificationType }) => (
<Button
variant="primary"
onClick={() => {
showNotification({
message: type,
type,
});
}}
>
Show notification
</Button>
);

export default {
title: "Common/notify",
component: NotifyButton,
argTypes: {
type: {
options: notificationTypes,
control: { type: "select" },
},
},
} as ComponentMeta<typeof NotifyButton>;

export const Default: ComponentStory<typeof NotifyButton> = (args) => (
<NotifyButton {...args} />
);
Default.args = {
type: "info",
};
89 changes: 60 additions & 29 deletions src/utils/notify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,30 @@ import styles from "./notify.module.scss";

import React from "react";
import { render } from "react-dom";
import { toast, Toaster, DefaultToastOptions } from "react-hot-toast";
import {
toast,
Toaster,
DefaultToastOptions,
ToastOptions,
} from "react-hot-toast";
import { uuidv4 } from "@/types/helpers";
import { NOTIFICATIONS_Z_INDEX } from "@/common";
import reportError from "@/telemetry/reportError";
import { Except, RequireAtLeastOne } from "type-fest";
import { getErrorMessage } from "@/errors/errorHelpers";
import { truncate } from "lodash";
import { merge, truncate } from "lodash";
import { SIDEBAR_WIDTH_CSS_PROPERTY } from "@/contentScript/sidebarDomControllerLite";
import ErrorIcon from "@/icons/error.svg?loadAsComponent";
import WarningIcon from "@/icons/warning.svg?loadAsComponent";

const MINIMUM_NOTIFICATION_DURATION = 2000;

type NotificationType = "info" | "success" | "error" | "warning" | "loading";
export type NotificationType =
| "info"
| "success"
| "error"
| "warning"
| "loading";
type Notification = RequireAtLeastOne<
{
message: string;
Expand All @@ -47,26 +59,9 @@ type Notification = RequireAtLeastOne<

type SimpleNotification = string | Except<Notification, "type">;

const containerStyle: React.CSSProperties = {
zIndex: NOTIFICATIONS_Z_INDEX,
fontFamily: "sans-serif",
};

const toastOptions: DefaultToastOptions = {
// These colors match react-hot-toast’s status icons
success: {
style: {
border: "solid 2px #61d345",
},
},
error: {
style: {
border: "solid 2px #ff4b4b",
},
},
};
type ToastStyle = Partial<Record<NotificationType, ToastOptions>>;

const Message: React.VoidFunctionComponent<{
const Message: React.FunctionComponent<{
message: string;
id: string;
dismissable: boolean;
Expand All @@ -90,6 +85,31 @@ const Message: React.VoidFunctionComponent<{
</>
);

const getIcon = (
Icon: React.FC<React.SVGProps<SVGSVGElement>>,
color: string
) => <Icon style={{ height: 24, color, flex: "0 0 24px" }} />;

const toastStyle: ToastStyle = {
success: {
style: {
border: "solid 2px #61d345",
},
},
warning: {
icon: getIcon(WarningIcon, "#e89c00"),
style: {
border: "solid 2px #e89c00",
},
},
error: {
icon: getIcon(ErrorIcon, "#e84e2c"),
style: {
border: "solid 2px #e84e2c",
},
},
};

function getMessageDisplayTime(message: string): number {
const wpm = 100; // 180 is the average words read per minute, make it slower
return Math.max(
Expand All @@ -104,6 +124,18 @@ export function initToaster(): void {
root.setAttribute("style", "all: initial");

document.body.append(root);

const containerStyle: React.CSSProperties = {
zIndex: NOTIFICATIONS_Z_INDEX,
fontFamily: "sans-serif",
};

// Override for built-in toasts
const toastOptions: DefaultToastOptions = {
// These colors match react-hot-toast’s status icons
success: toastStyle.success,
error: toastStyle.error,
};
render(<Toaster {...{ containerStyle, toastOptions }} />, root);
}

Expand Down Expand Up @@ -132,13 +164,7 @@ export function showNotification({

duration ??= getMessageDisplayTime(message);

// TODO: Temporary style override until warnings are natively supported
// https://github.com/timolins/react-hot-toast/issues/29
if (type === "warning") {
type = "error";
}

const options = {
const options: ToastOptions = {
id,
duration,
// Keep the notification centered on the document even when the sidebar is open
Expand All @@ -154,6 +180,11 @@ export function showNotification({
toast[type](component, options);
break;

case "warning":
// eslint-disable-next-line security/detect-object-injection -- Filtered
toast(component, merge(options, toastStyle[type]));
break;

default:
toast(component, options);
}
Expand Down

0 comments on commit 6bad05d

Please sign in to comment.