@@ -273,7 +297,11 @@ export class RawSelectPlants
description={Content.BOX_SELECT_DESCRIPTION} />
-
+
{this.selectedPointData.map(p => {
if (p.kind == "PlantTemplate" || p.body.pointer_type == "Plant") {
return {
let count = 0;
- const newToast = (): [FBToast, HTMLDivElement] => {
+ const newToast = (idPrefix = ""): [FBToast, HTMLDivElement] => {
const parent = document.createElement("div");
- const child = new FBToast(parent, "title", "message" + (count++), "red");
+ const child =
+ new FBToast(parent, "title", "message" + (count++), "red", idPrefix);
+ parent.appendChild(child.toastEl);
return [child, parent];
};
@@ -90,6 +92,30 @@ describe("FBToast", () => {
i.detach();
expect(FBToast.everyMessage[message]).toBeFalsy();
expect(p.removeChild).toHaveBeenCalledWith(i.toastEl);
+ expect(i.isAttached).toBeFalsy();
+ });
+
+ it("doesn't detach from the DOM", () => {
+ const [i, p] = newToast();
+ p.innerHTML = "";
+ const { message } = i;
+ FBToast.everyMessage[message] = true;
+ p.removeChild = jest.fn();
+ i.isAttached = true;
+ i.detach();
+ expect(FBToast.everyMessage[message]).toBeFalsy();
+ expect(p.removeChild).not.toHaveBeenCalled();
+ expect(i.isAttached).toBeTruthy();
+ });
+
+ it("sets id", () => {
+ const toast = newToast("id-prefix")[0];
+ expect(toast.toastEl.id).toEqual(expect.stringMatching("^id-prefix-toast-"));
+ });
+
+ it("doesn't set id", () => {
+ const toast = newToast()[0];
+ expect(toast.toastEl.id).toEqual("");
});
it("does polling", () => {
diff --git a/frontend/toast/__tests__/toast_internal_support_test.ts b/frontend/toast/__tests__/toast_internal_support_test.ts
index c9c86ce2d..3f1d9ab0e 100644
--- a/frontend/toast/__tests__/toast_internal_support_test.ts
+++ b/frontend/toast/__tests__/toast_internal_support_test.ts
@@ -20,20 +20,34 @@ describe("toast internal support files", () => {
container.className = "toast-container";
document.body.appendChild(container);
- createToastOnce(msg, "bar", "baz", fallback);
+ createToastOnce(msg, "bar", "baz", "id-prefix", fallback);
expect(FBToast.everyMessage[msg]).toBe(true);
expect(fallback).not.toHaveBeenCalled();
expect(mockRun).toHaveBeenCalled();
- createToastOnce(msg, "bar", "baz", fallback);
+ createToastOnce(msg, "bar", "baz", "id-prefix", fallback);
expect(fallback).toHaveBeenCalled();
});
+ it("uses default fallback logger", () => {
+ document.body.innerHTML = "";
+ console.warn = jest.fn();
+ const container = document.createElement("DIV");
+ container.className = "toast-container";
+ document.body.appendChild(container);
+ const msg = "foo";
+ delete FBToast.everyMessage[msg];
+ createToastOnce(msg, "bar", "baz", "");
+ expect(console.warn).not.toHaveBeenCalled();
+ expect(mockRun).toHaveBeenCalled();
+ createToastOnce(msg, "bar", "baz", "");
+ expect(console.warn).toHaveBeenCalled();
+ });
+
it("crashes if you don't attach .toast-container", () => {
document.body.innerHTML = "";
- expect(() => createToast("x", "y", "z"))
- .toThrow();
+ expect(() => createToast("x", "y", "z", "id-prefix")).toThrow();
});
});
diff --git a/frontend/toast/__tests__/toast_test.ts b/frontend/toast/__tests__/toast_test.ts
index 9822a399a..3f71c7ab3 100644
--- a/frontend/toast/__tests__/toast_test.ts
+++ b/frontend/toast/__tests__/toast_test.ts
@@ -14,93 +14,113 @@ const {
info,
fun,
init,
+ removeToast,
busy,
}: typeof import("../toast") = jest.requireActual("../toast");
describe("toasts", () => {
it("pops a warning() toast", () => {
warning("test suite msg 1");
- expect(createToastOnce).toHaveBeenCalledWith("test suite msg 1",
- "Warning",
- "orange",
- console.warn);
+ expect(createToastOnce).toHaveBeenCalledWith(
+ "test suite msg 1", "Warning", "orange", "", console.warn);
});
it("pops a warning() toast with different title and color", () => {
- warning("test suite msg", "new title", "purple");
- expect(createToastOnce)
- .toHaveBeenCalledWith("test suite msg", "new title", "purple",
- console.warn);
+ warning("test suite msg", "new title", "purple", "id-prefix");
+ expect(createToastOnce).toHaveBeenCalledWith(
+ "test suite msg", "new title", "purple", "id-prefix", console.warn);
});
it("pops a error() toast", () => {
error("test suite msg 2");
- expect(createToastOnce).toHaveBeenCalledWith("test suite msg 2",
- "Error",
- "red",
- console.error);
+ expect(createToastOnce).toHaveBeenCalledWith(
+ "test suite msg 2", "Error", "red", "", console.error);
});
it("pops a error() toast with different title and color", () => {
- error("test suite msg", "new title", "purple");
- expect(createToastOnce)
- .toHaveBeenCalledWith("test suite msg", "new title", "purple",
- console.error);
+ error("test suite msg", "new title", "purple", "id-prefix");
+ expect(createToastOnce).toHaveBeenCalledWith(
+ "test suite msg", "new title", "purple", "id-prefix", console.error);
});
it("pops a success() toast", () => {
success("test suite msg");
- expect(createToast)
- .toHaveBeenCalledWith("test suite msg", "Success", "green");
+ expect(createToast).toHaveBeenCalledWith(
+ "test suite msg", "Success", "green", "");
});
it("pops a success() toast with different title and color", () => {
- success("test suite msg", "new title", "purple");
- expect(createToast)
- .toHaveBeenCalledWith("test suite msg", "new title", "purple");
+ success("test suite msg", "new title", "purple", "id-prefix");
+ expect(createToast).toHaveBeenCalledWith(
+ "test suite msg", "new title", "purple", "id-prefix");
});
it("pops a info() toast", () => {
info("test suite msg");
- expect(createToast)
- .toHaveBeenCalledWith("test suite msg", "FYI", "blue");
+ expect(createToast).toHaveBeenCalledWith(
+ "test suite msg", "FYI", "blue", "");
});
it("pops a info() toast with different title and color", () => {
- info("test suite msg", "new title", "purple");
- expect(createToast)
- .toHaveBeenCalledWith("test suite msg", "new title", "purple");
+ info("test suite msg", "new title", "purple", "id-prefix");
+ expect(createToast).toHaveBeenCalledWith(
+ "test suite msg", "new title", "purple", "id-prefix");
});
it("pops a busy() toast", () => {
busy("test suite msg");
- expect(createToast)
- .toHaveBeenCalledWith("test suite msg", "Busy", "yellow");
+ expect(createToast).toHaveBeenCalledWith(
+ "test suite msg", "Busy", "yellow", "");
});
it("pops a busy() toast with different title and color", () => {
- busy("test suite msg", "new title", "purple");
- expect(createToast)
- .toHaveBeenCalledWith("test suite msg", "new title", "purple");
+ busy("test suite msg", "new title", "purple", "id-prefix");
+ expect(createToast).toHaveBeenCalledWith(
+ "test suite msg", "new title", "purple", "id-prefix");
});
it("pops a fun() toast", () => {
fun("test suite msg");
- expect(createToast)
- .toHaveBeenCalledWith("test suite msg", "Did you know?", "dark-blue");
+ expect(createToast).toHaveBeenCalledWith(
+ "test suite msg", "Did you know?", "dark-blue", "");
});
it("pops a fun() toast with different title and color", () => {
- fun("test suite msg", "new title", "purple");
- expect(createToast)
- .toHaveBeenCalledWith("test suite msg", "new title", "purple");
+ fun("test suite msg", "new title", "purple", "id-prefix");
+ expect(createToast).toHaveBeenCalledWith(
+ "test suite msg", "new title", "purple", "id-prefix");
});
+ const getToastContainerCount = () =>
+ Object.values(document.querySelectorAll(".toast-container")).length;
+
+ const getToastCount = () =>
+ document.querySelector(".toast-container")?.childElementCount;
+
it("adds the appropriate div to the DOM", () => {
- const count1 = document.querySelectorAll(".toast-container").item.length;
- expect(count1).toEqual(1);
+ document.body.innerHTML = "";
+ expect(getToastContainerCount()).toEqual(0);
init();
- const count2 = document.querySelectorAll(".toast-container").item.length;
- expect(count2).toEqual(1);
+ expect(getToastContainerCount()).toEqual(1);
+ });
+
+ it("removes a toast message", () => {
+ document.body.innerHTML = "";
+ init();
+ expect(getToastCount()).toEqual(0);
+ const toast = document.createElement("div");
+ toast.id = "id-prefix-123";
+ document.querySelector(".toast-container")?.appendChild(toast);
+ expect(getToastCount()).toEqual(1);
+ removeToast("id-prefix");
+ expect(getToastCount()).toEqual(0);
+ });
+
+ it("doesn't remove a toast message: parent missing", () => {
+ document.body.innerHTML = "";
+ expect(getToastContainerCount()).toEqual(0);
+ console.error = jest.fn();
+ removeToast("id-prefix");
+ expect(console.error).toHaveBeenCalledWith("toast-container is null.");
});
});
diff --git a/frontend/toast/fb_toast.ts b/frontend/toast/fb_toast.ts
index 5343bf608..1cff0e7dd 100644
--- a/frontend/toast/fb_toast.ts
+++ b/frontend/toast/fb_toast.ts
@@ -1,3 +1,5 @@
+import { uuid } from "farmbot";
+
/** This is a [surprisingly reliable] legacy component.
* TODO: Convert this to React. */
export class FBToast {
@@ -29,7 +31,10 @@ export class FBToast {
constructor(public parent: Element,
title: string,
raw_message: string,
- color: string) {
+ color: string,
+ idPrefix: string) {
+
+ idPrefix && (this.toastEl.id = `${idPrefix}-toast-${uuid()}`);
this.message = raw_message.replace(/\s+/g, " ");
/** Fill contents. */
@@ -84,7 +89,7 @@ export class FBToast {
detach = () => {
clearInterval(this.intervalId);
delete FBToast.everyMessage[this.message];
- if (this.isAttached) {
+ if (this.isAttached && this.parent.contains(this.toastEl)) {
this.parent.removeChild(this.toastEl);
this.isAttached = false;
}
diff --git a/frontend/toast/toast.ts b/frontend/toast/toast.ts
index a6902da00..ca347f047 100644
--- a/frontend/toast/toast.ts
+++ b/frontend/toast/toast.ts
@@ -4,45 +4,70 @@ import { t } from "../i18next_wrapper";
/**
* Orange message with "Warning" as the default title.
*/
-export const warning =
- (message: string, title = t("Warning"), color = "orange") => {
- createToastOnce(message, title, color, console.warn);
- };
+export const warning = (
+ message: string,
+ title = t("Warning"),
+ color = "orange",
+ idPrefix = "",
+) => {
+ createToastOnce(message, title, color, idPrefix, console.warn);
+};
/**
* Red message with "Error" as the default title.
*/
-export const error = (message: string, title = t("Error"), color = "red") => {
- createToastOnce(message, title, color, console.error);
+export const error = (
+ message: string,
+ title = t("Error"),
+ color = "red",
+ idPrefix = "",
+) => {
+ createToastOnce(message, title, color, idPrefix, console.error);
};
/**
* Green message with "Success" as the default title.
*/
-export const success =
- (message: string, title = t("Success"), color = "green") =>
- createToast(message, title, color);
+export const success = (
+ message: string,
+ title = t("Success"),
+ color = "green",
+ idPrefix = "",
+) =>
+ createToast(message, title, color, idPrefix);
/**
* Blue message with "FYI" as the default title.
*/
-export const info =
- (message: string, title = t("FYI"), color = "blue") =>
- createToast(message, title, color);
+export const info = (
+ message: string,
+ title = t("FYI"),
+ color = "blue",
+ idPrefix = "",
+) =>
+ createToast(message, title, color, idPrefix);
/**
* Yellow message with "Busy" as the default title.
*/
-export const busy =
- (message: string, title = t("Busy"), color = "yellow") =>
- createToast(message, title, color);
+export const busy = (
+ message: string,
+ title = t("Busy"),
+ color = "yellow",
+ idPrefix = "",
+) =>
+ createToast(message, title, color, idPrefix);
/**
* Dark blue message with "Did you know?" as the default title.
*/
-export const fun =
- (message: string, title = t("Did you know?"), color = "dark-blue") =>
- createToast(message, title, color);
+export const fun = (
+ message: string,
+ title = t("Did you know?"),
+ color = "dark-blue",
+ idPrefix = "",
+) =>
+ createToast(message, title, color, idPrefix);
/**
* Adds a hidden container div for holding toast messages.
@@ -52,3 +77,14 @@ export const init = () => {
toastContainer.classList.add("toast-container");
document.body.appendChild(toastContainer);
};
+
+/** Remove all toast messages that match the provided id prefix. */
+export const removeToast = (idPrefix: string) => {
+ const parent = document.querySelector(".toast-container");
+ const toasts = document.querySelectorAll(`[id^="${idPrefix}-"]`);
+ if (parent) {
+ Object.values(toasts).map(toast => parent.removeChild(toast));
+ } else {
+ console.error("toast-container is null.");
+ }
+};
diff --git a/frontend/toast/toast_internal_support.ts b/frontend/toast/toast_internal_support.ts
index 9f82b1afe..403ba5103 100644
--- a/frontend/toast/toast_internal_support.ts
+++ b/frontend/toast/toast_internal_support.ts
@@ -3,7 +3,9 @@ import { FBToast } from "./fb_toast";
/**
* The function responsible for attaching the messages to the container.
*/
-export const createToast = (message: string, title: string, color: string) => {
+export const createToast = (
+ message: string, title: string, color: string, idPrefix: string,
+) => {
/**
* Container element for all of the messages created from init().
@@ -17,18 +19,20 @@ export const createToast = (message: string, title: string, color: string) => {
/**
* Create elements.
*/
- const t = new FBToast(parent, title, message, color);
+ const t = new FBToast(parent, title, message, color, idPrefix);
t.run();
};
export const createToastOnce = (message: string,
title: string,
color: string,
- fallbackLogger = console.warn) => {
+ idPrefix: string,
+ fallbackLogger = console.warn,
+) => {
if (FBToast.everyMessage[message]) {
fallbackLogger(message);
} else {
- createToast(message, title, color);
+ createToast(message, title, color, idPrefix);
FBToast.everyMessage[message] = true;
}
};