feat(domain): add duplicate feature
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useTransition } from "react";
|
||||
import Link from "next/link";
|
||||
import { User } from "@prisma/client";
|
||||
import { PenLine, RefreshCwIcon } from "lucide-react";
|
||||
@@ -13,6 +13,13 @@ import { fetcher } from "@/lib/utils";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Modal } from "@/components/ui/modal";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -67,8 +74,10 @@ function TableColumnSekleton() {
|
||||
|
||||
export default function DomainList({ user, action }: DomainListProps) {
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const t = useTranslations("List");
|
||||
const [isShowForm, setShowForm] = useState(false);
|
||||
const [isShowDuplicateForm, setShowDuplicateForm] = useState(false);
|
||||
const [formType, setFormType] = useState<FormType>("add");
|
||||
const [currentEditDomain, setCurrentEditDomain] =
|
||||
useState<DomainFormData | null>(null);
|
||||
@@ -123,6 +132,26 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicate = () => {
|
||||
startTransition(async () => {
|
||||
const response = await fetch(`${action}/duplicate`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
domain: currentEditDomain?.domain_name,
|
||||
}),
|
||||
});
|
||||
if (!response.ok || response.status !== 200) {
|
||||
toast.error("Duplicate Failed!", {
|
||||
description: await response.text(),
|
||||
});
|
||||
} else {
|
||||
toast.success(`Duplicate successfully!`);
|
||||
setShowDuplicateForm(false);
|
||||
handleRefresh();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="xl:col-span-2">
|
||||
@@ -290,27 +319,52 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
<TimeAgoIntl date={domain.updatedAt as Date} />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 flex items-center gap-1">
|
||||
<Button
|
||||
className="h-7 px-1 text-xs hover:bg-slate-100 dark:hover:text-primary-foreground sm:px-1.5"
|
||||
size="sm"
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
setCurrentEditDomain(domain);
|
||||
setShowForm(false);
|
||||
setFormType("edit");
|
||||
setShowForm(!isShowForm);
|
||||
}}
|
||||
>
|
||||
<p className="hidden text-nowrap sm:block">
|
||||
{t("Edit")}
|
||||
</p>
|
||||
<PenLine className="mx-0.5 size-4 sm:ml-1 sm:size-3" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="size-[25px] p-1.5"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
<Icons.moreVertical className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
className="flex w-full items-center gap-2 text-nowrap"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setCurrentEditDomain(domain);
|
||||
setShowForm(false);
|
||||
setFormType("edit");
|
||||
setShowForm(!isShowForm);
|
||||
}}
|
||||
>
|
||||
{/* <PenLine className="mx-0.5 size-4" /> */}
|
||||
{t("Edit")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
className="flex w-full items-center gap-2"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setCurrentEditDomain(domain);
|
||||
setShowDuplicateForm(false);
|
||||
setShowDuplicateForm(!isShowDuplicateForm);
|
||||
}}
|
||||
>
|
||||
{t("Duplicate")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/* {isShowDomainInfo && selectedDomain?.id === domain.id && (
|
||||
<DomainInfo domain={domain} />
|
||||
)} */}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
@@ -355,6 +409,42 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
showModal={isShowDuplicateForm}
|
||||
setShowModal={setShowDuplicateForm}
|
||||
>
|
||||
<div className="flex flex-col items-start border-b p-4 pt-8 sm:px-16">
|
||||
<h2 className="mb-2 text-lg font-bold">
|
||||
{t("Confirm duplicate domain")} ?
|
||||
</h2>
|
||||
<p>
|
||||
{t(
|
||||
"This will duplicate all configuration information for the {domain} domain, and create a new domain",
|
||||
{ domain: currentEditDomain?.domain_name || "" },
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex w-full items-center justify-between gap-2">
|
||||
<Button
|
||||
type="reset"
|
||||
variant="destructive"
|
||||
className="w-[100px] px-0"
|
||||
onClick={() => setShowDuplicateForm(false)}
|
||||
>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full text-nowrap"
|
||||
disabled={isPending}
|
||||
onClick={() => handleDuplicate()}
|
||||
>
|
||||
{t("Duplicate")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ export default function UsersList({ user }: UrlListProps) {
|
||||
setShowForm(!isShowForm);
|
||||
}}
|
||||
>
|
||||
<p>{t("Edit")}</p>
|
||||
<p className="text-nowrap">{t("Edit")}</p>
|
||||
<PenLine className="ml-1 size-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { createDomain, getDomainByName } from "@/lib/dto/domains";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const user = checkUserStatus(await getCurrentUser());
|
||||
if (user instanceof Response) return user;
|
||||
if (user.role !== "ADMIN") {
|
||||
return Response.json("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const { domain } = await req.json();
|
||||
if (!domain) {
|
||||
return Response.json("Domain name is required", { status: 400 });
|
||||
}
|
||||
|
||||
const target_domain = await getDomainByName(domain);
|
||||
if (!target_domain) {
|
||||
return Response.json("Domain not found", { status: 404 });
|
||||
}
|
||||
|
||||
const newDomain = await createDomain({
|
||||
domain_name: target_domain.domain_name + "-copy",
|
||||
enable_short_link: !!target_domain.enable_short_link,
|
||||
enable_email: !!target_domain.enable_email,
|
||||
enable_dns: !!target_domain.enable_dns,
|
||||
cf_zone_id: target_domain.cf_zone_id,
|
||||
cf_api_key: target_domain.cf_api_key,
|
||||
cf_email: target_domain.cf_email,
|
||||
cf_record_types: target_domain.cf_record_types,
|
||||
cf_api_key_encrypted: false,
|
||||
resend_api_key: target_domain.resend_api_key,
|
||||
max_short_links: target_domain.max_short_links,
|
||||
max_email_forwards: target_domain.max_email_forwards,
|
||||
max_dns_records: target_domain.max_dns_records,
|
||||
min_url_length: target_domain.min_url_length,
|
||||
min_email_length: target_domain.min_email_length,
|
||||
min_record_length: target_domain.min_record_length,
|
||||
active: true,
|
||||
});
|
||||
|
||||
if (!newDomain) {
|
||||
return Response.json("Failed to create domain", { status: 400 });
|
||||
}
|
||||
|
||||
return Response.json("Success", { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("[Error]", error);
|
||||
return Response.json(error.message || "Server error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,12 @@ export async function getDomainsByFeatureClient(feature: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDomainByName(domain_name: string) {
|
||||
return await prisma.domain.findUnique({
|
||||
where: { domain_name },
|
||||
});
|
||||
}
|
||||
|
||||
export async function checkDomainIsConfiguratedResend(domain_name: string) {
|
||||
try {
|
||||
const domain = await prisma.domain.findUnique({
|
||||
|
||||
+4
-1
@@ -181,7 +181,10 @@
|
||||
"USER": "USER",
|
||||
"Total Users": "Total Users",
|
||||
"Edit User": "Edit User",
|
||||
"Login Password": "Password"
|
||||
"Login Password": "Password",
|
||||
"Duplicate": "Duplicate",
|
||||
"Confirm duplicate domain": "Confirm duplicate domain",
|
||||
"This will duplicate all configuration information for the {domain} domain, and create a new domain": "This will duplicate all configuration information for the {domain} domain, and create a new domain"
|
||||
},
|
||||
"Components": {
|
||||
"Dashboard": "Dashboard",
|
||||
|
||||
+4
-1
@@ -181,7 +181,10 @@
|
||||
"USER": "用户",
|
||||
"Total Users": "用户总数",
|
||||
"Edit User": "编辑用户",
|
||||
"Login Password": "用户密码"
|
||||
"Login Password": "用户密码",
|
||||
"Duplicate": "复制",
|
||||
"Confirm duplicate domain": "确认复制域名",
|
||||
"This will duplicate all configuration information for the {domain} domain, and create a new domain": "这将复制 {domain} 域名的所有配置信息,并创建一个新域名"
|
||||
},
|
||||
"Components": {
|
||||
"Dashboard": "用户面板",
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user