Restore missing catalog admin API routes so Visual Manager works.
Local Build and Deploy / deploy (push) Successful in 1m10s
Local Build and Deploy / deploy (push) Successful in 1m10s
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
ce8c3503e9
commit
11d3cf31cc
6 files changed
+490
-10
No files matched your search
@@ -0,0 +1,47 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
|
||||||
|
const CATALOGUE_DIR = path.join(
|
||||||
|
/*turbopackIgnore: true*/ process.cwd(),
|
||||||
|
"public",
|
||||||
|
"swf",
|
||||||
|
"c_images",
|
||||||
|
"catalogue",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GET = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_VIEW },
|
||||||
|
async (request) => {
|
||||||
|
const sp = request.nextUrl.searchParams;
|
||||||
|
const limit = Math.min(
|
||||||
|
Math.max(Number(sp.get("limit") || 120) || 120, 1),
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
const offset = Math.max(Number(sp.get("offset") || 0) || 0, 0);
|
||||||
|
const search = (sp.get("search") || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
let icons: number[] = [];
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(CATALOGUE_DIR);
|
||||||
|
icons = files
|
||||||
|
.map((f) => /^icon_(\d+)\.(?:png|gif)$/i.exec(f))
|
||||||
|
.filter((m): m is RegExpExecArray => Boolean(m))
|
||||||
|
.map((m) => Number(m[1]))
|
||||||
|
.filter((n) => Number.isFinite(n))
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
} catch {
|
||||||
|
icons = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
icons = icons.filter((id) => String(id).includes(search));
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = icons.length;
|
||||||
|
const slice = icons.slice(offset, offset + limit);
|
||||||
|
return apiOk({ icons: slice, total });
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
|
||||||
|
const CATALOGUE_DIR = path.join(
|
||||||
|
/*turbopackIgnore: true*/ process.cwd(),
|
||||||
|
"public",
|
||||||
|
"swf",
|
||||||
|
"c_images",
|
||||||
|
"catalogue",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GET = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_VIEW },
|
||||||
|
async (request) => {
|
||||||
|
const sp = request.nextUrl.searchParams;
|
||||||
|
const limit = Math.min(
|
||||||
|
Math.max(Number(sp.get("limit") || 80) || 80, 1),
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
const offset = Math.max(Number(sp.get("offset") || 0) || 0, 0);
|
||||||
|
const search = (sp.get("search") || "").trim().toLowerCase();
|
||||||
|
const type = sp.get("type") || "all";
|
||||||
|
|
||||||
|
let names: string[] = [];
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(CATALOGUE_DIR);
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const file of files) {
|
||||||
|
if (/^icon_\d+\./i.test(file)) continue;
|
||||||
|
const base = file.replace(/\.(png|gif|jpg|jpeg|webp)$/i, "");
|
||||||
|
if (!base || seen.has(base)) continue;
|
||||||
|
seen.add(base);
|
||||||
|
names.push(base);
|
||||||
|
}
|
||||||
|
names.sort((a, b) => a.localeCompare(b));
|
||||||
|
} catch {
|
||||||
|
names = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "header") {
|
||||||
|
names = names.filter((n) => /header/i.test(n));
|
||||||
|
} else if (type === "teaser") {
|
||||||
|
names = names.filter((n) => /teaser/i.test(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
names = names.filter((n) => n.toLowerCase().includes(search));
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = names.length;
|
||||||
|
const images = names.slice(offset, offset + limit);
|
||||||
|
return apiOk({ images, total });
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { loadCatalogItemsData } from "@/lib/services/catalog-items-loader";
|
||||||
|
|
||||||
|
export const GET = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_VIEW },
|
||||||
|
async (request) => {
|
||||||
|
const pageId = Number(request.nextUrl.searchParams.get("pageId"));
|
||||||
|
if (!Number.isFinite(pageId) || pageId <= 0) {
|
||||||
|
return apiError("Invalid pageId");
|
||||||
|
}
|
||||||
|
const data = await loadCatalogItemsData(pageId);
|
||||||
|
return apiOk(data as unknown as Record<string, unknown>);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { lookupHabboIt } from "@/lib/services/habbo-furnidata-cache";
|
||||||
|
|
||||||
|
export const GET = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_VIEW },
|
||||||
|
async (request) => {
|
||||||
|
const raw = request.nextUrl.searchParams.get("classnames") || "";
|
||||||
|
const classnames = raw
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 100);
|
||||||
|
|
||||||
|
const suggestions: Record<string, { name: string; description: string }> =
|
||||||
|
{};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
classnames.map(async (classname) => {
|
||||||
|
const entry = await lookupHabboIt(classname);
|
||||||
|
if (!entry) return;
|
||||||
|
suggestions[classname] = {
|
||||||
|
name: entry.name ?? "",
|
||||||
|
description: entry.description ?? "",
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return apiOk({ suggestions });
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import {
|
||||||
|
deletePage,
|
||||||
|
getAncestors,
|
||||||
|
getTreeFlat,
|
||||||
|
} from "@/lib/services/catalog-tree";
|
||||||
|
import type { TreeNode } from "@/types/catalog";
|
||||||
|
|
||||||
|
type PageRow = {
|
||||||
|
id: number;
|
||||||
|
caption: string;
|
||||||
|
parentId: number;
|
||||||
|
orderNum: number;
|
||||||
|
enabled: string;
|
||||||
|
visible: string;
|
||||||
|
iconImage: number;
|
||||||
|
iconColor: number;
|
||||||
|
pageLayout: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getBcTreeFlat(): Promise<TreeNode[]> {
|
||||||
|
const [allPages, itemCounts] = await Promise.all([
|
||||||
|
prisma.catalogPagesBc.findMany({ orderBy: { orderNum: "asc" } }),
|
||||||
|
prisma.catalogItemsBc.groupBy({ by: ["pageId"], _count: true }),
|
||||||
|
]);
|
||||||
|
const itemCountMap = new Map(itemCounts.map((c) => [c.pageId, c._count]));
|
||||||
|
const childCountMap = new Map<number, number>();
|
||||||
|
for (const p of allPages) {
|
||||||
|
childCountMap.set(p.parentId, (childCountMap.get(p.parentId) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
const pageMap = new Map(allPages.map((p) => [p.id, p]));
|
||||||
|
const depthCache = new Map<number, number>();
|
||||||
|
function computeDepth(pageId: number): number {
|
||||||
|
if (depthCache.has(pageId)) return depthCache.get(pageId)!;
|
||||||
|
const page = pageMap.get(pageId);
|
||||||
|
if (!page || page.parentId <= 0) {
|
||||||
|
depthCache.set(pageId, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const d = computeDepth(page.parentId) + 1;
|
||||||
|
depthCache.set(pageId, d);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
return allPages.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
caption: p.caption,
|
||||||
|
parentId: p.parentId,
|
||||||
|
depth: computeDepth(p.id),
|
||||||
|
orderNum: p.orderNum,
|
||||||
|
enabled: String(p.enabled),
|
||||||
|
visible: String(p.visible),
|
||||||
|
iconImage: p.iconImage,
|
||||||
|
iconColor: p.iconColor,
|
||||||
|
pageLayout: p.pageLayout,
|
||||||
|
childCount: childCountMap.get(p.id) ?? 0,
|
||||||
|
itemCount: itemCountMap.get(p.id) ?? 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTreeNode(
|
||||||
|
p: PageRow,
|
||||||
|
childCount: number,
|
||||||
|
itemCount: number,
|
||||||
|
depth = 0,
|
||||||
|
): TreeNode {
|
||||||
|
return {
|
||||||
|
id: p.id,
|
||||||
|
caption: p.caption,
|
||||||
|
parentId: p.parentId,
|
||||||
|
depth,
|
||||||
|
orderNum: p.orderNum,
|
||||||
|
enabled: String(p.enabled),
|
||||||
|
visible: String(p.visible),
|
||||||
|
iconImage: p.iconImage,
|
||||||
|
iconColor: p.iconColor,
|
||||||
|
pageLayout: p.pageLayout,
|
||||||
|
childCount,
|
||||||
|
itemCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getChildren(
|
||||||
|
parentId: number,
|
||||||
|
isBc: boolean,
|
||||||
|
): Promise<TreeNode[]> {
|
||||||
|
const parentFilter =
|
||||||
|
parentId === -1
|
||||||
|
? { OR: [{ parentId: -1 }, { parentId: 0 }] }
|
||||||
|
: { parentId };
|
||||||
|
|
||||||
|
if (isBc) {
|
||||||
|
const pages = await prisma.catalogPagesBc.findMany({
|
||||||
|
where: parentFilter,
|
||||||
|
orderBy: { orderNum: "asc" },
|
||||||
|
});
|
||||||
|
const ids = pages.map((p) => p.id);
|
||||||
|
const [childCounts, itemCounts] = await Promise.all([
|
||||||
|
ids.length
|
||||||
|
? prisma.catalogPagesBc.groupBy({
|
||||||
|
by: ["parentId"],
|
||||||
|
where: { parentId: { in: ids } },
|
||||||
|
_count: true,
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
ids.length
|
||||||
|
? prisma.catalogItemsBc.groupBy({
|
||||||
|
by: ["pageId"],
|
||||||
|
where: { pageId: { in: ids } },
|
||||||
|
_count: true,
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
]);
|
||||||
|
const childMap = Object.fromEntries(
|
||||||
|
childCounts.map((c) => [c.parentId, c._count]),
|
||||||
|
);
|
||||||
|
const itemMap = Object.fromEntries(
|
||||||
|
itemCounts.map((c) => [c.pageId, c._count]),
|
||||||
|
);
|
||||||
|
return pages.map((p) =>
|
||||||
|
toTreeNode(p, childMap[p.id] ?? 0, itemMap[p.id] ?? 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = await prisma.catalogPages.findMany({
|
||||||
|
where: parentFilter,
|
||||||
|
orderBy: { orderNum: "asc" },
|
||||||
|
});
|
||||||
|
const ids = pages.map((p) => p.id);
|
||||||
|
const [childCounts, itemCounts] = await Promise.all([
|
||||||
|
ids.length
|
||||||
|
? prisma.catalogPages.groupBy({
|
||||||
|
by: ["parentId"],
|
||||||
|
where: { parentId: { in: ids } },
|
||||||
|
_count: true,
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
ids.length
|
||||||
|
? prisma.catalogItems.groupBy({
|
||||||
|
by: ["pageId"],
|
||||||
|
where: { pageId: { in: ids } },
|
||||||
|
_count: true,
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
]);
|
||||||
|
const childMap = Object.fromEntries(
|
||||||
|
childCounts.map((c) => [c.parentId, c._count]),
|
||||||
|
);
|
||||||
|
const itemMap = Object.fromEntries(
|
||||||
|
itemCounts.map((c) => [c.pageId, c._count]),
|
||||||
|
);
|
||||||
|
return pages.map((p) =>
|
||||||
|
toTreeNode(p, childMap[p.id] ?? 0, itemMap[p.id] ?? 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchPages(q: string, isBc: boolean): Promise<TreeNode[]> {
|
||||||
|
const flat = isBc ? await getBcTreeFlat() : await getTreeFlat();
|
||||||
|
const needle = q.toLowerCase();
|
||||||
|
return flat.filter(
|
||||||
|
(p) =>
|
||||||
|
p.caption.toLowerCase().includes(needle) ||
|
||||||
|
String(p.id) === needle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_VIEW },
|
||||||
|
async (request) => {
|
||||||
|
const sp = request.nextUrl.searchParams;
|
||||||
|
const isBc = sp.get("catalog") === "bc";
|
||||||
|
|
||||||
|
if (sp.get("mode") === "full") {
|
||||||
|
const pages = isBc ? await getBcTreeFlat() : await getTreeFlat();
|
||||||
|
return apiOk({ pages });
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageIdRaw = sp.get("pageId");
|
||||||
|
if (pageIdRaw) {
|
||||||
|
const id = Number(pageIdRaw);
|
||||||
|
if (!Number.isFinite(id) || id <= 0) return apiError("Invalid pageId");
|
||||||
|
if (isBc) {
|
||||||
|
const page = await prisma.catalogPagesBc.findUnique({ where: { id } });
|
||||||
|
if (!page) return apiError("Page not found", 404);
|
||||||
|
return apiOk({ page });
|
||||||
|
}
|
||||||
|
const page = await prisma.catalogPages.findUnique({ where: { id } });
|
||||||
|
if (!page) return apiError("Page not found", 404);
|
||||||
|
return apiOk({ page });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ancestorsRaw = sp.get("ancestors");
|
||||||
|
if (ancestorsRaw) {
|
||||||
|
const id = Number(ancestorsRaw);
|
||||||
|
if (!Number.isFinite(id) || id <= 0) return apiError("Invalid id");
|
||||||
|
if (isBc) {
|
||||||
|
const chain: Array<{ id: number; caption: string }> = [];
|
||||||
|
let currentId = id;
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const page = await prisma.catalogPagesBc.findUnique({
|
||||||
|
where: { id: currentId },
|
||||||
|
select: { id: true, caption: true, parentId: true },
|
||||||
|
});
|
||||||
|
if (!page) break;
|
||||||
|
chain.unshift({ id: page.id, caption: page.caption });
|
||||||
|
if (page.parentId <= 0) break;
|
||||||
|
currentId = page.parentId;
|
||||||
|
}
|
||||||
|
return apiOk({
|
||||||
|
ancestors: chain.filter((a) => a.id !== id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const chain = await getAncestors(id);
|
||||||
|
return apiOk({ ancestors: chain.filter((a) => a.id !== id) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = sp.get("search");
|
||||||
|
if (search?.trim()) {
|
||||||
|
const pages = await searchPages(search.trim(), isBc);
|
||||||
|
return apiOk({ pages });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentIdRaw = sp.get("parentId");
|
||||||
|
if (parentIdRaw !== null) {
|
||||||
|
const parentId = Number(parentIdRaw);
|
||||||
|
if (!Number.isFinite(parentId)) return apiError("Invalid parentId");
|
||||||
|
const pages = await getChildren(parentId, isBc);
|
||||||
|
return apiOk({ pages });
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiOk({ pages: [] });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PATCH = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_EDIT },
|
||||||
|
async (request) => {
|
||||||
|
const body = (await request.json()) as {
|
||||||
|
pageId?: number;
|
||||||
|
toggleField?: "toggleEnabled" | "toggleVisible";
|
||||||
|
catalog?: string;
|
||||||
|
};
|
||||||
|
const pageId = Number(body.pageId);
|
||||||
|
const field =
|
||||||
|
body.toggleField === "toggleVisible" ? "visible" : "enabled";
|
||||||
|
if (!Number.isFinite(pageId) || pageId <= 0) {
|
||||||
|
return apiError("Invalid pageId");
|
||||||
|
}
|
||||||
|
const isBc = body.catalog === "bc";
|
||||||
|
if (isBc) {
|
||||||
|
const page = await prisma.catalogPagesBc.findUnique({
|
||||||
|
where: { id: pageId },
|
||||||
|
select: { enabled: true, visible: true },
|
||||||
|
});
|
||||||
|
if (!page) return apiError("Page not found", 404);
|
||||||
|
const current = field === "enabled" ? page.enabled : page.visible;
|
||||||
|
await prisma.catalogPagesBc.update({
|
||||||
|
where: { id: pageId },
|
||||||
|
data: { [field]: current === "1" ? "0" : "1" },
|
||||||
|
});
|
||||||
|
return apiOk({});
|
||||||
|
}
|
||||||
|
const page = await prisma.catalogPages.findUnique({
|
||||||
|
where: { id: pageId },
|
||||||
|
select: { enabled: true, visible: true },
|
||||||
|
});
|
||||||
|
if (!page) return apiError("Page not found", 404);
|
||||||
|
const current = field === "enabled" ? page.enabled : page.visible;
|
||||||
|
await prisma.catalogPages.update({
|
||||||
|
where: { id: pageId },
|
||||||
|
data: { [field]: current === "1" ? "0" : "1" },
|
||||||
|
});
|
||||||
|
return apiOk({});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DELETE = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_EDIT },
|
||||||
|
async (request) => {
|
||||||
|
const sp = request.nextUrl.searchParams;
|
||||||
|
const pageId = Number(sp.get("pageId"));
|
||||||
|
const mode = sp.get("mode") === "cascade" ? "cascade" : "reparent";
|
||||||
|
const isBc = sp.get("catalog") === "bc";
|
||||||
|
if (!Number.isFinite(pageId) || pageId <= 0) {
|
||||||
|
return apiError("Invalid pageId");
|
||||||
|
}
|
||||||
|
if (isBc) {
|
||||||
|
const page = await prisma.catalogPagesBc.findUnique({
|
||||||
|
where: { id: pageId },
|
||||||
|
select: { parentId: true },
|
||||||
|
});
|
||||||
|
if (!page) return apiError("Page not found", 404);
|
||||||
|
if (mode === "reparent") {
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.catalogPagesBc.updateMany({
|
||||||
|
where: { parentId: pageId },
|
||||||
|
data: { parentId: page.parentId },
|
||||||
|
}),
|
||||||
|
prisma.catalogItemsBc.deleteMany({ where: { pageId } }),
|
||||||
|
prisma.catalogPagesBc.delete({ where: { id: pageId } }),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
const children = await prisma.catalogPagesBc.findMany({
|
||||||
|
where: { parentId: pageId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
const ids = [pageId, ...children.map((c) => c.id)];
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.catalogItemsBc.deleteMany({
|
||||||
|
where: { pageId: { in: ids } },
|
||||||
|
}),
|
||||||
|
prisma.catalogPagesBc.deleteMany({ where: { id: { in: ids } } }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return apiOk({});
|
||||||
|
}
|
||||||
|
await deletePage(pageId, mode);
|
||||||
|
return apiOk({});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import type { RootTab } from "@/types/catalog";
|
import type { RootTab } from "@/types/catalog";
|
||||||
@@ -26,16 +25,21 @@ export function CatalogManagerDialog() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen} disablePointerDismissal>
|
<Dialog open={open} onOpenChange={setOpen} disablePointerDismissal>
|
||||||
<DialogTrigger asChild>
|
<Button
|
||||||
<Button variant="outline" className="gap-2">
|
type="button"
|
||||||
<LayoutGrid className="h-4 w-4" />
|
variant="outline"
|
||||||
Visual Manager
|
className="gap-2"
|
||||||
</Button>
|
onClick={() => setOpen(true)}
|
||||||
</DialogTrigger>
|
>
|
||||||
|
<LayoutGrid className="h-4 w-4" />
|
||||||
|
Visual Manager
|
||||||
|
</Button>
|
||||||
<DialogContent className="max-w-full w-[98vw] h-[95vh] max-h-[95vh] p-0 flex flex-col gap-0 sm:max-w-[98vw]">
|
<DialogContent className="max-w-full w-[98vw] h-[95vh] max-h-[95vh] p-0 flex flex-col gap-0 sm:max-w-[98vw]">
|
||||||
<CatalogManagerProvider>
|
{open ? (
|
||||||
<CatalogManagerInner open={open} onOpenChange={setOpen} />
|
<CatalogManagerProvider>
|
||||||
</CatalogManagerProvider>
|
<CatalogManagerInner open={open} onOpenChange={setOpen} />
|
||||||
|
</CatalogManagerProvider>
|
||||||
|
) : null}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user