mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
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:
@@ -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,140 +75,98 @@ 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
|
id: service.id,
|
||||||
.filter((service) => selectedServices.includes(service.id))
|
type: service.type,
|
||||||
.map((service) => ({
|
})),
|
||||||
id: service.id,
|
|
||||||
type: service.type,
|
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dialog
|
||||||
<DropdownMenuItem
|
open={open}
|
||||||
onSelect={(e) => {
|
onOpenChange={(isOpen) => {
|
||||||
e.preventDefault();
|
setOpen(isOpen);
|
||||||
setOpen(true);
|
if (!isOpen) {
|
||||||
}}
|
// Reset form when closing
|
||||||
>
|
setName("");
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
setDescription("");
|
||||||
Duplicate Project
|
}
|
||||||
</DropdownMenuItem>
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" className="w-full justify-start">
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
Duplicate
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Duplicate Project</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new project with the selected services
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<Dialog
|
<div className="grid gap-4 py-4">
|
||||||
open={open}
|
<div className="grid gap-2">
|
||||||
onOpenChange={(isOpen) => {
|
<Label htmlFor="name">Name</Label>
|
||||||
setOpen(isOpen);
|
<Input
|
||||||
if (!isOpen) {
|
id="name"
|
||||||
// Reset form when closing
|
value={name}
|
||||||
setName("");
|
onChange={(e) => setName(e.target.value)}
|
||||||
setDescription("");
|
placeholder="New project name"
|
||||||
setIncludeServices(true);
|
/>
|
||||||
setSelectedServices([]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Duplicate Project</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Create a new project with the same configuration
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="name">Name</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
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="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">
|
|
||||||
<Label>Select services to duplicate</Label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{services.map((service) => (
|
|
||||||
<div
|
|
||||||
key={service.id}
|
|
||||||
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})
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<div className="grid gap-2">
|
||||||
<Button
|
<Label htmlFor="description">Description</Label>
|
||||||
variant="outline"
|
<Input
|
||||||
onClick={() => setOpen(false)}
|
id="description"
|
||||||
disabled={isLoading}
|
value={description}
|
||||||
>
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
Cancel
|
placeholder="Project description (optional)"
|
||||||
</Button>
|
/>
|
||||||
<Button onClick={handleDuplicate} disabled={isLoading}>
|
</div>
|
||||||
{isLoading ? (
|
|
||||||
<>
|
<div className="grid gap-2">
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Label>Selected services to duplicate</Label>
|
||||||
Duplicating...
|
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
|
||||||
</>
|
{selectedServices.map((service) => (
|
||||||
) : (
|
<div key={service.id} className="flex items-center space-x-2">
|
||||||
"Duplicate"
|
<span className="text-sm">
|
||||||
)}
|
{service.name} ({service.type})
|
||||||
</Button>
|
</span>
|
||||||
</DialogFooter>
|
</div>
|
||||||
</DialogContent>
|
))}
|
||||||
</Dialog>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleDuplicate} disabled={isLoading}>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Duplicating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Duplicate"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,20 +671,27 @@ const Project = (
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
{(auth?.role === "owner" ||
|
{(auth?.role === "owner" ||
|
||||||
auth?.canDeleteServices) && (
|
auth?.canDeleteServices) && (
|
||||||
<DialogAction
|
<>
|
||||||
title="Delete Services"
|
<DialogAction
|
||||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
title="Delete Services"
|
||||||
type="destructive"
|
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||||
onClick={handleBulkDelete}
|
type="destructive"
|
||||||
>
|
onClick={handleBulkDelete}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full justify-start text-destructive"
|
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Button
|
||||||
Delete
|
variant="ghost"
|
||||||
</Button>
|
className="w-full justify-start text-destructive"
|
||||||
</DialogAction>
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DuplicateProject
|
||||||
|
projectId={projectId}
|
||||||
|
services={applications}
|
||||||
|
selectedServiceIds={selectedServices}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
Reference in New Issue
Block a user