Merge pull request #761 from ln-12/json_view_copy_button
Add copy functionality to JsonView component
This commit is contained in:
commit
fc86dad130
1 changed files with 100 additions and 4 deletions
|
|
@ -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 type { JsonValue } from "@/utils/jsonUtils";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Copy, CheckCheck } from "lucide-react";
|
import { Copy, CheckCheck } from "lucide-react";
|
||||||
|
|
@ -101,6 +102,7 @@ const JsonNode = memo(
|
||||||
initialExpandDepth,
|
initialExpandDepth,
|
||||||
isError = false,
|
isError = false,
|
||||||
}: JsonNodeProps) => {
|
}: JsonNodeProps) => {
|
||||||
|
const { toast } = useToast();
|
||||||
const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
|
const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
|
||||||
const [typeStyleMap] = useState<Record<string, string>>({
|
const [typeStyleMap] = useState<Record<string, string>>({
|
||||||
number: "text-blue-600",
|
number: "text-blue-600",
|
||||||
|
|
@ -113,6 +115,52 @@ const JsonNode = memo(
|
||||||
});
|
});
|
||||||
const dataType = getDataType(data);
|
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 renderCollapsible = (isArray: boolean) => {
|
||||||
const items = isArray
|
const items = isArray
|
||||||
? (data as JsonValue[])
|
? (data as JsonValue[])
|
||||||
|
|
@ -206,7 +254,7 @@ const JsonNode = memo(
|
||||||
|
|
||||||
if (!isTooLong) {
|
if (!isTooLong) {
|
||||||
return (
|
return (
|
||||||
<div className="flex mr-1 rounded hover:bg-gray-800/20">
|
<div className="flex mr-1 rounded hover:bg-gray-800/20 group items-start">
|
||||||
{name && (
|
{name && (
|
||||||
<span className="mr-1 text-gray-600 dark:text-gray-400">
|
<span className="mr-1 text-gray-600 dark:text-gray-400">
|
||||||
{name}:
|
{name}:
|
||||||
|
|
@ -220,12 +268,28 @@ const JsonNode = memo(
|
||||||
>
|
>
|
||||||
"{value}"
|
"{value}"
|
||||||
</pre>
|
</pre>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCopyValue(value as unknown as JsonValue);
|
||||||
|
}}
|
||||||
|
aria-label={name ? `Copy value of ${name}` : "Copy value"}
|
||||||
|
title={name ? `Copy value of ${name}` : "Copy value"}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="size-4 text-foreground" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex mr-1 rounded group hover:bg-gray-800/20">
|
<div className="flex mr-1 rounded group hover:bg-gray-800/20 items-start">
|
||||||
{name && (
|
{name && (
|
||||||
<span className="mr-1 text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400">
|
<span className="mr-1 text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400">
|
||||||
{name}:
|
{name}:
|
||||||
|
|
@ -241,6 +305,22 @@ const JsonNode = memo(
|
||||||
>
|
>
|
||||||
{isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`}
|
{isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`}
|
||||||
</pre>
|
</pre>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCopyValue(value as unknown as JsonValue);
|
||||||
|
}}
|
||||||
|
aria-label={name ? `Copy value of ${name}` : "Copy value"}
|
||||||
|
title={name ? `Copy value of ${name}` : "Copy value"}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="size-4 text-foreground" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -253,7 +333,7 @@ const JsonNode = memo(
|
||||||
return renderString(data as string);
|
return renderString(data as string);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center mr-1 rounded hover:bg-gray-800/20">
|
<div className="flex items-center mr-1 rounded hover:bg-gray-800/20 group">
|
||||||
{name && (
|
{name && (
|
||||||
<span className="mr-1 text-gray-600 dark:text-gray-400">
|
<span className="mr-1 text-gray-600 dark:text-gray-400">
|
||||||
{name}:
|
{name}:
|
||||||
|
|
@ -262,6 +342,22 @@ const JsonNode = memo(
|
||||||
<span className={typeStyleMap[dataType] || typeStyleMap.default}>
|
<span className={typeStyleMap[dataType] || typeStyleMap.default}>
|
||||||
{data === null ? "null" : String(data)}
|
{data === null ? "null" : String(data)}
|
||||||
</span>
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCopyValue(data as JsonValue);
|
||||||
|
}}
|
||||||
|
aria-label={name ? `Copy value of ${name}` : "Copy value"}
|
||||||
|
title={name ? `Copy value of ${name}` : "Copy value"}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="size-4 text-foreground" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue