mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 01:57:31 +01:00
Avoid rerendering toast action consumers
This commit is contained in:
1 parent
8020f551ca
commit
42544053c4
20 files changed
+146
-45
No files matched your search
@@ -3,7 +3,7 @@ import type { ExecutionWorkspace } from "@paperclipai/shared";
|
||||
import { Link } from "@/lib/router";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { executionWorkspacesApi } from "../api/execution-workspaces";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { formatDateTime, issueUrl } from "../lib/utils";
|
||||
import { Button } from "./ui/button";
|
||||
@@ -44,7 +44,7 @@ export function ExecutionWorkspaceCloseDialog({
|
||||
onClosed,
|
||||
}: ExecutionWorkspaceCloseDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const actionLabel = currentStatus === "cleanup_failed" ? "Retry close" : "Close workspace";
|
||||
|
||||
const readinessQuery = useQuery({
|
||||
|
||||
@@ -77,7 +77,7 @@ vi.mock("../context/CompanyContext", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../context/ToastContext", () => ({
|
||||
useToast: () => toastState,
|
||||
useToastActions: () => toastState,
|
||||
}));
|
||||
|
||||
vi.mock("../api/issues", () => ({
|
||||
|
||||
@@ -14,7 +14,7 @@ import { queryKeys } from "../lib/queryKeys";
|
||||
import { useProjectOrder } from "../hooks/useProjectOrder";
|
||||
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
|
||||
import { buildExecutionPolicy } from "../lib/issue-execution-policy";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import {
|
||||
assigneeValueFromSelection,
|
||||
currentUserAssigneeOption,
|
||||
@@ -280,7 +280,7 @@ export function NewIssueDialog() {
|
||||
const { newIssueOpen, newIssueDefaults, closeNewIssue } = useDialog();
|
||||
const { companies, selectedCompanyId, selectedCompany } = useCompany();
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [status, setStatus] = useState("todo");
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "@/lib/router";
|
||||
import { X } from "lucide-react";
|
||||
import { useToast, type ToastItem, type ToastTone } from "../context/ToastContext";
|
||||
import {
|
||||
useToastActions,
|
||||
useToastState,
|
||||
type ToastItem,
|
||||
type ToastTone,
|
||||
} from "../context/ToastContext";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
const toneClasses: Record<ToastTone, string> = {
|
||||
@@ -75,7 +80,8 @@ function AnimatedToast({
|
||||
}
|
||||
|
||||
export function ToastViewport() {
|
||||
const { toasts, dismissToast } = useToast();
|
||||
const toasts = useToastState();
|
||||
const { dismissToast } = useToastActions();
|
||||
|
||||
if (toasts.length === 0) return null;
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import type { ActiveRunForIssue, LiveRunForIssue } from "../api/heartbeats";
|
||||
import { authApi } from "../api/auth";
|
||||
import { useCompany } from "./CompanyContext";
|
||||
import type { ToastInput } from "./ToastContext";
|
||||
import { useToast } from "./ToastContext";
|
||||
import { useToastActions } from "./ToastContext";
|
||||
import { upsertIssueCommentInPages } from "../lib/optimistic-issue-comments";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { toCompanyRelativePath } from "../lib/company-routes";
|
||||
import { useLocation } from "../lib/router";
|
||||
@@ -722,7 +723,7 @@ export const __liveUpdatesTestUtils = {
|
||||
export function LiveUpdatesProvider({ children }: { children: ReactNode }) {
|
||||
const { selectedCompanyId, selectedCompany } = useCompany();
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const location = useLocation();
|
||||
const gateRef = useRef<ToastGate>({ cooldownHits: new Map(), suppressUntil: 0 });
|
||||
const pathnameRef = useRef(location.pathname);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { ToastProvider, useToastActions, useToastState } from "./ToastContext";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
describe("ToastContext", () => {
|
||||
let container: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("does not rerender action-only consumers when toast state changes", () => {
|
||||
const root = createRoot(container);
|
||||
let actionOnlyRenderCount = 0;
|
||||
let pushToastRef: ((input: { title: string }) => string | null) | null = null;
|
||||
let clearToastsRef: (() => void) | null = null;
|
||||
|
||||
function ActionOnlyConsumer() {
|
||||
actionOnlyRenderCount += 1;
|
||||
const { pushToast, clearToasts } = useToastActions();
|
||||
pushToastRef = pushToast;
|
||||
clearToastsRef = clearToasts;
|
||||
return null;
|
||||
}
|
||||
|
||||
function ToastCount() {
|
||||
const toasts = useToastState();
|
||||
return <div data-testid="toast-count">{String(toasts.length)}</div>;
|
||||
}
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<ToastProvider>
|
||||
<ActionOnlyConsumer />
|
||||
<ToastCount />
|
||||
</ToastProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(actionOnlyRenderCount).toBe(1);
|
||||
expect(container.querySelector('[data-testid="toast-count"]')?.textContent).toBe("0");
|
||||
|
||||
act(() => {
|
||||
pushToastRef?.({ title: "Saved" });
|
||||
});
|
||||
|
||||
expect(actionOnlyRenderCount).toBe(1);
|
||||
expect(container.querySelector('[data-testid="toast-count"]')?.textContent).toBe("1");
|
||||
|
||||
act(() => {
|
||||
clearToastsRef?.();
|
||||
});
|
||||
|
||||
expect(actionOnlyRenderCount).toBe(1);
|
||||
expect(container.querySelector('[data-testid="toast-count"]')?.textContent).toBe("0");
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -36,13 +36,16 @@ export interface ToastItem {
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
interface ToastContextValue {
|
||||
toasts: ToastItem[];
|
||||
interface ToastActionsContextValue {
|
||||
pushToast: (input: ToastInput) => string | null;
|
||||
dismissToast: (id: string) => void;
|
||||
clearToasts: () => void;
|
||||
}
|
||||
|
||||
interface ToastContextValue extends ToastActionsContextValue {
|
||||
toasts: ToastItem[];
|
||||
}
|
||||
|
||||
const DEFAULT_TTL_BY_TONE: Record<ToastTone, number> = {
|
||||
info: 4000,
|
||||
success: 3500,
|
||||
@@ -55,7 +58,8 @@ const MAX_TOASTS = 5;
|
||||
const DEDUPE_WINDOW_MS = 3500;
|
||||
const DEDUPE_MAX_AGE_MS = 20000;
|
||||
|
||||
const ToastContext = createContext<ToastContextValue | null>(null);
|
||||
const ToastStateContext = createContext<ToastItem[] | null>(null);
|
||||
const ToastActionsContext = createContext<ToastActionsContextValue | null>(null);
|
||||
|
||||
function normalizeTtl(value: number | undefined, tone: ToastTone) {
|
||||
const fallback = DEFAULT_TTL_BY_TONE[tone];
|
||||
@@ -150,23 +154,40 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
timersRef.current.clear();
|
||||
}, []);
|
||||
|
||||
const value = useMemo<ToastContextValue>(
|
||||
const actions = useMemo<ToastActionsContextValue>(
|
||||
() => ({
|
||||
toasts,
|
||||
pushToast,
|
||||
dismissToast,
|
||||
clearToasts,
|
||||
}),
|
||||
[toasts, pushToast, dismissToast, clearToasts],
|
||||
[pushToast, dismissToast, clearToasts],
|
||||
);
|
||||
|
||||
return <ToastContext.Provider value={value}>{children}</ToastContext.Provider>;
|
||||
return (
|
||||
<ToastActionsContext.Provider value={actions}>
|
||||
<ToastStateContext.Provider value={toasts}>{children}</ToastStateContext.Provider>
|
||||
</ToastActionsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useToast() {
|
||||
const context = useContext(ToastContext);
|
||||
export function useToastState() {
|
||||
const context = useContext(ToastStateContext);
|
||||
if (!context) {
|
||||
throw new Error("useToast must be used within a ToastProvider");
|
||||
throw new Error("useToastState must be used within a ToastProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useToastActions() {
|
||||
const context = useContext(ToastActionsContext);
|
||||
if (!context) {
|
||||
throw new Error("useToastActions must be used within a ToastProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useToast() {
|
||||
const toasts = useToastState();
|
||||
const actions = useToastActions();
|
||||
return useMemo<ToastContextValue>(() => ({ toasts, ...actions }), [toasts, actions]);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useToast } from "@/context/ToastContext";
|
||||
import { useToastActions } from "@/context/ToastContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChoosePathButton } from "@/components/PathInstructionsModal";
|
||||
import { invalidateDynamicParser } from "@/adapters/dynamic-loader";
|
||||
@@ -255,7 +255,7 @@ export function AdapterManager() {
|
||||
const { selectedCompany } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
|
||||
const [installPackage, setInstallPackage] = useState("");
|
||||
const [installVersion, setInstallVersion] = useState("");
|
||||
|
||||
@@ -18,7 +18,7 @@ import { issuesApi } from "../api/issues";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
@@ -1540,7 +1540,7 @@ function ConfigurationTab({
|
||||
hideInstructionsFile?: boolean;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const [awaitingRefreshAfterSave, setAwaitingRefreshAfterSave] = useState(false);
|
||||
const lastAgentRef = useRef(agent);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
import { useNavigate, useLocation } from "@/lib/router";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { authApi } from "../api/auth";
|
||||
import { companiesApi } from "../api/companies";
|
||||
@@ -580,7 +580,7 @@ function expandAncestors(filePath: string): string[] {
|
||||
export function CompanyExport() {
|
||||
const { selectedCompanyId, selectedCompany } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { data: session, isFetched: isSessionFetched } = useQuery({
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from "@paperclipai/shared";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { authApi } from "../api/auth";
|
||||
import { companiesApi } from "../api/companies";
|
||||
import { agentsApi } from "../api/agents";
|
||||
@@ -651,7 +651,7 @@ export function CompanyImport() {
|
||||
setSelectedCompanyId,
|
||||
} = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const queryClient = useQueryClient();
|
||||
const packageInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const { data: session } = useQuery({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION } from "@paperclipai/shared";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { companiesApi } from "../api/companies";
|
||||
import { accessApi } from "../api/access";
|
||||
import { assetsApi } from "../api/assets";
|
||||
@@ -34,7 +34,7 @@ export function CompanySettings() {
|
||||
setSelectedCompanyId
|
||||
} = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const queryClient = useQueryClient();
|
||||
// General settings local state
|
||||
const [companyName, setCompanyName] = useState("");
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
import { companySkillsApi } from "../api/companySkills";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { MarkdownBody } from "../components/MarkdownBody";
|
||||
@@ -530,7 +530,7 @@ function SkillPane({
|
||||
onSave: () => void;
|
||||
savePending: boolean;
|
||||
}) {
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
|
||||
if (!detail) {
|
||||
if (loading) {
|
||||
@@ -759,7 +759,7 @@ export function CompanySkills() {
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const [skillFilter, setSkillFilter] = useState("");
|
||||
const [source, setSource] = useState("");
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { projectsApi } from "../api/projects";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { assigneeValueFromSelection, suggestedCommentAssigneeValue } from "../lib/assignees";
|
||||
import { extractIssueTimelineEvents } from "../lib/issue-timeline-events";
|
||||
@@ -373,7 +373,8 @@ export function IssueDetail() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const { isMobile } = useSidebar();
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [mobilePropsOpen, setMobilePropsOpen] = useState(false);
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useToast } from "@/context/ToastContext";
|
||||
import { useToastActions } from "@/context/ToastContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function firstNonEmptyLine(value: string | null | undefined): string | null {
|
||||
@@ -64,7 +64,7 @@ export function PluginManager() {
|
||||
const { selectedCompany } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
|
||||
const [installPackage, setInstallPackage] = useState("");
|
||||
const [installDialogOpen, setInstallDialogOpen] = useState(false);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { assetsApi } from "../api/assets";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { ProjectProperties, type ProjectConfigFieldKey, type ProjectFieldSaveState } from "../components/ProjectProperties";
|
||||
@@ -455,7 +455,7 @@ export function ProjectDetail() {
|
||||
const { companies, selectedCompanyId, setSelectedCompanyId } = useCompany();
|
||||
const { closePanel } = usePanel();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -22,7 +22,7 @@ import { agentsApi } from "../api/agents";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { buildRoutineTriggerPatch } from "../lib/routine-trigger-patch";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
@@ -268,7 +268,7 @@ export function RoutineDetail() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const hydratedRoutineIdRef = useRef<string | null>(null);
|
||||
const titleInputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
||||
|
||||
@@ -31,7 +31,7 @@ vi.mock("../context/BreadcrumbContext", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../context/ToastContext", () => ({
|
||||
useToast: () => ({ pushToast: vi.fn() }),
|
||||
useToastActions: () => ({ pushToast: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("../api/routines", () => ({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { issuesApi } from "../api/issues";
|
||||
import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { groupBy } from "../lib/groupBy";
|
||||
import { createIssueDetailLocationState } from "../lib/issueDetailBreadcrumb";
|
||||
@@ -293,7 +293,7 @@ export function Routines() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
||||
const titleInputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const assigneeSelectorRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
@@ -34,7 +34,7 @@ import type {
|
||||
} from "@paperclipai/shared";
|
||||
import { pluginsApi } from "@/api/plugins";
|
||||
import { ApiError } from "@/api/client";
|
||||
import { useToast, type ToastInput } from "@/context/ToastContext";
|
||||
import { useToastActions, type ToastInput } from "@/context/ToastContext";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bridge error type (mirrors the SDK's PluginBridgeError)
|
||||
@@ -369,7 +369,7 @@ export function useHostContext(): PluginHostContext {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function usePluginToast(): PluginToastFn {
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
return useCallback(
|
||||
(input: PluginToastInput) => pushToast(input),
|
||||
[pushToast],
|
||||
|
||||
Reference in new issue
Block a user