diff --git a/apps/builder/app/shared/notifications/subscription.tsx b/apps/builder/app/shared/notifications/subscription.tsx
index 6c453d138e3b..3cc70e729c34 100644
--- a/apps/builder/app/shared/notifications/subscription.tsx
+++ b/apps/builder/app/shared/notifications/subscription.tsx
@@ -122,6 +122,8 @@ export const startSubscription = () => {
const NEW_VERSION_TOAST_ID = "new-builder-version";
manager.subscribe("builderVersion", (serverVersion) => {
if (serverVersion !== publicStaticEnv.VERSION) {
+ const message =
+ "A new version of Webstudio is available. Reload to get the latest - see what's new at https://wstd.us/changelog";
toast.info(
<>
A new version of Webstudio is available. Reload to get the latest —
@@ -134,7 +136,11 @@ export const startSubscription = () => {
wstd.us/changelog
>,
- { id: NEW_VERSION_TOAST_ID, duration: Number.POSITIVE_INFINITY }
+ {
+ id: NEW_VERSION_TOAST_ID,
+ duration: Number.POSITIVE_INFINITY,
+ copyText: message,
+ }
);
}
});
diff --git a/packages/design-system/src/components/toast.test.tsx b/packages/design-system/src/components/toast.test.tsx
new file mode 100644
index 000000000000..2f91228734ab
--- /dev/null
+++ b/packages/design-system/src/components/toast.test.tsx
@@ -0,0 +1,26 @@
+import { describe, expect, test } from "vitest";
+import { __testingToast__ } from "./toast";
+
+const { getTextContent } = __testingToast__;
+
+describe("getTextContent", () => {
+ test("returns string toast content as-is", () => {
+ expect(getTextContent("Project saved successfully")).toBe(
+ "Project saved successfully"
+ );
+ });
+
+ test("extracts text from jsx toast content", () => {
+ expect(
+ getTextContent(
+ <>
+ A new version of Webstudio is available. Reload to get the latest -
+ see what's new at{" "}
+ wstd.us/changelog
+ >
+ )
+ ).toBe(
+ "A new version of Webstudio is available. Reload to get the latest - see what's new at wstd.us/changelog"
+ );
+ });
+});
diff --git a/packages/design-system/src/components/toast.tsx b/packages/design-system/src/components/toast.tsx
index 7f847cf0ebf0..e3ecbe38119b 100644
--- a/packages/design-system/src/components/toast.tsx
+++ b/packages/design-system/src/components/toast.tsx
@@ -1,4 +1,4 @@
-import type { JSX } from "react";
+import { Children, isValidElement, type JSX } from "react";
import * as ToastPrimitive from "@radix-ui/react-toast";
import hotToast, {
resolveValue,
@@ -343,6 +343,26 @@ const mapToVariant: Record = {
custom: "warning",
};
+const getTextContent = (node: React.ReactNode): string => {
+ if (node === undefined || node === null || typeof node === "boolean") {
+ return "";
+ }
+
+ if (typeof node === "string" || typeof node === "number") {
+ return String(node);
+ }
+
+ if (Array.isArray(node)) {
+ return node.map(getTextContent).join("");
+ }
+
+ if (isValidElement<{ children?: React.ReactNode }>(node)) {
+ return getTextContent(node.props.children);
+ }
+
+ return Children.toArray(node).map(getTextContent).join("");
+};
+
export const Toaster = () => {
const { toasts, handlers } = useToaster();
const { startPause, endPause } = handlers;
@@ -352,6 +372,10 @@ export const Toaster = () => {
{toasts.map((toastData) => {
const toastVariant = mapToVariant[toastData.type];
const children = resolveValue(toastData.message, toastData);
+ const copyText =
+ "copyText" in toastData && typeof toastData.copyText === "string"
+ ? toastData.copyText
+ : getTextContent(children);
return (
{
hotToast.remove(toastData.id);
}}
onCopy={() => {
- navigator.clipboard.writeText(children?.toString() ?? "");
+ navigator.clipboard.writeText(copyText);
}}
icon={toastData.icon}
>
@@ -380,7 +404,9 @@ export const Toaster = () => {
);
};
-type Options = Pick;
+type Options = Pick & {
+ copyText?: string;
+};
export const toast = {
info: (value: JSX.Element | string, options?: Options) =>
@@ -393,3 +419,7 @@ export const toast = {
hotToast.success(value, options),
dismiss: hotToast.dismiss,
};
+
+export const __testingToast__ = {
+ getTextContent,
+};