diff --git a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx index 915b2159d..e12f27229 100644 --- a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx +++ b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx @@ -31,13 +31,11 @@ import { findEnvironmentById } from "@dokploy/server"; type Environment = Awaited>; interface AdvancedEnvironmentSelectorProps { projectId: string; - environments: Environment[]; currentEnvironmentId?: string; } export const AdvancedEnvironmentSelector = ({ projectId, - environments, currentEnvironmentId, }: AdvancedEnvironmentSelectorProps) => { const router = useRouter(); @@ -46,6 +44,11 @@ export const AdvancedEnvironmentSelector = ({ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedEnvironment, setSelectedEnvironment] = useState(null); + const { data: project } = api.project.one.useQuery({ projectId },{ + enabled: !!projectId, + }); + const environments = project?.environments || []; + // Form states const [name, setName] = useState(""); const [description, setDescription] = useState(""); diff --git a/apps/dokploy/components/dashboard/project/duplicate-project.tsx b/apps/dokploy/components/dashboard/project/duplicate-project.tsx index 91e695fa0..b9cdf56c4 100644 --- a/apps/dokploy/components/dashboard/project/duplicate-project.tsx +++ b/apps/dokploy/components/dashboard/project/duplicate-project.tsx @@ -15,6 +15,13 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { api } from "@/utils/api"; export type Services = { @@ -36,23 +43,32 @@ export type Services = { }; interface DuplicateProjectProps { - projectId: string; + environmentId: string; services: Services[]; selectedServiceIds: string[]; } export const DuplicateProject = ({ - projectId, + environmentId, services, selectedServiceIds, }: DuplicateProjectProps) => { 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 [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "existing-environment" + const [selectedTargetProject, setSelectedTargetProject] = useState(""); + const [selectedTargetEnvironment, setSelectedTargetEnvironment] = useState(""); const utils = api.useUtils(); const router = useRouter(); + // Queries for project and environment selection + const { data: allProjects } = api.project.all.useQuery(); + const { data: selectedProjectEnvironments } = api.environment.byProjectId.useQuery( + { projectId: selectedTargetProject }, + { enabled: !!selectedTargetProject } + ); + const selectedServices = services.filter((service) => selectedServiceIds.includes(service.id), ); @@ -68,7 +84,7 @@ export const DuplicateProject = ({ ); setOpen(false); if (duplicateType === "new-project") { - router.push(`/dashboard/project/${newProject.projectId}`); + router.push(`/dashboard/project/${newProject?.projectId}/environment/${newProject?.environmentId}`); } }, onError: (error) => { @@ -82,8 +98,20 @@ export const DuplicateProject = ({ return; } + if (duplicateType === "existing-environment") { + if (!selectedTargetProject) { + toast.error("Please select a target project"); + return; + } + if (!selectedTargetEnvironment) { + toast.error("Please select a target environment"); + return; + } + } + + // TODO: Update duplicate API to support targetProjectId and targetEnvironmentId await duplicateProject({ - sourceProjectId: projectId, + sourceEnvironmentId: selectedTargetEnvironment, name, description, includeServices: true, @@ -105,6 +133,8 @@ export const DuplicateProject = ({ setName(""); setDescription(""); setDuplicateType("new-project"); + setSelectedTargetProject(""); + setSelectedTargetEnvironment(""); } }} > @@ -127,7 +157,14 @@ export const DuplicateProject = ({ { + setDuplicateType(value); + // Reset selections when changing type + if (value !== "existing-environment") { + setSelectedTargetProject(""); + setSelectedTargetEnvironment(""); + } + }} className="grid gap-2" >
@@ -135,8 +172,8 @@ export const DuplicateProject = ({
- - + +
@@ -165,6 +202,73 @@ export const DuplicateProject = ({ )} + {duplicateType === "existing-environment" && ( + <> + {allProjects?.filter((p) => p.projectId !== environmentId).length === 0 ? ( +
+

+ No other projects available. Create a new project first. +

+
+ ) : ( + <> + {/* Step 1: Select Project */} +
+ + +
+ + {/* Step 2: Select Environment (only show if project is selected) */} + {selectedTargetProject && ( +
+ + +
+ )} + + )} + + )} +
@@ -187,18 +291,25 @@ export const DuplicateProject = ({ > Cancel - diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index 5586d90fe..ad051ea52 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -790,7 +790,6 @@ const EnvironmentPage = ( @@ -957,7 +956,7 @@ const EnvironmentPage = ( diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index b0027b1d0..42797a5ae 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -19,6 +19,7 @@ import { deleteProject, findApplicationById, findComposeById, + findEnvironmentById, findMariadbById, findMemberById, findMongoById, @@ -80,7 +81,7 @@ export const projectRouter = createTRPCRouter({ if (ctx.user.role === "member") { await addNewProject( ctx.user.id, - project.projectId, + project.project.projectId, ctx.session.activeOrganizationId, ); } @@ -312,7 +313,7 @@ export const projectRouter = createTRPCRouter({ duplicate: protectedProcedure .input( z.object({ - sourceProjectId: z.string(), + sourceEnvironmentId: z.string(), name: z.string(), description: z.string().optional(), includeServices: z.boolean().default(true), @@ -346,9 +347,10 @@ export const projectRouter = createTRPCRouter({ } // Get source project - const sourceProject = await findProjectById(input.sourceProjectId); + const sourceEnvironment = input.duplicateInSameProject ? await findEnvironmentById(input.sourceEnvironmentId) : null; - if (sourceProject.organizationId !== ctx.session.activeOrganizationId) { + + if (input.duplicateInSameProject &&sourceEnvironment?.project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", @@ -357,15 +359,18 @@ export const projectRouter = createTRPCRouter({ // Create new project or use existing one const targetProject = input.duplicateInSameProject - ? sourceProject + ? sourceEnvironment : await createProject( { name: input.name, description: input.description, - env: sourceProject.env, + env: sourceEnvironment?.project.env, }, ctx.session.activeOrganizationId, - ); + ).then((value) => value.environment); + + console.log("targetProject", targetProject); + if (input.includeServices) { const servicesToDuplicate = input.selectedServices || []; @@ -398,7 +403,7 @@ export const projectRouter = createTRPCRouter({ name: input.duplicateInSameProject ? `${application.name} (copy)` : application.name, - projectId: targetProject.projectId, + environmentId: targetProject?.environmentId || "", }); for (const domain of domains) { @@ -468,7 +473,7 @@ export const projectRouter = createTRPCRouter({ name: input.duplicateInSameProject ? `${postgres.name} (copy)` : postgres.name, - projectId: targetProject.projectId, + environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { @@ -504,7 +509,7 @@ export const projectRouter = createTRPCRouter({ name: input.duplicateInSameProject ? `${mariadb.name} (copy)` : mariadb.name, - projectId: targetProject.projectId, + environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { @@ -540,7 +545,7 @@ export const projectRouter = createTRPCRouter({ name: input.duplicateInSameProject ? `${mongo.name} (copy)` : mongo.name, - projectId: targetProject.projectId, + environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { @@ -576,7 +581,7 @@ export const projectRouter = createTRPCRouter({ name: input.duplicateInSameProject ? `${mysql.name} (copy)` : mysql.name, - projectId: targetProject.projectId, + environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { @@ -612,7 +617,7 @@ export const projectRouter = createTRPCRouter({ name: input.duplicateInSameProject ? `${redis.name} (copy)` : redis.name, - projectId: targetProject.projectId, + environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { @@ -647,7 +652,7 @@ export const projectRouter = createTRPCRouter({ name: input.duplicateInSameProject ? `${compose.name} (copy)` : compose.name, - projectId: targetProject.projectId, + environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { @@ -682,7 +687,7 @@ export const projectRouter = createTRPCRouter({ if (!input.duplicateInSameProject && ctx.user.role === "member") { await addNewProject( ctx.user.id, - targetProject.projectId, + targetProject?.projectId || "", ctx.session.activeOrganizationId, ); } diff --git a/packages/server/src/services/project.ts b/packages/server/src/services/project.ts index 1633c8866..fb8bd8635 100644 --- a/packages/server/src/services/project.ts +++ b/packages/server/src/services/project.ts @@ -36,14 +36,13 @@ export const createProject = async ( } // Automatically create a production environment - try { - await createProductionEnvironment(newProject.projectId); - } catch (error) { - console.error("Error creating production environment:", error); - // Don't fail project creation if environment creation fails - } + const newEnvironment = await createProductionEnvironment(newProject.projectId); + return { + project: newProject, + environment: newEnvironment, + }; + - return newProject; }; export const findProjectById = async (projectId: string) => {