diff --git a/apps/dokploy/components/dashboard/project/duplicate-project.tsx b/apps/dokploy/components/dashboard/project/duplicate-project.tsx index 038ddcb6..d8abea2d 100644 --- a/apps/dokploy/components/dashboard/project/duplicate-project.tsx +++ b/apps/dokploy/components/dashboard/project/duplicate-project.tsx @@ -15,6 +15,7 @@ import { Copy, Loader2 } from "lucide-react"; import { useRouter } from "next/router"; import { useState } from "react"; import { toast } from "sonner"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; export type Services = { appName: string; @@ -48,6 +49,7 @@ export const DuplicateProject = ({ const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [description, setDescription] = useState(""); + const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project" const utils = api.useUtils(); const router = useRouter(); @@ -59,9 +61,15 @@ export const DuplicateProject = ({ api.project.duplicate.useMutation({ onSuccess: async (newProject) => { await utils.project.all.invalidate(); - toast.success("Project duplicated successfully"); + toast.success( + duplicateType === "new-project" + ? "Project duplicated successfully" + : "Services duplicated successfully", + ); setOpen(false); - router.push(`/dashboard/project/${newProject.projectId}`); + if (duplicateType === "new-project") { + router.push(`/dashboard/project/${newProject.projectId}`); + } }, onError: (error) => { toast.error(error.message); @@ -69,7 +77,7 @@ export const DuplicateProject = ({ }); const handleDuplicate = async () => { - if (!name) { + if (duplicateType === "new-project" && !name) { toast.error("Project name is required"); return; } @@ -83,6 +91,7 @@ export const DuplicateProject = ({ id: service.id, type: service.type, })), + duplicateInSameProject: duplicateType === "same-project", }); }; @@ -95,6 +104,7 @@ export const DuplicateProject = ({ // Reset form when closing setName(""); setDescription(""); + setDuplicateType("new-project"); } }} > @@ -106,32 +116,54 @@ export const DuplicateProject = ({ - Duplicate Project + Duplicate Services - Create a new project with the selected services + Choose where to duplicate the selected services
- - setName(e.target.value)} - placeholder="New project name" - /> + + +
+ + +
+
+ + +
+
-
- - setDescription(e.target.value)} - placeholder="Project description (optional)" - /> -
+ {duplicateType === "new-project" && ( + <> +
+ + setName(e.target.value)} + placeholder="New project name" + /> +
+ +
+ + setDescription(e.target.value)} + placeholder="Project description (optional)" + /> +
+ + )}
@@ -159,10 +191,14 @@ export const DuplicateProject = ({ {isLoading ? ( <> - Duplicating... + {duplicateType === "new-project" + ? "Duplicating project..." + : "Duplicating services..."} + ) : duplicateType === "new-project" ? ( + "Duplicate project" ) : ( - "Duplicate" + "Duplicate services" )} diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index 05875450..b98c93fa 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -309,6 +309,7 @@ export const projectRouter = createTRPCRouter({ }), ) .optional(), + duplicateInSameProject: z.boolean().default(false), }), ) .mutation(async ({ ctx, input }) => { @@ -331,15 +332,17 @@ export const projectRouter = createTRPCRouter({ }); } - // Create new project - const newProject = await createProject( - { - name: input.name, - description: input.description, - env: sourceProject.env, - }, - ctx.session.activeOrganizationId, - ); + // Create new project or use existing one + const targetProject = input.duplicateInSameProject + ? sourceProject + : await createProject( + { + name: input.name, + description: input.description, + env: sourceProject.env, + }, + ctx.session.activeOrganizationId, + ); if (input.includeServices) { const servicesToDuplicate = input.selectedServices || []; @@ -362,7 +365,10 @@ export const projectRouter = createTRPCRouter({ const newApplication = await createApplication({ ...application, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${application.name} (copy)` + : application.name, + projectId: targetProject.projectId, }); for (const domain of domains) { @@ -423,7 +429,10 @@ export const projectRouter = createTRPCRouter({ const newPostgres = await createPostgres({ ...postgres, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${postgres.name} (copy)` + : postgres.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -449,7 +458,10 @@ export const projectRouter = createTRPCRouter({ await findMariadbById(id); const newMariadb = await createMariadb({ ...mariadb, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${mariadb.name} (copy)` + : mariadb.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -475,7 +487,10 @@ export const projectRouter = createTRPCRouter({ await findMongoById(id); const newMongo = await createMongo({ ...mongo, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${mongo.name} (copy)` + : mongo.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -501,7 +516,10 @@ export const projectRouter = createTRPCRouter({ await findMySqlById(id); const newMysql = await createMysql({ ...mysql, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${mysql.name} (copy)` + : mysql.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -526,7 +544,10 @@ export const projectRouter = createTRPCRouter({ const { redisId, mounts, ...redis } = await findRedisById(id); const newRedis = await createRedis({ ...redis, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${redis.name} (copy)` + : redis.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -545,7 +566,10 @@ export const projectRouter = createTRPCRouter({ await findComposeById(id); const newCompose = await createCompose({ ...compose, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${compose.name} (copy)` + : compose.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -572,21 +596,20 @@ export const projectRouter = createTRPCRouter({ }; // Duplicate selected services - for (const service of servicesToDuplicate) { await duplicateService(service.id, service.type); } } - if (ctx.user.role === "member") { + if (!input.duplicateInSameProject && ctx.user.role === "member") { await addNewProject( ctx.user.id, - newProject.projectId, + targetProject.projectId, ctx.session.activeOrganizationId, ); } - return newProject; + return targetProject; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST",