mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: enhance project duplication functionality with options for new or same project
This commit is contained in:
@@ -15,6 +15,7 @@ import { Copy, Loader2 } from "lucide-react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -48,6 +49,7 @@ export const DuplicateProject = ({
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -59,9 +61,15 @@ export const DuplicateProject = ({
|
|||||||
api.project.duplicate.useMutation({
|
api.project.duplicate.useMutation({
|
||||||
onSuccess: async (newProject) => {
|
onSuccess: async (newProject) => {
|
||||||
await utils.project.all.invalidate();
|
await utils.project.all.invalidate();
|
||||||
toast.success("Project duplicated successfully");
|
toast.success(
|
||||||
|
duplicateType === "new-project"
|
||||||
|
? "Project duplicated successfully"
|
||||||
|
: "Services duplicated successfully",
|
||||||
|
);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
router.push(`/dashboard/project/${newProject.projectId}`);
|
if (duplicateType === "new-project") {
|
||||||
|
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
@@ -69,7 +77,7 @@ export const DuplicateProject = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleDuplicate = async () => {
|
const handleDuplicate = async () => {
|
||||||
if (!name) {
|
if (duplicateType === "new-project" && !name) {
|
||||||
toast.error("Project name is required");
|
toast.error("Project name is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -83,6 +91,7 @@ export const DuplicateProject = ({
|
|||||||
id: service.id,
|
id: service.id,
|
||||||
type: service.type,
|
type: service.type,
|
||||||
})),
|
})),
|
||||||
|
duplicateInSameProject: duplicateType === "same-project",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,6 +104,7 @@ export const DuplicateProject = ({
|
|||||||
// Reset form when closing
|
// Reset form when closing
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
setDuplicateType("new-project");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -106,32 +116,54 @@ export const DuplicateProject = ({
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Duplicate Project</DialogTitle>
|
<DialogTitle>Duplicate Services</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Create a new project with the selected services
|
Choose where to duplicate the selected services
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="name">Name</Label>
|
<Label>Duplicate to</Label>
|
||||||
<Input
|
<RadioGroup
|
||||||
id="name"
|
value={duplicateType}
|
||||||
value={name}
|
onValueChange={setDuplicateType}
|
||||||
onChange={(e) => setName(e.target.value)}
|
className="grid gap-2"
|
||||||
placeholder="New project name"
|
>
|
||||||
/>
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="new-project" id="new-project" />
|
||||||
|
<Label htmlFor="new-project">New project</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="same-project" id="same-project" />
|
||||||
|
<Label htmlFor="same-project">Same project</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
{duplicateType === "new-project" && (
|
||||||
<Label htmlFor="description">Description</Label>
|
<>
|
||||||
<Input
|
<div className="grid gap-2">
|
||||||
id="description"
|
<Label htmlFor="name">Name</Label>
|
||||||
value={description}
|
<Input
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
id="name"
|
||||||
placeholder="Project description (optional)"
|
value={name}
|
||||||
/>
|
onChange={(e) => setName(e.target.value)}
|
||||||
</div>
|
placeholder="New project name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="description">Description</Label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Project description (optional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Selected services to duplicate</Label>
|
<Label>Selected services to duplicate</Label>
|
||||||
@@ -159,10 +191,14 @@ export const DuplicateProject = ({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Duplicating...
|
{duplicateType === "new-project"
|
||||||
|
? "Duplicating project..."
|
||||||
|
: "Duplicating services..."}
|
||||||
</>
|
</>
|
||||||
|
) : duplicateType === "new-project" ? (
|
||||||
|
"Duplicate project"
|
||||||
) : (
|
) : (
|
||||||
"Duplicate"
|
"Duplicate services"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
duplicateInSameProject: z.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
@@ -331,15 +332,17 @@ export const projectRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new project
|
// Create new project or use existing one
|
||||||
const newProject = await createProject(
|
const targetProject = input.duplicateInSameProject
|
||||||
{
|
? sourceProject
|
||||||
name: input.name,
|
: await createProject(
|
||||||
description: input.description,
|
{
|
||||||
env: sourceProject.env,
|
name: input.name,
|
||||||
},
|
description: input.description,
|
||||||
ctx.session.activeOrganizationId,
|
env: sourceProject.env,
|
||||||
);
|
},
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
|
||||||
if (input.includeServices) {
|
if (input.includeServices) {
|
||||||
const servicesToDuplicate = input.selectedServices || [];
|
const servicesToDuplicate = input.selectedServices || [];
|
||||||
@@ -362,7 +365,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const newApplication = await createApplication({
|
const newApplication = await createApplication({
|
||||||
...application,
|
...application,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${application.name} (copy)`
|
||||||
|
: application.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
@@ -423,7 +429,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const newPostgres = await createPostgres({
|
const newPostgres = await createPostgres({
|
||||||
...postgres,
|
...postgres,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${postgres.name} (copy)`
|
||||||
|
: postgres.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -449,7 +458,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
await findMariadbById(id);
|
await findMariadbById(id);
|
||||||
const newMariadb = await createMariadb({
|
const newMariadb = await createMariadb({
|
||||||
...mariadb,
|
...mariadb,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${mariadb.name} (copy)`
|
||||||
|
: mariadb.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -475,7 +487,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
await findMongoById(id);
|
await findMongoById(id);
|
||||||
const newMongo = await createMongo({
|
const newMongo = await createMongo({
|
||||||
...mongo,
|
...mongo,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${mongo.name} (copy)`
|
||||||
|
: mongo.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -501,7 +516,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
await findMySqlById(id);
|
await findMySqlById(id);
|
||||||
const newMysql = await createMysql({
|
const newMysql = await createMysql({
|
||||||
...mysql,
|
...mysql,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${mysql.name} (copy)`
|
||||||
|
: mysql.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -526,7 +544,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
const { redisId, mounts, ...redis } = await findRedisById(id);
|
const { redisId, mounts, ...redis } = await findRedisById(id);
|
||||||
const newRedis = await createRedis({
|
const newRedis = await createRedis({
|
||||||
...redis,
|
...redis,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${redis.name} (copy)`
|
||||||
|
: redis.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -545,7 +566,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
await findComposeById(id);
|
await findComposeById(id);
|
||||||
const newCompose = await createCompose({
|
const newCompose = await createCompose({
|
||||||
...compose,
|
...compose,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${compose.name} (copy)`
|
||||||
|
: compose.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -572,21 +596,20 @@ export const projectRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Duplicate selected services
|
// Duplicate selected services
|
||||||
|
|
||||||
for (const service of servicesToDuplicate) {
|
for (const service of servicesToDuplicate) {
|
||||||
await duplicateService(service.id, service.type);
|
await duplicateService(service.id, service.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
||||||
await addNewProject(
|
await addNewProject(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newProject.projectId,
|
targetProject.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newProject;
|
return targetProject;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
|
|||||||
Reference in New Issue
Block a user