From 45a1fb88fa3304f280e1e4419d4c1239508f0abb Mon Sep 17 00:00:00 2001 From: ravindu0823 Date: Tue, 9 Jun 2026 14:39:42 +0530 Subject: [PATCH 1/2] feat: add copy button to User and Database Name credential fields The Internal Credentials cards rendered User and Database Name as plain disabled inputs with no copy affordance, unlike the Password and Internal Host fields. Add a new CopyInput component (a visible input paired with a copy button) and use it for the User and Database Name fields across all database types. Fixes #4495 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dokploy/__test__/utils/copy-input.test.ts | 32 +++++++++++++++++++ .../show-internal-libsql-credentials.tsx | 3 +- .../show-internal-mariadb-credentials.tsx | 5 +-- .../show-internal-mongo-credentials.tsx | 3 +- .../show-internal-mysql-credentials.tsx | 5 +-- .../show-internal-postgres-credentials.tsx | 5 +-- .../show-internal-redis-credentials.tsx | 3 +- apps/dokploy/components/shared/copy-input.tsx | 26 +++++++++++++++ 8 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 apps/dokploy/__test__/utils/copy-input.test.ts create mode 100644 apps/dokploy/components/shared/copy-input.tsx diff --git a/apps/dokploy/__test__/utils/copy-input.test.ts b/apps/dokploy/__test__/utils/copy-input.test.ts new file mode 100644 index 0000000000..90fca63daa --- /dev/null +++ b/apps/dokploy/__test__/utils/copy-input.test.ts @@ -0,0 +1,32 @@ +import copy from "copy-to-clipboard"; +import { toast } from "sonner"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { copyToClipboard } from "@/components/shared/copy-input"; + +vi.mock("copy-to-clipboard", () => ({ default: vi.fn() })); +vi.mock("sonner", () => ({ toast: { success: vi.fn() } })); + +describe("copyToClipboard", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("copies the value to the clipboard", () => { + copyToClipboard("admin"); + expect(copy).toHaveBeenCalledWith("admin"); + }); + + test("shows a success toast", () => { + copyToClipboard("admin"); + expect(toast.success).toHaveBeenCalledWith("Value is copied to clipboard"); + }); + + test("returns the copied value", () => { + expect(copyToClipboard("admin")).toBe("admin"); + }); + + test("falls back to an empty string for undefined", () => { + expect(copyToClipboard(undefined)).toBe(""); + expect(copy).toHaveBeenCalledWith(""); + }); +}); diff --git a/apps/dokploy/components/dashboard/libsql/general/show-internal-libsql-credentials.tsx b/apps/dokploy/components/dashboard/libsql/general/show-internal-libsql-credentials.tsx index 6c13502426..19128fe3c7 100644 --- a/apps/dokploy/components/dashboard/libsql/general/show-internal-libsql-credentials.tsx +++ b/apps/dokploy/components/dashboard/libsql/general/show-internal-libsql-credentials.tsx @@ -1,4 +1,5 @@ import { SelectGroup } from "@radix-ui/react-select"; +import { CopyInput } from "@/components/shared/copy-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -28,7 +29,7 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
- +
diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx index 04d431fa1e..db90b2c3a1 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx @@ -1,4 +1,5 @@ import { toast } from "sonner"; +import { CopyInput } from "@/components/shared/copy-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -25,11 +26,11 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
- +
- +
diff --git a/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx b/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx index ef38fe3cbc..ec8769ad60 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx @@ -1,4 +1,5 @@ import { toast } from "sonner"; +import { CopyInput } from "@/components/shared/copy-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -25,7 +26,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
- +
diff --git a/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx b/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx index 39937badde..6c723ba6b9 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx @@ -1,4 +1,5 @@ import { toast } from "sonner"; +import { CopyInput } from "@/components/shared/copy-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -25,11 +26,11 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
- +
- +
diff --git a/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx b/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx index 3d1b1032e8..e120b081b5 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx @@ -1,4 +1,5 @@ import { toast } from "sonner"; +import { CopyInput } from "@/components/shared/copy-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -25,11 +26,11 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
- +
- +
diff --git a/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx b/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx index 3dd6814a24..0681fcf303 100644 --- a/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx +++ b/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx @@ -1,4 +1,5 @@ import { toast } from "sonner"; +import { CopyInput } from "@/components/shared/copy-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -25,7 +26,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
- +
diff --git a/apps/dokploy/components/shared/copy-input.tsx b/apps/dokploy/components/shared/copy-input.tsx new file mode 100644 index 0000000000..f7e1360e1d --- /dev/null +++ b/apps/dokploy/components/shared/copy-input.tsx @@ -0,0 +1,26 @@ +import copy from "copy-to-clipboard"; +import { Clipboard } from "lucide-react"; +import { toast } from "sonner"; +import { Button } from "../ui/button"; +import { Input, type InputProps } from "../ui/input"; + +export const copyToClipboard = (value: string | undefined): string => { + const text = value ?? ""; + copy(text); + toast.success("Value is copied to clipboard"); + return text; +}; + +export const CopyInput = ({ ...props }: InputProps) => { + return ( +
+ + +
+ ); +}; From c7f6f36a4399bb535494fef1347b12ac38637c9b Mon Sep 17 00:00:00 2001 From: ravindu0823 Date: Tue, 9 Jun 2026 14:57:19 +0530 Subject: [PATCH 2/2] fix: mark CopyInput button as type="button" to avoid accidental form submits Address review: the shared Button does not default type to "button", so a typeless button acts as type="submit". CopyInput is a reusable component; if dropped inside a form its copy action would submit the form. Set type="button" explicitly. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/dokploy/components/shared/copy-input.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dokploy/components/shared/copy-input.tsx b/apps/dokploy/components/shared/copy-input.tsx index f7e1360e1d..23ccb1ea8e 100644 --- a/apps/dokploy/components/shared/copy-input.tsx +++ b/apps/dokploy/components/shared/copy-input.tsx @@ -16,6 +16,7 @@ export const CopyInput = ({ ...props }: InputProps) => {