Skip to content

Commit 416fc15

Browse files
authored
Merge branch 'main' into patch-1
2 parents 7d5f796 + b39528d commit 416fc15

File tree

2 files changed

+132
-10
lines changed

2 files changed

+132
-10
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import JsonEditor from "./JsonEditor";
55
import { updateValueAtPath } from "@/utils/jsonUtils";
66
import { generateDefaultValue } from "@/utils/schemaUtils";
77
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
8+
import { useToast } from "@/lib/hooks/useToast";
9+
import { CheckCheck, Copy } from "lucide-react";
810

911
interface DynamicJsonFormProps {
1012
schema: JsonSchemaType;
@@ -60,6 +62,9 @@ const DynamicJsonForm = ({
6062
const isOnlyJSON = !isSimpleObject(schema);
6163
const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON);
6264
const [jsonError, setJsonError] = useState<string>();
65+
const [copiedJson, setCopiedJson] = useState<boolean>(false);
66+
const { toast } = useToast();
67+
6368
// Store the raw JSON string to allow immediate feedback during typing
6469
// while deferring parsing until the user stops typing
6570
const [rawJsonValue, setRawJsonValue] = useState<string>(
@@ -399,19 +404,64 @@ const DynamicJsonForm = ({
399404
}
400405
}, [shouldUseJsonMode, isJsonMode]);
401406

407+
const handleCopyJson = useCallback(() => {
408+
const copyToClipboard = async () => {
409+
try {
410+
await navigator.clipboard.writeText(
411+
JSON.stringify(value, null, 2) ?? "[]",
412+
);
413+
setCopiedJson(true);
414+
415+
toast({
416+
title: "JSON copied",
417+
description:
418+
"The JSON data has been successfully copied to your clipboard.",
419+
});
420+
421+
setTimeout(() => {
422+
setCopiedJson(false);
423+
}, 2000);
424+
} catch (error) {
425+
toast({
426+
title: "Error",
427+
description: `Failed to copy JSON: ${error instanceof Error ? error.message : String(error)}`,
428+
variant: "destructive",
429+
});
430+
}
431+
};
432+
433+
copyToClipboard();
434+
}, [toast, value]);
435+
402436
return (
403437
<div className="space-y-4">
404438
<div className="flex justify-end space-x-2">
405439
{isJsonMode && (
406-
<Button
407-
type="button"
408-
variant="outline"
409-
size="sm"
410-
onClick={formatJson}
411-
>
412-
Format JSON
413-
</Button>
440+
<>
441+
<Button
442+
type="button"
443+
variant="outline"
444+
size="sm"
445+
onClick={handleCopyJson}
446+
>
447+
{copiedJson ? (
448+
<CheckCheck className="h-4 w-4 mr-2" />
449+
) : (
450+
<Copy className="h-4 w-4 mr-2" />
451+
)}
452+
Copy JSON
453+
</Button>
454+
<Button
455+
type="button"
456+
variant="outline"
457+
size="sm"
458+
onClick={formatJson}
459+
>
460+
Format JSON
461+
</Button>
462+
</>
414463
)}
464+
415465
{!isOnlyJSON && (
416466
<Button variant="outline" size="sm" onClick={handleSwitchToFormMode}>
417467
{isJsonMode ? "Switch to Form" : "Switch to JSON"}

client/src/components/__tests__/DynamicJsonForm.test.tsx

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,11 @@ describe("DynamicJsonForm Complex Fields", () => {
117117
const input = screen.getByRole("textbox");
118118
expect(input).toHaveProperty("type", "textarea");
119119
const buttons = screen.getAllByRole("button");
120-
expect(buttons).toHaveLength(1);
121-
expect(buttons[0]).toHaveProperty("textContent", "Format JSON");
120+
expect(buttons).toHaveLength(2); // Copy JSON + Format JSON
121+
const copyButton = screen.getByRole("button", { name: /copy json/i });
122+
const formatButton = screen.getByRole("button", { name: /format json/i });
123+
expect(copyButton).toBeTruthy();
124+
expect(formatButton).toBeTruthy();
122125
});
123126

124127
it("should pass changed values to onChange", () => {
@@ -137,3 +140,72 @@ describe("DynamicJsonForm Complex Fields", () => {
137140
});
138141
});
139142
});
143+
144+
describe("DynamicJsonForm Copy JSON Functionality", () => {
145+
const mockWriteText = jest.fn(() => Promise.resolve());
146+
147+
beforeEach(() => {
148+
jest.clearAllMocks();
149+
Object.assign(navigator, {
150+
clipboard: {
151+
writeText: mockWriteText,
152+
},
153+
});
154+
});
155+
156+
const renderFormInJsonMode = (props = {}) => {
157+
const defaultProps = {
158+
schema: {
159+
type: "object",
160+
properties: {
161+
nested: { oneOf: [{ type: "string" }, { type: "integer" }] },
162+
},
163+
} as unknown as JsonSchemaType,
164+
value: { nested: "test value" },
165+
onChange: jest.fn(),
166+
};
167+
return render(<DynamicJsonForm {...defaultProps} {...props} />);
168+
};
169+
170+
describe("Copy JSON Button", () => {
171+
it("should render Copy JSON button when in JSON mode", () => {
172+
renderFormInJsonMode();
173+
174+
const copyButton = screen.getByRole("button", { name: "Copy JSON" });
175+
expect(copyButton).toBeTruthy();
176+
});
177+
178+
it("should not render Copy JSON button when not in JSON mode", () => {
179+
const simpleSchema = {
180+
type: "string" as const,
181+
description: "Test string field",
182+
};
183+
184+
render(
185+
<DynamicJsonForm
186+
schema={simpleSchema}
187+
value="test"
188+
onChange={jest.fn()}
189+
/>,
190+
);
191+
192+
const copyButton = screen.queryByRole("button", { name: "Copy JSON" });
193+
expect(copyButton).toBeNull();
194+
});
195+
196+
it("should copy JSON to clipboard when clicked", async () => {
197+
const testValue = { nested: "test value", number: 42 };
198+
199+
renderFormInJsonMode({ value: testValue });
200+
201+
const copyButton = screen.getByRole("button", { name: "Copy JSON" });
202+
fireEvent.click(copyButton);
203+
204+
await waitFor(() => {
205+
expect(mockWriteText).toHaveBeenCalledWith(
206+
JSON.stringify(testValue, null, 2),
207+
);
208+
});
209+
});
210+
});
211+
});

0 commit comments

Comments
 (0)