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, +};