mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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:
parent
f31f0cf733
commit
683ec8e5f2
@ -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>
|
||||
);
|
||||
}
|
112
invokeai/frontend/web/src/features/toast/toast.ts
Normal file
112
invokeai/frontend/web/src/features/toast/toast.ts
Normal 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;
|
Loading…
Reference in New Issue
Block a user