Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/toast-alert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@serverlessworkflow/diagram-editor": minor
---

add toast alert system through shadcn Sonner
1 change: 1 addition & 0 deletions packages/serverless-workflow-diagram-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"elkjs": "catalog:",
"js-yaml": "catalog:",
"radix-ui": "catalog:",
"sonner": "catalog:",
Comment thread
cheryl7114 marked this conversation as resolved.
"use-sync-external-store": "catalog:"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2021-Present The Serverless 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.
*/

@layer sonner {
.dec-root [data-sonner-toaster] { --width: 300px !important; }
.dec-root [data-sonner-toast] { background: #ffffff !important; border-radius: 10px !important; border: 1px solid #e9edf4 !important; }
.dec-root [data-sonner-toast][data-type="success"] { border-left: 4px solid #22c55e !important; }
.dec-root [data-sonner-toast][data-type="error"] { border-left: 4px solid #ef4444 !important; }
.dec-root [data-sonner-toast][data-type="warning"] { border-left: 4px solid #f97316 !important; }
.dec-root [data-sonner-toast][data-type="info"] { border-left: 4px solid #3b82f6 !important; }
.dec-root.dark [data-sonner-toast] { background: #2d3748 !important; color: #ffffff !important; border-top-color: #4a5568 !important; border-right-color: #4a5568 !important; border-bottom-color: #4a5568 !important; }
.dec-root.dark [data-sonner-toast] [data-title], .dec-root.dark [data-sonner-toast] [data-description] { color: #ffffff !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: #6b7280;
cursor: pointer;
}
.dec-root.dark [data-sonner-toast] [data-close-button] { color: #ffffff; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2021-Present The Serverless 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 (
<Sonner
position="top-left"
className="dec:toaster dec:group"
closeButton
icons={{
success: <CircleCheckIcon className="dec:size-4" style={{ color: "#22c55e" }} />,
info: <InfoIcon className="dec:size-4" style={{ color: "#3b82f6" }} />,
warning: <TriangleAlertIcon className="dec:size-4" style={{ color: "#f97316" }} />,
error: <OctagonXIcon className="dec:size-4" style={{ color: "#ef4444" }} />,
loading: <Loader2Icon className="dec:size-4 dec:animate-spin" />,
close: <XIcon size={14} />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
);
};

export { Toaster };
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ 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";

/**
* DiagramEditor component API
Expand Down Expand Up @@ -130,6 +131,7 @@ export const DiagramEditor = (props: DiagramEditorProps) => {
}}
</DiagramEditorInner>
</I18nProvider>
<Toaster theme={resolvedColorMode} />
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export const en = {
"aria.panel.workflowInfo": "Workflow information panel",
"aria.panel.content": "Panel content",
"aria.panel.exportActions": "Export actions",
"toast.clipboard.error": "Failed to copy",
"toast.download.success": "Download started",
"toast.download.error": "Download failed",
} as const;

export type TranslationKeys = keyof typeof en;
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { exportToMermaid } from "@/core";
import { copyToClipboard } from "@/lib/clipboard";
import { downloadFile } from "@/lib/download";
import type { Specification } from "@serverlessworkflow/sdk";
import { toast } from "sonner";

export function MermaidActions({ model }: { model: Specification.Workflow }): React.JSX.Element {
const { t } = useI18n();
Expand Down Expand Up @@ -51,8 +52,9 @@ export function MermaidActions({ model }: { model: Specification.Workflow }): Re
copyTimeoutRef.current = null;
}, 2000);
} catch (error) {
console.error("Failed to copy mermaid code:", error);
// TODO: Create component to show errors to users
toast.error(t("toast.clipboard.error"), {
description: error instanceof Error ? error.message : undefined,
});
}
};

Expand All @@ -66,9 +68,11 @@ export function MermaidActions({ model }: { model: Specification.Workflow }): Re
.substring(0, 200);
const filename = `${sanitizedName}.mmd`;
downloadFile(mermaidCode, filename);
toast.success(t("toast.download.success"));
} catch (error) {
console.error("Failed to download mermaid file:", error);
// TODO: Create component to show errors to users
toast.error(t("toast.download.error"), {
description: error instanceof Error ? error.message : undefined,
});
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,35 +24,91 @@ import { WORKFLOW_WITH_METADATA_JSON } from "../fixtures/workflows";
import * as clipboard from "../../src/lib/clipboard";
import * as core from "../../src/core";
import * as download from "../../src/lib/download";
import * as sonner from "sonner";

describe("MermaidActions", () => {
const toastMock = vi.fn();
const MERMAID_CODE = "mermaid code";

afterEach(() => {
toastMock.mockClear();
vi.restoreAllMocks();
});

it("should call copyMermaidToClipboard when copy button is clicked", async () => {
const user = userEvent.setup();
const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON);
const copySpy = vi.spyOn(clipboard, "copyToClipboard").mockResolvedValue(undefined);
vi.spyOn(core, "exportToMermaid").mockReturnValue("mermaid code");
vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE);

renderWithProviders(<MermaidActions model={model!} />, { model });
const copyButton = screen.getByText(/Copy Mermaid Code/i);

const copyButton = screen.getByRole("button", {
name: /Copy Mermaid Code/i,
});

await user.click(copyButton);

expect(copySpy).toHaveBeenCalledWith(MERMAID_CODE);
});

it("should show error toast when clipboard copy fails", async () => {
const user = userEvent.setup();
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(<MermaidActions model={model!} />, { model });

const copyButton = screen.getByRole("button", {
name: /Copy Mermaid Code/i,
});

await user.click(copyButton);

expect(copySpy).toHaveBeenCalledWith("mermaid code");
expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Clipboard error" });
});

it("should call downloadMermaidFile when download button is clicked", async () => {
it("should call downloadMermaidFile and show success toast when download button is clicked", async () => {
const user = userEvent.setup();
const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON);
const downloadSpy = vi.spyOn(download, "downloadFile").mockImplementation(() => {});
vi.spyOn(core, "exportToMermaid").mockReturnValue("mermaid code");
vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE);
vi.spyOn(sonner.toast, "error").mockImplementation(toastMock);
vi.spyOn(sonner.toast, "success").mockImplementation(toastMock);

renderWithProviders(<MermaidActions model={model!} />, { model });
const downloadButton = screen.getByText(/Download as Mermaid File/i);

const downloadButton = screen.getByRole("button", {
name: /Download as Mermaid File/i,
});

await user.click(downloadButton);

expect(downloadSpy).toHaveBeenCalledWith(MERMAID_CODE, "test-wf.mmd");
expect(toastMock).toHaveBeenCalledWith(expect.any(String));
});

it("should show error toast when download fails", 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(<MermaidActions model={model!} />, { model });

const downloadButton = screen.getByRole("button", {
name: /Download as Mermaid File/i,
});

await user.click(downloadButton);

expect(downloadSpy).toHaveBeenCalledWith("mermaid code", "test-wf.mmd");
expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Download error" });
});
});
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ catalog:
react: ^19.2.7
react-dom: ^19.2.7
rimraf: ^6.1.3
sonner: ^2.0.7
storybook: ^10.4.6
syncpack: ^15.3.2
tailwindcss: ^4.3.1
Expand Down
Loading