diff --git a/.changeset/toast-notification-removed.md b/.changeset/toast-notification-removed.md new file mode 100644 index 00000000..33829dd7 --- /dev/null +++ b/.changeset/toast-notification-removed.md @@ -0,0 +1,5 @@ +--- +"@openworkflowspec/diagram-editor": minor +--- + +Toast notification removed for side panel buttons diff --git a/packages/open-workflow-diagram-editor/package.json b/packages/open-workflow-diagram-editor/package.json index 3e5285e5..d77b79e8 100644 --- a/packages/open-workflow-diagram-editor/package.json +++ b/packages/open-workflow-diagram-editor/package.json @@ -52,7 +52,6 @@ "html-to-image": "catalog:", "js-yaml": "catalog:", "radix-ui": "catalog:", - "sonner": "catalog:", "use-sync-external-store": "catalog:" }, "devDependencies": { diff --git a/packages/open-workflow-diagram-editor/src/components/ui/sonner.css b/packages/open-workflow-diagram-editor/src/components/ui/sonner.css deleted file mode 100644 index 661b1784..00000000 --- a/packages/open-workflow-diagram-editor/src/components/ui/sonner.css +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021-Present The Open Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - @reference "../../styles.css"; - -@layer sonner { - /* sonner defaults unlayered, so these overrides need !important */ - .dec-root [data-sonner-toaster] { --width: 300px !important; } - .dec-root [data-sonner-toast] { background: var(--dec-toast-bg) !important; border-radius: 10px !important; border: 1px solid var(--dec-toast-border) !important; } - .dec-root [data-sonner-toast][data-type="success"] { border-left: 4px solid var(--dec-toast-success) !important; } - .dec-root [data-sonner-toast][data-type="error"] { border-left: 4px solid var(--dec-error-accent) !important; } - .dec-root [data-sonner-toast][data-type="warning"] { border-left: 4px solid var(--dec-toast-warning) !important; } - .dec-root [data-sonner-toast][data-type="info"] { border-left: 4px solid var(--dec-toast-info) !important; } - .dec-root [data-sonner-toast] [data-title], .dec-root [data-sonner-toast] [data-description] { color: var(--dec-toast-text) !important; } - .dec-root [data-sonner-toast] [data-close-button] { - position: absolute !important; - top: 50% !important; - right: 12px !important; - left: auto !important; - transform: translateY(-50%) !important; - background: transparent !important; - border: none !important; - box-shadow: none !important; - color: var(--dec-toast-close-btn); - cursor: pointer; - } -} diff --git a/packages/open-workflow-diagram-editor/src/components/ui/sonner.tsx b/packages/open-workflow-diagram-editor/src/components/ui/sonner.tsx deleted file mode 100644 index e8de5262..00000000 --- a/packages/open-workflow-diagram-editor/src/components/ui/sonner.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2021-Present The Open Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - CircleCheckIcon, - InfoIcon, - Loader2Icon, - OctagonXIcon, - TriangleAlertIcon, - XIcon, -} from "lucide-react"; -import { Toaster as Sonner, type ToasterProps } from "sonner"; -import "./sonner.css"; - -const Toaster = ({ ...props }: ToasterProps) => { - return ( - - ), - info: , - warning: ( - - ), - error: , - loading: , - close: , - }} - {...props} - /> - ); -}; - -export { Toaster }; diff --git a/packages/open-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx b/packages/open-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx index dafad7fe..572d0d07 100644 --- a/packages/open-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx +++ b/packages/open-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx @@ -27,7 +27,6 @@ import { useResolvedColorMode } from "../hooks/useResolvedColorMode"; import { SidebarProvider } from "@/components/ui/sidebar"; import { SidePanel } from "@/side-panel/SidePanel"; import { DiagramEditorErrorBoundary } from "./error-pages/DiagramEditorErrorBoundary"; -import { Toaster } from "@/components/ui/sonner"; /** * Imperative handle exposed by `DiagramEditor` via `ref`. @@ -184,7 +183,6 @@ export const DiagramEditor = React.forwardRef - ); }, diff --git a/packages/open-workflow-diagram-editor/src/i18n/locales/en.ts b/packages/open-workflow-diagram-editor/src/i18n/locales/en.ts index 4dbe7c83..008634d9 100644 --- a/packages/open-workflow-diagram-editor/src/i18n/locales/en.ts +++ b/packages/open-workflow-diagram-editor/src/i18n/locales/en.ts @@ -43,7 +43,9 @@ export const en = { "sidebar.exportMermaid.copy": "Copy Mermaid Code", "sidebar.exportMermaid.download": "Download as Mermaid File", "sidebar.exportMermaid.copied": "Copied!", + "sidebar.export.downloaded": "Downloaded", "sidebar.exportPng.download": "Download as PNG", + "sidebar.exportPng.downloading": "Downloading...", "aria.minimap.hide": "Hide minimap", "aria.minimap.show": "Show minimap", "aria.badge": "Badge:", @@ -53,9 +55,6 @@ export const en = { "aria.panel.exportActions": "Export actions", "workflowError.autoLayout.title": "Layout Error", "workflowError.autoLayout.message": "Failed to apply auto-layout to the diagram.", - "toast.clipboard.error": "Failed to copy", - "toast.download.success": "Download started", - "toast.download.error": "Download failed", } as const; export type TranslationKeys = keyof typeof en; diff --git a/packages/open-workflow-diagram-editor/src/lib/download.ts b/packages/open-workflow-diagram-editor/src/lib/download.ts index 6daeb23c..fe67999f 100644 --- a/packages/open-workflow-diagram-editor/src/lib/download.ts +++ b/packages/open-workflow-diagram-editor/src/lib/download.ts @@ -24,9 +24,7 @@ export function downloadFile(content: string, filename: string, mimeType = "text const link = document.createElement("a"); link.href = url; link.download = filename; - document.body.appendChild(link); link.click(); - document.body.removeChild(link); setTimeout(() => { URL.revokeObjectURL(url); }, 100); diff --git a/packages/open-workflow-diagram-editor/src/side-panel/WorkflowActions.tsx b/packages/open-workflow-diagram-editor/src/side-panel/WorkflowActions.tsx index acc83562..b0e3e61c 100644 --- a/packages/open-workflow-diagram-editor/src/side-panel/WorkflowActions.tsx +++ b/packages/open-workflow-diagram-editor/src/side-panel/WorkflowActions.tsx @@ -16,7 +16,7 @@ import * as React from "react"; import { useI18n } from "@openworkflowspec/i18n"; -import { ClipboardPen, Download, ClipboardCheck, FileImage } from "lucide-react"; +import { ClipboardPen, Download, ClipboardCheck, FileImage, Import } from "lucide-react"; import { useReactFlow, useStore } from "@xyflow/react"; import { Button } from "@/components/ui/button"; import { exportToMermaid } from "@/core"; @@ -26,12 +26,13 @@ import { exportDiagramAsPng } from "@/lib/exportPng"; import { sanitizeFilename } from "@/lib/utils"; import { useDiagramEditorContext } from "@/store/DiagramEditorContext"; import type { Specification } from "@openworkflowspec/sdk"; -import { toast } from "sonner"; export function WorkflowActions({ model }: { model: Specification.Workflow }): React.JSX.Element { const { t } = useI18n(); const [isCopied, setIsCopied] = React.useState(false); + const [downloadedType, setDownloadedType] = React.useState<"mermaid" | "png" | null>(null); const copyTimeoutRef = React.useRef | null>(null); + const downloadTimeoutRef = React.useRef | null>(null); const reactFlowInstance = useReactFlow(); const diagramDomNode = useStore((s) => s.domNode); const { isExporting, setIsExporting } = useDiagramEditorContext(); @@ -41,6 +42,10 @@ export function WorkflowActions({ model }: { model: Specification.Workflow }): R if (copyTimeoutRef.current) { clearTimeout(copyTimeoutRef.current); } + + if (downloadTimeoutRef.current) { + clearTimeout(downloadTimeoutRef.current); + } }; }, []); @@ -59,9 +64,10 @@ export function WorkflowActions({ model }: { model: Specification.Workflow }): R copyTimeoutRef.current = null; }, 2000); } catch (error) { - toast.error(t("toast.clipboard.error"), { - description: error instanceof Error ? error.message : undefined, - }); + console.error( + "Failed to copy Mermaid code:", + error instanceof Error ? error.message : undefined, + ); } }; @@ -70,11 +76,21 @@ export function WorkflowActions({ model }: { model: Specification.Workflow }): R const mermaidCode = exportToMermaid(model); const filename = `${sanitizeFilename(model.document?.name)}.mmd`; downloadFile(mermaidCode, filename); - toast.success(t("toast.download.success")); + setDownloadedType("mermaid"); + + if (downloadTimeoutRef.current) { + clearTimeout(downloadTimeoutRef.current); + } + + downloadTimeoutRef.current = setTimeout(() => { + setDownloadedType(null); + downloadTimeoutRef.current = null; + }, 2000); } catch (error) { - toast.error(t("toast.download.error"), { - description: error instanceof Error ? error.message : undefined, - }); + console.error( + "Failed to download Mermaid file:", + error instanceof Error ? error.message : undefined, + ); } }; @@ -86,11 +102,22 @@ export function WorkflowActions({ model }: { model: Specification.Workflow }): R `${sanitizeFilename(model.document?.name)}.png`, diagramDomNode, ); - toast.success(t("toast.download.success")); + + setDownloadedType("png"); + + if (downloadTimeoutRef.current) { + clearTimeout(downloadTimeoutRef.current); + } + + downloadTimeoutRef.current = setTimeout(() => { + setDownloadedType(null); + downloadTimeoutRef.current = null; + }, 2000); } catch (error) { - toast.error(t("toast.download.error"), { - description: error instanceof Error ? error.message : undefined, - }); + console.error( + "Failed to export diagram as PNG:", + error instanceof Error ? error.message : undefined, + ); } finally { setIsExporting(false); } @@ -113,8 +140,10 @@ export function WorkflowActions({ model }: { model: Specification.Workflow }): R size="sm" className="dec:cursor-pointer" > - - {t("sidebar.exportMermaid.download")} + {downloadedType === "mermaid" ? : } + {downloadedType === "mermaid" + ? t("sidebar.export.downloaded") + : t("sidebar.exportMermaid.download")} ); diff --git a/packages/open-workflow-diagram-editor/src/styles.css b/packages/open-workflow-diagram-editor/src/styles.css index a0bed771..7d28037f 100644 --- a/packages/open-workflow-diagram-editor/src/styles.css +++ b/packages/open-workflow-diagram-editor/src/styles.css @@ -15,15 +15,28 @@ */ /* layer order (Priority: lowest -> highest) */ + +@import 'tailwindcss' prefix(dec); +@import './components/ui/shadcn.css'; + +@layer base { + .dec-root, + .dec-root *, + .dec-root::before, + .dec-root::after, + .dec-root::backdrop, + .dec-root *::before, + .dec-root *::after, + .dec-root *::backdrop { + --tw-border-style: solid; + } +} + @layer base, side-panel, react-flow-overrides, custom-nodes, - custom-edge-labels, - sonner; - -@import 'tailwindcss' prefix(dec); -@import './components/ui/shadcn.css'; + custom-edge-labels; @custom-variant dark (&:where(.dec-root.dark, .dec-root.dark *)); diff --git a/packages/open-workflow-diagram-editor/tests/lib/download.test.ts b/packages/open-workflow-diagram-editor/tests/lib/download.test.ts index d113d092..f33ef7ca 100644 --- a/packages/open-workflow-diagram-editor/tests/lib/download.test.ts +++ b/packages/open-workflow-diagram-editor/tests/lib/download.test.ts @@ -24,8 +24,6 @@ describe("downloadFile", () => { it("creates and triggers file download", () => { const mockClick = vi.fn(); - const mockAppendChild = vi.fn(); - const mockRemoveChild = vi.fn(); const mockElement = { click: mockClick, href: "", @@ -35,8 +33,6 @@ describe("downloadFile", () => { vi.spyOn(document, "createElement").mockReturnValue( mockElement as unknown as HTMLAnchorElement, ); - vi.spyOn(document.body, "appendChild").mockImplementation(mockAppendChild); - vi.spyOn(document.body, "removeChild").mockImplementation(mockRemoveChild); vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:mock-url"); vi.spyOn(URL, "revokeObjectURL").mockImplementation(vi.fn()); @@ -44,8 +40,8 @@ describe("downloadFile", () => { downloadFile(testCode, "test.mmd"); expect(document.createElement).toHaveBeenCalledWith("a"); + expect(mockElement.href).toBe("blob:mock-url"); + expect(mockElement.download).toBe("test.mmd"); expect(mockClick).toHaveBeenCalled(); - expect(mockAppendChild).toHaveBeenCalled(); - expect(mockRemoveChild).toHaveBeenCalled(); }); }); diff --git a/packages/open-workflow-diagram-editor/tests/side-panel/WorkflowActions.test.tsx b/packages/open-workflow-diagram-editor/tests/side-panel/WorkflowActions.test.tsx index 6eb63bd7..3ad31c01 100644 --- a/packages/open-workflow-diagram-editor/tests/side-panel/WorkflowActions.test.tsx +++ b/packages/open-workflow-diagram-editor/tests/side-panel/WorkflowActions.test.tsx @@ -25,7 +25,6 @@ import * as clipboard from "../../src/lib/clipboard"; import * as core from "../../src/core"; import * as download from "../../src/lib/download"; import * as exportPng from "../../src/lib/exportPng"; -import * as sonner from "sonner"; describe("WorkflowActions", () => { const toastMock = vi.fn(); @@ -58,8 +57,6 @@ describe("WorkflowActions", () => { const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON); vi.spyOn(clipboard, "copyToClipboard").mockRejectedValue(new Error("Clipboard error")); vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE); - vi.spyOn(sonner.toast, "error").mockImplementation(toastMock); - vi.spyOn(sonner.toast, "success").mockImplementation(toastMock); renderWithProviders(, { model }); @@ -69,7 +66,7 @@ describe("WorkflowActions", () => { await user.click(copyButton); - expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Clipboard error" }); + expect(clipboard.copyToClipboard).toHaveBeenCalledWith(MERMAID_CODE); }); it("should call downloadMermaidFile and show success toast when download button is clicked", async () => { @@ -77,8 +74,6 @@ describe("WorkflowActions", () => { const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON); const downloadSpy = vi.spyOn(download, "downloadFile").mockImplementation(() => {}); vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE); - vi.spyOn(sonner.toast, "error").mockImplementation(toastMock); - vi.spyOn(sonner.toast, "success").mockImplementation(toastMock); renderWithProviders(, { model }); @@ -89,18 +84,16 @@ describe("WorkflowActions", () => { await user.click(downloadButton); expect(downloadSpy).toHaveBeenCalledWith(MERMAID_CODE, "test-wf.mmd"); - expect(toastMock).toHaveBeenCalledWith(expect.any(String)); + expect(screen.getByRole("button", { name: "Downloaded" })).toBeInTheDocument(); }); - it("should show error toast when download fails", async () => { + it("should handle download failure", async () => { const user = userEvent.setup(); const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON); vi.spyOn(download, "downloadFile").mockImplementation(() => { throw new Error("Download error"); }); vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE); - vi.spyOn(sonner.toast, "error").mockImplementation(toastMock); - vi.spyOn(sonner.toast, "success").mockImplementation(toastMock); renderWithProviders(, { model }); @@ -110,7 +103,7 @@ describe("WorkflowActions", () => { await user.click(downloadButton); - expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Download error" }); + expect(download.downloadFile).toHaveBeenCalledWith(MERMAID_CODE, "test-wf.mmd"); }); it("should disable the PNG button when isExporting is true", () => { @@ -118,16 +111,17 @@ describe("WorkflowActions", () => { renderWithProviders(, { model, isExporting: true }); - expect(screen.getByRole("button", { name: /Download as PNG/i })).toBeDisabled(); + const pngButton = screen.getByRole("button", { + name: "Downloading...", + }); + expect(pngButton).toBeDisabled(); }); - it("should call exportDiagramAsPng with sanitized filename and show success toast", async () => { + it("should call exportDiagramAsPng with sanitized filename and show downloaded state", async () => { const user = userEvent.setup(); const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON); const exportSpy = vi.spyOn(exportPng, "exportDiagramAsPng").mockResolvedValue(undefined); const setIsExporting = vi.fn(); - vi.spyOn(sonner.toast, "error").mockImplementation(toastMock); - vi.spyOn(sonner.toast, "success").mockImplementation(toastMock); renderWithProviders(, { model, setIsExporting }); @@ -135,24 +129,20 @@ describe("WorkflowActions", () => { await vi.waitFor(() => expect(exportSpy).toHaveBeenCalled()); expect(exportSpy).toHaveBeenCalledWith(expect.anything(), "test-wf.png", null); - expect(toastMock).toHaveBeenCalledWith(expect.any(String)); + expect(screen.getByRole("button", { name: "Downloaded" })).toBeInTheDocument(); expect(setIsExporting).toHaveBeenCalledWith(true); expect(setIsExporting).toHaveBeenLastCalledWith(false); }); - it("should show error toast and still reset isExporting when PNG export fails", async () => { + it("should reset isExporting when PNG export fails", async () => { const user = userEvent.setup(); const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON); vi.spyOn(exportPng, "exportDiagramAsPng").mockRejectedValue(new Error("Export failed")); const setIsExporting = vi.fn(); - vi.spyOn(sonner.toast, "error").mockImplementation(toastMock); - vi.spyOn(sonner.toast, "success").mockImplementation(toastMock); renderWithProviders(, { model, setIsExporting }); await user.click(screen.getByRole("button", { name: /Download as PNG/i })); await vi.waitFor(() => expect(setIsExporting).toHaveBeenLastCalledWith(false)); - - expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Export failed" }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 811b2361..c325e1ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,9 +132,6 @@ catalogs: rimraf: specifier: ^6.1.3 version: 6.1.3 - sonner: - specifier: ^2.0.8 - version: 2.0.8 storybook: specifier: ^10.6.0 version: 10.6.0 @@ -349,9 +346,6 @@ importers: radix-ui: specifier: 'catalog:' version: 1.6.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - sonner: - specifier: 'catalog:' - version: 2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) use-sync-external-store: specifier: 'catalog:' version: 1.7.0(react@19.3.0) @@ -3732,16 +3726,6 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - sonner@2.0.8: - resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==} - peerDependencies: - '@types/react': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc - react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -7102,13 +7086,6 @@ snapshots: sisteransi@1.0.5: {} - sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): - dependencies: - react: 19.3.0 - react-dom: 19.3.0(react@19.3.0) - optionalDependencies: - '@types/react': 19.3.0 - source-map-js@1.2.1: {} source-map@0.6.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d3d94c06..6e18b6c7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -44,7 +44,6 @@ catalog: react: ^19.3.0 react-dom: ^19.3.0 rimraf: ^6.1.3 - sonner: ^2.0.8 storybook: ^10.6.0 syncpack: ^15.3.3 tailwindcss: ^4.3.3