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.
This commit is contained in:
psychedelicious 2024-05-21 19:33:24 +10:00
parent f31f0cf733
commit 683ec8e5f2
2 changed files with 139 additions and 0 deletions

View File

@ -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 (
<Flex flexDir="column">
<Text fontSize="md">{message}</Text>
<Flex gap="2" alignItems="center">
<Text fontSize="sm">{t('toast.sessionRef', { sessionId })}</Text>
<IconButton
size="sm"
aria-label="Copy"
icon={<PiCopyBold />}
onClick={onCopy.bind(null, sessionId)}
variant="ghost"
/>
</Flex>
</Flex>
);
}

View File

@ -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<UseToastOptions, 'id'> & {
// 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<Record<string, ToastInternalState | undefined>>({});
// 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;