Skip to content

Commit 2ad8856

Browse files
feat(reactotron-core-ui): add copy button for nested objects in API response (#1597)
## Please verify the following: - [x] `yarn build-and-test:local` passes - [ ] I have added tests for any new features, if relevant - [ ] `README.md` (or relevant documentation) has been updated with your changes --- # Describe your PR Adds a **Copy button for nested objects** in the `TreeView` component inside the **Response Body** tab. This allows users to quickly copy the JSON representation of any nested object in an API response without manually selecting or expanding large sections of the tree. --- # Motivation Previously, Reactotron only allowed copying **top-level values**. When API responses contained deeply nested objects, users had to manually expand the tree and select the text to copy. This change enables **one-click copying of any nested object**, improving the developer experience when inspecting API responses. --- # Implementation - **TreeView/index.tsx** - Accepts an optional `copyToClipboard` prop - Renders a `ButtonCopy` next to object-type nodes (e.g. `{3}`) - Copies `JSON.stringify(data, null, 2)` when clicked - Uses `event.stopPropagation()` so clicking the button does not toggle the node - **ContentView/index.tsx** - Passes `copyToClipboard` down to `TreeView` - **ApiResponseCommand/index.tsx** - Provides the existing `copyToClipboard` handler to `ContentView` for the **Response Body** tab --- # Test Plan and Preview 1. Open Reactotron and trigger an API request with nested objects 2. Navigate to the **Response Body** tab 3. Verify a **Copy** button appears next to nested objects 4. Click the button and confirm the **prettified JSON** is copied to the clipboard 5. Ensure clicking **Copy** does **not expand/collapse** the tree node <img width="1112" height="1868" alt="image" src="https://github.com/user-attachments/assets/6839b931-cd94-486b-a82f-29297d79029b" /> Co-authored-by: Deepak Mukka <deepak.mukka@gullak.money>
1 parent 96df54c commit 2ad8856

3 files changed

Lines changed: 36 additions & 5 deletions

File tree

lib/reactotron-core-ui/src/components/ContentView/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ interface Props {
2121
// value: object | string | number | boolean | null | undefined
2222
value: any
2323
treeLevel?: number
24+
copyToClipboard?: (text: string) => void
2425
}
2526

26-
export default function ContentView({ value, treeLevel }: Props) {
27+
export default function ContentView({ value, treeLevel, copyToClipboard }: Props) {
2728
if (value === null) return <NullContainer>null</NullContainer>
2829
if (value === undefined) return <UndefinedContainer>undefined</UndefinedContainer>
2930

@@ -54,7 +55,7 @@ export default function ContentView({ value, treeLevel }: Props) {
5455
return isShallow(checkValue) ? (
5556
makeTable(checkValue)
5657
) : (
57-
<TreeView value={checkValue} level={treeLevel} />
58+
<TreeView value={checkValue} level={treeLevel} copyToClipboard={copyToClipboard} />
5859
)
5960
}
6061

lib/reactotron-core-ui/src/components/TreeView/index.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ const theme = {
2929

3030
const MutedContainer = styled.span`
3131
color: ${(props) => props.theme.highlight};
32+
display: inline-flex;
33+
`
34+
35+
const ButtonCopy = styled.button`
36+
margin-left: 6px;
37+
padding: 0 6px;
38+
font-size: 10px;
39+
border: none;
40+
border-radius: 3px;
41+
cursor: pointer;
42+
color: ${(props) => props.theme.background};
43+
background-color: ${(props) => props.theme.highlight};
44+
45+
&:hover {
46+
opacity: 0.85;
47+
}
3248
`
3349

3450
const getTreeTheme = (baseTheme: ReactotronTheme) => ({
@@ -41,9 +57,10 @@ interface Props {
4157
// value: object
4258
value: any
4359
level?: number
60+
copyToClipboard?: (text: string) => void
4461
}
4562

46-
export default function TreeView({ value, level = 1 }: Props) {
63+
export default function TreeView({ value, level = 1, copyToClipboard }: Props) {
4764
const colorScheme = useColorScheme()
4865

4966
return (
@@ -54,7 +71,18 @@ export default function TreeView({ value, level = 1 }: Props) {
5471
theme={getTreeTheme(themes[colorScheme])}
5572
getItemString={(type, data, itemType, itemString) => {
5673
if (type === "Object") {
57-
return <MutedContainer>{itemType}</MutedContainer>
74+
const handleCopy = copyToClipboard
75+
? (event: React.MouseEvent) => {
76+
event.stopPropagation()
77+
copyToClipboard(JSON.stringify(data, null, 2))
78+
}
79+
: undefined
80+
return (
81+
<MutedContainer>
82+
{itemType}
83+
{handleCopy && <ButtonCopy onClick={handleCopy}>Copy</ButtonCopy>}
84+
</MutedContainer>
85+
)
5886
}
5987

6088
return (

lib/reactotron-core-ui/src/timelineCommands/ApiResponseCommand/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,9 @@ const ApiResponseCommand: FunctionComponent<Props> = ({
167167
{!!request.params && tabBuilder(Tab.RequestParams, "Request Params")}
168168
{tabBuilder(Tab.RequestHeaders, "Request Headers")}
169169
</TabsContainer>
170-
{onTab === Tab.ResponseBody && <ContentView value={response.body} />}
170+
{onTab === Tab.ResponseBody && (
171+
<ContentView value={response.body} copyToClipboard={copyToClipboard} />
172+
)}
171173
{onTab === Tab.ResponseHeaders && <ContentView value={response.headers} />}
172174
{onTab === Tab.RequestBody && <ContentView value={request.data} treeLevel={1} />}
173175
{onTab === Tab.RequestParams && <ContentView value={request.params} />}

0 commit comments

Comments
 (0)