From 683ec8e5f289c93d8fca488dbae654c2eb155ba2 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 21 May 2024 19:33:24 +1000
Subject: [PATCH] feat(ui): add stateful toast utility
Small wrapper around chakra's toast system simplifies creating and updating toasts. See comments in toast.ts for details.
---
.../toast/ToastWithSessionRefDescription.tsx | 27 +++++
.../frontend/web/src/features/toast/toast.ts | 112 ++++++++++++++++++
2 files changed, 139 insertions(+)
create mode 100644 invokeai/frontend/web/src/features/toast/ToastWithSessionRefDescription.tsx
create mode 100644 invokeai/frontend/web/src/features/toast/toast.ts
diff --git a/invokeai/frontend/web/src/features/toast/ToastWithSessionRefDescription.tsx b/invokeai/frontend/web/src/features/toast/ToastWithSessionRefDescription.tsx
new file mode 100644
index 0000000000..607dcaeb63
--- /dev/null
+++ b/invokeai/frontend/web/src/features/toast/ToastWithSessionRefDescription.tsx
@@ -0,0 +1,27 @@
+import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
+import { t } from 'i18next';
+import { PiCopyBold } from 'react-icons/pi';
+
+function onCopy(sessionId: string) {
+ navigator.clipboard.writeText(sessionId);
+}
+
+type Props = { message: string; sessionId: string };
+
+export default function ToastWithSessionRefDescription({ message, sessionId }: Props) {
+ return (
+
+ {message}
+
+ {t('toast.sessionRef', { sessionId })}
+ }
+ onClick={onCopy.bind(null, sessionId)}
+ variant="ghost"
+ />
+
+
+ );
+}
diff --git a/invokeai/frontend/web/src/features/toast/toast.ts b/invokeai/frontend/web/src/features/toast/toast.ts
new file mode 100644
index 0000000000..9970f5f18f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/toast/toast.ts
@@ -0,0 +1,112 @@
+import type { UseToastOptions } from '@invoke-ai/ui-library';
+import { createStandaloneToast, theme, TOAST_OPTIONS } from '@invoke-ai/ui-library';
+import { map } from 'nanostores';
+import { z } from 'zod';
+
+const toastApi = createStandaloneToast({
+ theme: theme,
+ defaultOptions: TOAST_OPTIONS.defaultOptions,
+}).toast;
+
+// Slightly modified version of UseToastOptions
+type ToastConfig = Omit & {
+ // Only string - Chakra allows numbers
+ id?: string;
+};
+
+type ToastArg = ToastConfig & {
+ /**
+ * Whether to append the number of times this toast has been shown to the title. Defaults to true.
+ * @example
+ * toast({ title: 'Hello', withCount: true });
+ * // first toast: 'Hello'
+ * // second toast: 'Hello (2)'
+ */
+ withCount?: boolean;
+};
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+// Any is correct here; we accept anything as toast data parameter.
+type ToastInternalState = {
+ id: string;
+ config: ToastConfig;
+ count: number;
+};
+
+// We expose a limited API for the toast
+type ToastApi = {
+ getState: () => ToastInternalState | null;
+ close: () => void;
+ isActive: () => boolean;
+};
+
+// Store each toast state by id, allowing toast consumers to not worry about persistent ids and updating and such
+const $toastMap = map>({});
+
+// Helpers to get the getters for the toast API
+const getIsActive = (id: string) => () => toastApi.isActive(id);
+const getClose = (id: string) => () => toastApi.close(id);
+const getGetState = (id: string) => () => $toastMap.get()[id] ?? null;
+
+/**
+ * Creates a toast with the given config. If the toast with the same id already exists, it will be updated.
+ * When a toast is updated, its title, description, status and duration will be overwritten by the new config.
+ * Set duration to `null` to make the toast persistent.
+ * @param arg The toast config.
+ * @returns An object with methods to get the toast state, close the toast and check if the toast is active
+ */
+export const toast = (arg: ToastArg): ToastApi => {
+ // All toasts need an id, set a random one if not provided
+ const id = arg.id ?? crypto.randomUUID();
+ if (!arg.id) {
+ arg.id = id;
+ }
+ if (arg.withCount === undefined) {
+ arg.withCount = true;
+ }
+ let state = $toastMap.get()[arg.id];
+ if (!state) {
+ // First time caller, create and set the state
+ state = { id, config: parseConfig(id, arg, 1), count: 1 };
+ $toastMap.setKey(id, state);
+ // Create the toast
+ toastApi(state.config);
+ } else {
+ // This toast is already active, update its state
+ state.count += 1;
+ state.config = parseConfig(id, arg, state.count);
+ $toastMap.setKey(id, state);
+ // Update the toast itself
+ toastApi.update(id, state.config);
+ }
+ return { getState: getGetState(id), close: getClose(id), isActive: getIsActive(id) };
+};
+
+/**
+ * Give a toast id, arg and current count, returns the parsed toast config (including dynamic title and description)
+ * @param id The id of the toast
+ * @param arg The arg passed to the toast function
+ * @param count The current call count of the toast
+ * @returns The parsed toast config
+ */
+const parseConfig = (id: string, arg: ToastArg, count: number): ToastConfig => {
+ const title = arg.withCount && count > 1 ? `${arg.title} (${count})` : arg.title;
+ const onCloseComplete = () => {
+ $toastMap.setKey(id, undefined);
+ if (arg.onCloseComplete) {
+ arg.onCloseComplete();
+ }
+ };
+ return { ...arg, title, onCloseComplete };
+};
+
+/**
+ * Enum of toast IDs that are often shared between multiple components (typo insurance)
+ */
+export const ToastID = z.enum([
+ 'MODEL_INSTALL_QUEUED',
+ 'MODEL_INSTALL_QUEUE_FAILED',
+ 'GRAPH_QUEUE_FAILED',
+ 'PARAMETER_SET',
+ 'PARAMETER_NOT_SET',
+]).enum;