diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 1febff6a..227c77b6 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -1,4 +1,5 @@ -import { useState, memo, useMemo, useCallback } from "react"; +import { useState, memo, useMemo, useCallback, useEffect } from "react"; +import type React from "react"; import type { JsonValue } from "@/utils/jsonUtils"; import clsx from "clsx"; import { Copy, CheckCheck } from "lucide-react"; @@ -101,6 +102,7 @@ const JsonNode = memo( initialExpandDepth, isError = false, }: JsonNodeProps) => { + const { toast } = useToast(); const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth); const [typeStyleMap] = useState>({ number: "text-blue-600", @@ -113,6 +115,52 @@ const JsonNode = memo( }); const dataType = getDataType(data); + const [copied, setCopied] = useState(false); + useEffect(() => { + let timeoutId: NodeJS.Timeout; + if (copied) { + timeoutId = setTimeout(() => setCopied(false), 500); + } + return () => { + if (timeoutId) clearTimeout(timeoutId); + }; + }, [copied]); + + const handleCopyValue = useCallback( + (value: JsonValue) => { + try { + let text: string; + const valueType = getDataType(value); + switch (valueType) { + case "string": + text = value as unknown as string; + break; + case "number": + case "boolean": + text = String(value); + break; + case "null": + text = "null"; + break; + case "undefined": + text = "undefined"; + break; + default: + text = JSON.stringify(value); + } + navigator.clipboard.writeText(text); + setCopied(true); + } catch (error) { + toast({ + title: "Error", + description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`, + variant: "destructive", + }); + } + }, + [toast], + ); + const renderCollapsible = (isArray: boolean) => { const items = isArray ? (data as JsonValue[]) @@ -206,7 +254,7 @@ const JsonNode = memo( if (!isTooLong) { return ( -
+
{name && ( {name}: @@ -220,12 +268,28 @@ const JsonNode = memo( > "{value}" +
); } return ( -
+
{name && ( {name}: @@ -241,6 +305,22 @@ const JsonNode = memo( > {isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`} +
); }; @@ -253,7 +333,7 @@ const JsonNode = memo( return renderString(data as string); default: return ( -
+
{name && ( {name}: @@ -262,6 +342,22 @@ const JsonNode = memo( {data === null ? "null" : String(data)} +
); }