Merge branch 'main' into feat/add-resource-link-support
This commit is contained in:
commit
c9ab61b09a
2 changed files with 132 additions and 10 deletions
|
|
@ -5,6 +5,8 @@ import JsonEditor from "./JsonEditor";
|
|||
import { updateValueAtPath } from "@/utils/jsonUtils";
|
||||
import { generateDefaultValue } from "@/utils/schemaUtils";
|
||||
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
import { CheckCheck, Copy } from "lucide-react";
|
||||
|
||||
interface DynamicJsonFormProps {
|
||||
schema: JsonSchemaType;
|
||||
|
|
@ -60,6 +62,9 @@ const DynamicJsonForm = ({
|
|||
const isOnlyJSON = !isSimpleObject(schema);
|
||||
const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON);
|
||||
const [jsonError, setJsonError] = useState<string>();
|
||||
const [copiedJson, setCopiedJson] = useState<boolean>(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Store the raw JSON string to allow immediate feedback during typing
|
||||
// while deferring parsing until the user stops typing
|
||||
const [rawJsonValue, setRawJsonValue] = useState<string>(
|
||||
|
|
@ -399,19 +404,64 @@ const DynamicJsonForm = ({
|
|||
}
|
||||
}, [shouldUseJsonMode, isJsonMode]);
|
||||
|
||||
const handleCopyJson = useCallback(() => {
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(
|
||||
JSON.stringify(value, null, 2) ?? "[]",
|
||||
);
|
||||
setCopiedJson(true);
|
||||
|
||||
toast({
|
||||
title: "JSON copied",
|
||||
description:
|
||||
"The JSON data has been successfully copied to your clipboard.",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setCopiedJson(false);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to copy JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
copyToClipboard();
|
||||
}, [toast, value]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-end space-x-2">
|
||||
{isJsonMode && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={formatJson}
|
||||
>
|
||||
Format JSON
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyJson}
|
||||
>
|
||||
{copiedJson ? (
|
||||
<CheckCheck className="h-4 w-4 mr-2" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Copy JSON
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={formatJson}
|
||||
>
|
||||
Format JSON
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isOnlyJSON && (
|
||||
<Button variant="outline" size="sm" onClick={handleSwitchToFormMode}>
|
||||
{isJsonMode ? "Switch to Form" : "Switch to JSON"}
|
||||
|
|
|
|||
|
|
@ -117,8 +117,11 @@ describe("DynamicJsonForm Complex Fields", () => {
|
|||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveProperty("type", "textarea");
|
||||
const buttons = screen.getAllByRole("button");
|
||||
expect(buttons).toHaveLength(1);
|
||||
expect(buttons[0]).toHaveProperty("textContent", "Format JSON");
|
||||
expect(buttons).toHaveLength(2); // Copy JSON + Format JSON
|
||||
const copyButton = screen.getByRole("button", { name: /copy json/i });
|
||||
const formatButton = screen.getByRole("button", { name: /format json/i });
|
||||
expect(copyButton).toBeTruthy();
|
||||
expect(formatButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should pass changed values to onChange", () => {
|
||||
|
|
@ -137,3 +140,72 @@ describe("DynamicJsonForm Complex Fields", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("DynamicJsonForm Copy JSON Functionality", () => {
|
||||
const mockWriteText = jest.fn(() => Promise.resolve());
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: mockWriteText,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const renderFormInJsonMode = (props = {}) => {
|
||||
const defaultProps = {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
nested: { oneOf: [{ type: "string" }, { type: "integer" }] },
|
||||
},
|
||||
} as unknown as JsonSchemaType,
|
||||
value: { nested: "test value" },
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
return render(<DynamicJsonForm {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
describe("Copy JSON Button", () => {
|
||||
it("should render Copy JSON button when in JSON mode", () => {
|
||||
renderFormInJsonMode();
|
||||
|
||||
const copyButton = screen.getByRole("button", { name: "Copy JSON" });
|
||||
expect(copyButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not render Copy JSON button when not in JSON mode", () => {
|
||||
const simpleSchema = {
|
||||
type: "string" as const,
|
||||
description: "Test string field",
|
||||
};
|
||||
|
||||
render(
|
||||
<DynamicJsonForm
|
||||
schema={simpleSchema}
|
||||
value="test"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const copyButton = screen.queryByRole("button", { name: "Copy JSON" });
|
||||
expect(copyButton).toBeNull();
|
||||
});
|
||||
|
||||
it("should copy JSON to clipboard when clicked", async () => {
|
||||
const testValue = { nested: "test value", number: 42 };
|
||||
|
||||
renderFormInJsonMode({ value: testValue });
|
||||
|
||||
const copyButton = screen.getByRole("button", { name: "Copy JSON" });
|
||||
fireEvent.click(copyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith(
|
||||
JSON.stringify(testValue, null, 2),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue