Refactor Duplicate Project component and integrate with project detail page

- Updated the DuplicateProject component to accept projectId and selectedServiceIds as props, enhancing its flexibility.
- Removed unnecessary state management for service selection within the component.
- Integrated the DuplicateProject component directly into the project detail page, allowing for easier access to duplication functionality.
- Improved the user interface by utilizing DialogTrigger for initiating the duplication process and displaying selected services more clearly.
This commit is contained in:
Mauricio Siu
2025-03-30 02:42:35 -06:00
parent 2f16034cb0
commit 3910e22412
2 changed files with 113 additions and 165 deletions

View File

@@ -6,13 +6,11 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { findProjectById } from "@dokploy/server";
import { Copy, Loader2 } from "lucide-react"; import { Copy, Loader2 } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
@@ -36,25 +34,27 @@ export type Services = {
status?: "idle" | "running" | "done" | "error"; status?: "idle" | "running" | "done" | "error";
}; };
type Project = Awaited<ReturnType<typeof findProjectById>>;
interface DuplicateProjectProps { interface DuplicateProjectProps {
project: Project; projectId: string;
services: Services[]; services: Services[];
selectedServiceIds: string[];
} }
export const DuplicateProject = ({ export const DuplicateProject = ({
project, projectId,
services, services,
selectedServiceIds,
}: DuplicateProjectProps) => { }: DuplicateProjectProps) => {
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 [includeServices, setIncludeServices] = useState(true);
const [selectedServices, setSelectedServices] = useState<string[]>([]);
const utils = api.useUtils(); const utils = api.useUtils();
const router = useRouter(); const router = useRouter();
const selectedServices = services.filter((service) =>
selectedServiceIds.includes(service.id),
);
const { mutateAsync: duplicateProject, isLoading } = const { mutateAsync: duplicateProject, isLoading } =
api.project.duplicate.useMutation({ api.project.duplicate.useMutation({
onSuccess: async (newProject) => { onSuccess: async (newProject) => {
@@ -75,33 +75,18 @@ export const DuplicateProject = ({
} }
await duplicateProject({ await duplicateProject({
sourceProjectId: project.projectId, sourceProjectId: projectId,
name, name,
description, description,
includeServices, includeServices: true,
selectedServices: includeServices selectedServices: selectedServices.map((service) => ({
? services
.filter((service) => selectedServices.includes(service.id))
.map((service) => ({
id: service.id, id: service.id,
type: service.type, type: service.type,
})) })),
: [],
}); });
}; };
return ( return (
<>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
setOpen(true);
}}
>
<Copy className="mr-2 h-4 w-4" />
Duplicate Project
</DropdownMenuItem>
<Dialog <Dialog
open={open} open={open}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
@@ -110,16 +95,20 @@ export const DuplicateProject = ({
// Reset form when closing // Reset form when closing
setName(""); setName("");
setDescription(""); setDescription("");
setIncludeServices(true);
setSelectedServices([]);
} }
}} }}
> >
<DialogTrigger asChild>
<Button variant="ghost" className="w-full justify-start">
<Copy className="mr-2 h-4 w-4" />
Duplicate
</Button>
</DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Duplicate Project</DialogTitle> <DialogTitle>Duplicate Project</DialogTitle>
<DialogDescription> <DialogDescription>
Create a new project with the same configuration Create a new project with the selected services
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -144,48 +133,18 @@ export const DuplicateProject = ({
/> />
</div> </div>
<div className="flex items-center space-x-2">
<Checkbox
id="includeServices"
checked={includeServices}
onCheckedChange={(checked) => {
setIncludeServices(checked as boolean);
if (!checked) {
setSelectedServices([]);
}
}}
/>
<Label htmlFor="includeServices">Include services</Label>
</div>
{includeServices && services.length > 0 && (
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Select services to duplicate</Label> <Label>Selected services to duplicate</Label>
<div className="space-y-2"> <div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
{services.map((service) => ( {selectedServices.map((service) => (
<div <div key={service.id} className="flex items-center space-x-2">
key={service.id} <span className="text-sm">
className="flex items-center space-x-2"
>
<Checkbox
id={service.id}
checked={selectedServices.includes(service.id)}
onCheckedChange={(checked) => {
setSelectedServices((prev) =>
checked
? [...prev, service.id]
: prev.filter((id) => id !== service.id),
);
}}
/>
<Label htmlFor={service.id}>
{service.name} ({service.type}) {service.name} ({service.type})
</Label> </span>
</div> </div>
))} ))}
</div> </div>
</div> </div>
)}
</div> </div>
<DialogFooter> <DialogFooter>
@@ -209,6 +168,5 @@ export const DuplicateProject = ({
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</>
); );
}; };

View File

@@ -78,7 +78,6 @@ import {
FolderInput, FolderInput,
GlobeIcon, GlobeIcon,
Loader2, Loader2,
MoreHorizontal,
PlusIcon, PlusIcon,
Search, Search,
Trash2, Trash2,
@@ -595,22 +594,6 @@ const Project = (
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
{auth?.role === "owner" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DuplicateProject
project={data}
services={applications}
/>
</DropdownMenuContent>
</DropdownMenu>
)}
</div> </div>
</div> </div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]"> <CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
@@ -688,6 +671,7 @@ const Project = (
</DialogAction> </DialogAction>
{(auth?.role === "owner" || {(auth?.role === "owner" ||
auth?.canDeleteServices) && ( auth?.canDeleteServices) && (
<>
<DialogAction <DialogAction
title="Delete Services" title="Delete Services"
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`} description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
@@ -702,6 +686,12 @@ const Project = (
Delete Delete
</Button> </Button>
</DialogAction> </DialogAction>
<DuplicateProject
projectId={projectId}
services={applications}
selectedServiceIds={selectedServices}
/>
</>
)} )}
<Dialog <Dialog