Skip to content

Commit f0a64f6

Browse files
Merge pull request #668 from zendesk/kzachradnik/PDSC-9-Overlapping-notifications-from-Help-Center-and-Copenhagen-Theme
Kzachradnik/pdsc 9 overlapping notifications in copenhagen theme
2 parents 5c6188d + 3033578 commit f0a64f6

File tree

22 files changed

+366
-98
lines changed

22 files changed

+366
-98
lines changed

src/modules/approval-requests/components/approval-request/ApproverActions.test.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import type { ReactElement } from "react";
33
import { userEvent } from "@testing-library/user-event";
44
import { ThemeProvider } from "@zendeskgarden/react-theming";
55
import ApproverActions from "./ApproverActions";
6+
import { notify } from "../../../shared";
67

7-
const mockNotify = jest.fn();
8-
jest.mock("../../../shared/notifications/useNotify", () => ({
9-
useNotify: () => mockNotify,
8+
jest.mock("../../../shared/notifications", () => ({
9+
notify: jest.fn(),
1010
}));
1111

12+
const mockNotify = notify as jest.Mock;
13+
1214
const mockSubmitApprovalDecision = jest.fn();
1315
jest.mock("../../submitApprovalDecision", () => ({
1416
submitApprovalDecision: (...args: unknown[]) =>

src/modules/approval-requests/components/approval-request/ApproverActions.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { useTranslation } from "react-i18next";
44
import { Button } from "@zendeskgarden/react-buttons";
55
import { Field, Label, Message, Textarea } from "@zendeskgarden/react-forms";
66
import { Avatar } from "@zendeskgarden/react-avatars";
7-
import { useNotify } from "../../../shared/notifications/useNotify";
87
import { submitApprovalDecision } from "../../submitApprovalDecision";
98
import type { ApprovalDecision } from "../../submitApprovalDecision";
109
import type { ApprovalRequest } from "../../types";
1110
import { APPROVAL_REQUEST_STATES } from "../../constants";
11+
import { notify } from "../../../shared";
1212

1313
const PENDING_APPROVAL_STATUS = {
1414
APPROVED: "APPROVED",
@@ -69,7 +69,6 @@ function ApproverActions({
6969
assigneeUser,
7070
}: ApproverActionsProps) {
7171
const { t } = useTranslation();
72-
const notify = useNotify();
7372
const [comment, setComment] = useState("");
7473
const [pendingStatus, setPendingStatus] = useState<
7574
| (typeof PENDING_APPROVAL_STATUS)[keyof typeof PENDING_APPROVAL_STATUS]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { GlobalNotificationsRoot } from "../shared/notifications/GlobalNotificationsRoot";
3+
import { FLASH_NOTIFICATIONS_KEY } from "../shared";
4+
import type { ToastNotification } from "../shared";
5+
6+
describe("FlashNotifications (via GlobalNotificationsRoot)", () => {
7+
beforeEach(() => {
8+
sessionStorage.clear();
9+
jest.restoreAllMocks();
10+
});
11+
12+
it("reads flash notifications from sessionStorage and displays them", async () => {
13+
const payloads: ToastNotification[] = [
14+
{ title: "Hello", message: "World", type: "success" },
15+
{ title: "Second", message: "Message", type: "info" },
16+
];
17+
sessionStorage.setItem(FLASH_NOTIFICATIONS_KEY, JSON.stringify(payloads));
18+
19+
render(<GlobalNotificationsRoot />);
20+
21+
expect(await screen.findByText("Hello")).toBeInTheDocument();
22+
expect(await screen.findByText("Second")).toBeInTheDocument();
23+
expect(await screen.findByText("World")).toBeInTheDocument();
24+
expect(await screen.findByText("Message")).toBeInTheDocument();
25+
expect(sessionStorage.getItem(FLASH_NOTIFICATIONS_KEY)).toBeNull();
26+
});
27+
28+
it("logs an error if sessionStorage contains invalid JSON", () => {
29+
sessionStorage.setItem(FLASH_NOTIFICATIONS_KEY, "INVALID");
30+
31+
const spy = jest.spyOn(console, "error").mockImplementation(() => {});
32+
33+
render(<GlobalNotificationsRoot />);
34+
35+
expect(spy).toHaveBeenCalledWith(
36+
"Cannot parse flash notifications",
37+
expect.any(SyntaxError)
38+
);
39+
});
40+
41+
it("does nothing if no flash notifications are in sessionStorage", () => {
42+
const spy = jest.spyOn(console, "error").mockImplementation(() => {});
43+
44+
render(<GlobalNotificationsRoot />);
45+
46+
expect(spy).not.toHaveBeenCalled();
47+
expect(screen.queryByRole("alert")).toBeNull();
48+
});
49+
});
Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import type { ReactElement } from "react";
22
import { useEffect } from "react";
3-
import type { ToastNotification } from "../shared/notifications/ToastNotification";
4-
import { useNotify } from "../shared/notifications/useNotify";
3+
import type { ToastNotification } from "../shared";
4+
import { notify, FLASH_NOTIFICATIONS_KEY } from "../shared";
55

6-
interface FlashNotificationsProps {
7-
notifications: ToastNotification[];
8-
}
6+
export function FlashNotifications(): ReactElement {
7+
useEffect(() => {
8+
const raw = window.sessionStorage.getItem(FLASH_NOTIFICATIONS_KEY);
9+
if (!raw) {
10+
return;
11+
}
912

10-
export function FlashNotifications({
11-
notifications,
12-
}: FlashNotificationsProps): ReactElement {
13-
const notify = useNotify();
13+
try {
14+
const parsedNotifications = JSON.parse(
15+
raw || "[]"
16+
) as ToastNotification[];
17+
for (const notification of parsedNotifications) {
18+
notify(notification);
19+
}
1420

15-
useEffect(() => {
16-
for (const notification of notifications) {
17-
notify(notification);
21+
window.sessionStorage.removeItem(FLASH_NOTIFICATIONS_KEY);
22+
} catch (e) {
23+
console.error("Cannot parse flash notifications", e);
1824
}
19-
}, [notifications, notify]);
25+
}, []);
2026

2127
return <></>;
2228
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { cleanup, screen } from "@testing-library/react";
2+
import { renderFlashNotifications } from "./renderFlashNotifications";
3+
import { emitNotify } from "../shared/notifications/notifyBus";
4+
import type { Settings, ToastNotification } from "../shared";
5+
6+
afterEach(() => {
7+
cleanup();
8+
document.body.innerHTML = "";
9+
});
10+
11+
describe("renderFlashNotifications", () => {
12+
it("creates a container div and mounts provider", async () => {
13+
const settings = {} as Settings;
14+
const baseLocale = "en";
15+
16+
await renderFlashNotifications(settings, baseLocale);
17+
18+
const container: HTMLDivElement | null = document.body.querySelector("div");
19+
expect(container).not.toBeNull();
20+
});
21+
22+
it("renders toast when emitNotify is called after initialization", async () => {
23+
const settings = {} as Settings;
24+
const baseLocale = "en";
25+
26+
await renderFlashNotifications(settings, baseLocale);
27+
28+
const testId = "toast-test";
29+
const notification: ToastNotification = {
30+
type: "success",
31+
title: "Test",
32+
message: <div data-testid={testId} />,
33+
};
34+
35+
emitNotify(notification);
36+
37+
expect(await screen.findByTestId(testId)).toBeInTheDocument();
38+
});
39+
});

src/modules/flash-notifications/renderFlashNotifications.tsx

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,33 @@ import { render } from "react-dom";
22
import {
33
ThemeProviders,
44
createTheme,
5-
FLASH_NOTIFICATIONS_KEY,
65
initI18next,
76
loadTranslations,
87
} from "../shared";
9-
import type { Settings, ToastNotification } from "../shared";
10-
import { FlashNotifications } from "./FlashNotifications";
8+
import type { Settings } from "../shared";
9+
import { GlobalNotificationsRoot } from "../shared/notifications/GlobalNotificationsRoot";
10+
11+
/**
12+
* Note: Historically named "flash notifications" after Rails flash messages.
13+
* This function now renders all notifications, not only flash ones.
14+
* The name is kept for backward compatibility with document_head.hbs.
15+
*/
1116

1217
export async function renderFlashNotifications(
1318
settings: Settings,
1419
baseLocale: string
1520
) {
16-
const flashNotifications = window.sessionStorage.getItem(
17-
FLASH_NOTIFICATIONS_KEY
18-
);
19-
20-
if (flashNotifications === null) {
21-
return;
22-
}
23-
2421
initI18next(baseLocale);
2522
await loadTranslations(baseLocale, [
2623
() => import(`../shared/translations/locales/${baseLocale}.json`),
2724
]);
2825

29-
window.sessionStorage.removeItem(FLASH_NOTIFICATIONS_KEY);
30-
3126
try {
32-
const parsedNotifications = JSON.parse(
33-
flashNotifications
34-
) as ToastNotification[];
35-
3627
const container = document.createElement("div");
3728
document.body.appendChild(container);
38-
3929
render(
4030
<ThemeProviders theme={createTheme(settings)}>
41-
<FlashNotifications notifications={parsedNotifications} />
31+
<GlobalNotificationsRoot />
4232
</ThemeProviders>,
4333
container
4434
);

src/modules/new-request-form/fields/attachments/Attachments.tsx

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { AttachmentField } from "../../data-types";
1313
import { FileListItem } from "./FileListItem";
1414
import type { AttachedFile } from "./useAttachedFiles";
1515
import { useAttachedFiles } from "./useAttachedFiles";
16-
import { useNotify } from "../../../shared/notifications/useNotify";
16+
import { notify } from "../../../shared";
1717

1818
interface AttachmentProps {
1919
field: AttachmentField;
@@ -56,8 +56,6 @@ export function Attachments({
5656
value,
5757
})) ?? []
5858
);
59-
60-
const notify = useNotify();
6159
const { t } = useTranslation();
6260

6361
const uploadFailedTitle = useCallback(
@@ -111,16 +109,13 @@ export function Attachments({
111109
[t, uploadFailedTitle]
112110
);
113111

114-
const notifyError = useCallback(
115-
(title: string, errorMessage: string) => {
116-
notify({
117-
title,
118-
message: errorMessage,
119-
type: "error",
120-
});
121-
},
122-
[notify]
123-
);
112+
const notifyError = useCallback((title: string, errorMessage: string) => {
113+
notify({
114+
title,
115+
message: errorMessage,
116+
type: "error",
117+
});
118+
}, []);
124119

125120
const onDrop = useCallback(
126121
async (acceptedFiles: File[]) => {

src/modules/service-catalog/components/service-catalog-item/ServiceCatalogItem.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ import type { Organization } from "../../../ticket-fields/data-types/Organizatio
55
import { useServiceCatalogItem } from "../../hooks/useServiceCatalogItem";
66
import { submitServiceItemRequest } from "./submitServiceItemRequest";
77
import type { ServiceRequestResponse } from "../../data-types/ServiceRequestResponse";
8-
import { addFlashNotification } from "../../../shared";
8+
import { addFlashNotification, notify } from "../../../shared";
99
import { useTranslation } from "react-i18next";
10-
import { useNotify } from "../../../shared/notifications/useNotify";
1110
import { Anchor } from "@zendeskgarden/react-buttons";
1211

1312
const Container = styled.div`
@@ -52,8 +51,6 @@ export function ServiceCatalogItem({
5251
handleChange,
5352
} = useItemFormFields(serviceCatalogItem, baseLocale);
5453
const { t } = useTranslation();
55-
const notify = useNotify();
56-
5754
if (error) {
5855
throw error;
5956
}

src/modules/service-catalog/components/service-catalog-list/ServiceCatalogList.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { EmptyState } from "./EmptyState";
1010
import { Search } from "./Search";
1111
import debounce from "lodash.debounce";
1212
import { useServiceCatalogItems } from "../../hooks/useServiceCatalogItems";
13-
import { useNotify } from "../../../shared/notifications/useNotify";
13+
import { notify } from "../../../shared";
1414

1515
const StyledCol = styled(Col)`
1616
margin-bottom: ${(props) => props.theme.space.md};
@@ -35,7 +35,6 @@ export function ServiceCatalogList({
3535
const [searchInputValue, setSearchInputValue] = useState("");
3636

3737
const { t } = useTranslation();
38-
const notify = useNotify();
3938

4039
const {
4140
serviceCatalogItems,
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { ReactNode } from "react";
22
import type { IGardenTheme } from "@zendeskgarden/react-theming";
33
import { ThemeProvider } from "@zendeskgarden/react-theming";
4-
import { ToastProvider } from "@zendeskgarden/react-notifications";
54
import { ModalContainerProvider } from "./modal-container/ModalContainerProvider";
65

76
export function ThemeProviders({
@@ -13,10 +12,7 @@ export function ThemeProviders({
1312
}) {
1413
return (
1514
<ThemeProvider theme={theme}>
16-
{/* ToastProvider z-index needs to be higher than the z-index of the admin navbar */}
17-
<ToastProvider zIndex={2147483647}>
18-
<ModalContainerProvider>{children}</ModalContainerProvider>
19-
</ToastProvider>
15+
<ModalContainerProvider>{children}</ModalContainerProvider>
2016
</ThemeProvider>
2117
);
2218
}

0 commit comments

Comments
 (0)