refactor(catalog): unify commands and embed guarded catalog workspace
This commit is contained in:
1 parent
16f35568f5
commit
193b6686a9
57 files changed
+3268
-728
No files matched your search
@@ -144,6 +144,8 @@ function SortableRow({
|
||||
}
|
||||
|
||||
interface CatalogItemsTableProps {
|
||||
onDirtyChange?: (dirty: boolean) => void;
|
||||
onRefresh?: () => void;
|
||||
items: CatalogItemData[];
|
||||
pageId: number;
|
||||
pageLayout: string;
|
||||
@@ -154,6 +156,8 @@ interface CatalogItemsTableProps {
|
||||
}
|
||||
|
||||
export function CatalogItemsTable({
|
||||
onDirtyChange,
|
||||
onRefresh,
|
||||
items,
|
||||
pageId,
|
||||
pageLayout,
|
||||
@@ -291,6 +295,9 @@ export function CatalogItemsTable({
|
||||
}, [items, itemOrder]);
|
||||
|
||||
const hasUnsavedChanges = modifiedItems.length > 0 || hasOrderChanges;
|
||||
useEffect(() => {
|
||||
onDirtyChange?.(hasUnsavedChanges);
|
||||
}, [hasUnsavedChanges, onDirtyChange]);
|
||||
|
||||
// Warn before navigating away with unsaved changes
|
||||
useEffect(() => {
|
||||
@@ -383,7 +390,8 @@ export function CatalogItemsTable({
|
||||
errorMessage: `Failed to save item #${item.id}.`,
|
||||
onSuccess: () => {
|
||||
setSavingItemId(null);
|
||||
router.refresh();
|
||||
if (onRefresh) onRefresh();
|
||||
else router.refresh();
|
||||
},
|
||||
onError: () => setSavingItemId(null),
|
||||
},
|
||||
@@ -426,6 +434,10 @@ export function CatalogItemsTable({
|
||||
else failedCount++;
|
||||
}
|
||||
|
||||
if (savedCount > 0) {
|
||||
if (onRefresh) onRefresh();
|
||||
else router.refresh();
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
return {
|
||||
ok: false as const,
|
||||
@@ -456,7 +468,8 @@ export function CatalogItemsTable({
|
||||
errorMessage: "Failed to delete items.",
|
||||
onSuccess: () => {
|
||||
setSelected(new Set());
|
||||
router.refresh();
|
||||
if (onRefresh) onRefresh();
|
||||
else router.refresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -490,7 +503,8 @@ export function CatalogItemsTable({
|
||||
setSelected(new Set());
|
||||
setMoveOpen(false);
|
||||
setMoveTargetPageId(null);
|
||||
router.refresh();
|
||||
if (onRefresh) onRefresh();
|
||||
else router.refresh();
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -590,7 +604,8 @@ export function CatalogItemsTable({
|
||||
onSuccess: () => {
|
||||
setAddOpen(false);
|
||||
setNewItem({ ...DEFAULT_NEW_ITEM });
|
||||
router.refresh();
|
||||
if (onRefresh) onRefresh();
|
||||
else router.refresh();
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -641,7 +656,8 @@ export function CatalogItemsTable({
|
||||
onSuccess: () => {
|
||||
setMoveOneId(null);
|
||||
setMoveOneTarget(null);
|
||||
router.refresh();
|
||||
if (onRefresh) onRefresh();
|
||||
else router.refresh();
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -711,7 +727,8 @@ export function CatalogItemsTable({
|
||||
onSuccess: () => {
|
||||
setEditingItem(null);
|
||||
setEditingBase(null);
|
||||
router.refresh();
|
||||
if (onRefresh) onRefresh();
|
||||
else router.refresh();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -136,10 +136,18 @@ export function CatalogPageForm({
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
run(() => updateCatalogPage({ id: catalogPage.id, ...form }), {
|
||||
successMessage: "Page updated and catalog synced.",
|
||||
errorMessage: "Failed to update.",
|
||||
});
|
||||
run(
|
||||
() =>
|
||||
updateCatalogPage({
|
||||
id: catalogPage.id,
|
||||
...form,
|
||||
expected: catalogPage as unknown as Record<string, unknown>,
|
||||
}),
|
||||
{
|
||||
successMessage: "Page saved.",
|
||||
errorMessage: "Failed to update.",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
|
||||
@@ -11,3 +11,11 @@ export const CatalogManagerDialog = dynamic(
|
||||
),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export const CatalogManagerWorkspace = dynamic(
|
||||
() =>
|
||||
import("@/components/admin/catalog-manager/catalog-manager-dialog").then(
|
||||
(m) => ({ default: m.CatalogManagerWorkspace }),
|
||||
),
|
||||
{ ssr: false },
|
||||
);
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { FolderTree, HardHat, List, Package } from "lucide-react";
|
||||
import { FolderTree, HardHat, LayoutGrid, List, Package } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { requestCatalogNavigation } from "@/features/catalog/client/use-catalog-navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CatalogViewToggleProps {
|
||||
@@ -15,11 +17,13 @@ export function CatalogViewToggle({
|
||||
catalogType = "normal",
|
||||
}: CatalogViewToggleProps) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("pages.admin.catalog.workspace");
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
function setView(view: string) {
|
||||
if (view === currentView || !requestCatalogNavigation()) return;
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (view === "tree") {
|
||||
if (view === "manager") {
|
||||
params.delete("view");
|
||||
} else {
|
||||
params.set("view", view);
|
||||
@@ -28,18 +32,21 @@ export function CatalogViewToggle({
|
||||
}
|
||||
|
||||
function setCatalog(type: string) {
|
||||
if (type === catalogType || !requestCatalogNavigation()) return;
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (type === "normal") {
|
||||
params.delete("catalog");
|
||||
} else {
|
||||
params.set("catalog", type);
|
||||
}
|
||||
params.delete("view"); // reset to tree when switching
|
||||
params.delete("view");
|
||||
params.delete("root");
|
||||
params.delete("category");
|
||||
router.push(`?${params.toString()}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Catalog type selector */}
|
||||
<div className="flex items-center rounded-md border bg-card">
|
||||
<Button
|
||||
@@ -52,7 +59,7 @@ export function CatalogViewToggle({
|
||||
onClick={() => setCatalog("normal")}
|
||||
>
|
||||
<Package className="mr-1.5 h-4 w-4" />
|
||||
Catalog
|
||||
{t("normal")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -69,6 +76,14 @@ export function CatalogViewToggle({
|
||||
</div>
|
||||
|
||||
{/* View mode selector */}
|
||||
<Button
|
||||
variant={currentView === "manager" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setView("manager")}
|
||||
>
|
||||
<LayoutGrid className="mr-1.5 h-4 w-4" />
|
||||
{t("editor")}
|
||||
</Button>
|
||||
<div className="flex items-center rounded-md border bg-card">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -80,7 +95,7 @@ export function CatalogViewToggle({
|
||||
onClick={() => setView("tree")}
|
||||
>
|
||||
<FolderTree className="mr-1.5 h-4 w-4" />
|
||||
Tree
|
||||
{t("tree")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -92,7 +107,7 @@ export function CatalogViewToggle({
|
||||
onClick={() => setView("table")}
|
||||
>
|
||||
<List className="mr-1.5 h-4 w-4" />
|
||||
Table
|
||||
{t("table")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,11 @@ import { Store } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { CatalogFixAllButton } from "@/components/admin/catalog/catalog-fix-all-button";
|
||||
import { CatalogTree } from "@/components/admin/catalog-tree";
|
||||
import Link from "@/components/link";
|
||||
import { CatalogSearch } from "@/features/catalog/components/catalog-search";
|
||||
import { CatalogSyncStatus } from "@/features/catalog/components/catalog-sync-status";
|
||||
import { loadCatalogOverview } from "@/features/catalog/server/overview";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import {
|
||||
CatalogItems,
|
||||
@@ -28,7 +31,10 @@ import {
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { getCatalogItemCounts } from "@/lib/services/catalog-tree";
|
||||
import { CatalogActions } from "./catalog-actions";
|
||||
import { CatalogManagerDialog } from "./catalog-manager-dialog-wrapper";
|
||||
import {
|
||||
CatalogManagerDialog,
|
||||
CatalogManagerWorkspace,
|
||||
} from "./catalog-manager-dialog-wrapper";
|
||||
import { CatalogTable } from "./catalog-table";
|
||||
import { CatalogViewToggle } from "./catalog-view-toggle";
|
||||
|
||||
@@ -56,8 +62,44 @@ export default async function CatalogPagesPage({
|
||||
const params = await searchParams;
|
||||
const sp = new URLSearchParams(params);
|
||||
const { search, perPage, page, sort, order } = parseListParams(sp);
|
||||
const viewMode = sp.get("view") || "tree";
|
||||
const catalogType = sp.get("catalog") || "normal";
|
||||
const requestedView = sp.get("view");
|
||||
const viewMode =
|
||||
requestedView === "tree" || requestedView === "table"
|
||||
? requestedView
|
||||
: "manager";
|
||||
const catalogType = sp.get("catalog") === "bc" ? "bc" : "normal";
|
||||
if (viewMode === "manager") {
|
||||
const overview = await loadCatalogOverview(catalogType);
|
||||
return (
|
||||
<AdminPageShell icon={Store} title={t("title")} subtitle={t("subtitle")}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
|
||||
<CatalogViewToggle currentView={viewMode} catalogType={catalogType} />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Link className="btn btn-outline" href="/admin/catalog/maintenance">
|
||||
{t("workspace.diagnostics")}
|
||||
</Link>
|
||||
{canEdit && (
|
||||
<CatalogActions
|
||||
catalogType={catalogType}
|
||||
initialParentId={sp.get("newPageParent")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<CatalogSyncStatus canEdit={canEdit} />
|
||||
<CatalogSearch catalogType={catalogType} />
|
||||
</div>
|
||||
<CatalogManagerWorkspace
|
||||
key={catalogType}
|
||||
catalogType={catalogType}
|
||||
canEdit={canEdit}
|
||||
initialRoots={overview.roots}
|
||||
totals={overview.totals}
|
||||
/>
|
||||
</AdminPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
type TreeNodeShape = {
|
||||
id: number;
|
||||
@@ -94,7 +136,7 @@ export default async function CatalogPagesPage({
|
||||
.where(
|
||||
or(eq(CatalogPagesBc.parentId, -1), eq(CatalogPagesBc.parentId, 0)),
|
||||
)
|
||||
.orderBy(asc(CatalogPagesBc.orderNum));
|
||||
.orderBy(asc(CatalogPagesBc.orderNum), asc(CatalogPagesBc.id));
|
||||
const bcRootIds = bcRootRaw.map((p) => p.id);
|
||||
const [bcChildCounts, bcItemCounts] = await Promise.all([
|
||||
bcRootIds.length > 0
|
||||
@@ -290,7 +332,12 @@ export default async function CatalogPagesPage({
|
||||
initialParentId={sp.get("newPageParent")}
|
||||
/>
|
||||
)}
|
||||
<CatalogFixAllButton />
|
||||
<Link
|
||||
className="btn btn-outline"
|
||||
href="/admin/catalog/maintenance"
|
||||
>
|
||||
{t("workspace.diagnostics")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{viewMode === "tree" ? (
|
||||
@@ -323,7 +370,7 @@ export default async function CatalogPagesPage({
|
||||
.select()
|
||||
.from(CatalogPages)
|
||||
.where(or(eq(CatalogPages.parentId, -1), eq(CatalogPages.parentId, 0)))
|
||||
.orderBy(asc(CatalogPages.orderNum));
|
||||
.orderBy(asc(CatalogPages.orderNum), asc(CatalogPages.id));
|
||||
const rootIds = rootPagesRaw.map((p) => p.id);
|
||||
const [rootChildCounts, rootItemCountMap] = await Promise.all([
|
||||
rootIds.length > 0
|
||||
@@ -511,7 +558,9 @@ export default async function CatalogPagesPage({
|
||||
{canEdit && (
|
||||
<CatalogActions initialParentId={sp.get("newPageParent")} />
|
||||
)}
|
||||
<CatalogFixAllButton />
|
||||
<Link className="btn btn-outline" href="/admin/catalog/maintenance">
|
||||
{t("workspace.diagnostics")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/permissions", () => import("@/lib/permission-slugs"));
|
||||
const search = vi.hoisted(() => vi.fn());
|
||||
const guard = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@/features/catalog/server/search", () => ({ searchCatalog: search }));
|
||||
vi.mock("@/features/catalog/server/errors", () => ({
|
||||
catalogFailure: () => ({ message: "Invalid search", status: 400 }),
|
||||
}));
|
||||
vi.mock("@/lib/api-handler", () => ({
|
||||
withAdmin: (
|
||||
options: unknown,
|
||||
handler: (request: NextRequest) => Promise<Response>,
|
||||
) => {
|
||||
guard(options);
|
||||
return handler;
|
||||
},
|
||||
}));
|
||||
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { GET } from "./route";
|
||||
|
||||
beforeEach(() => {
|
||||
search.mockReset();
|
||||
});
|
||||
describe("catalog search route", () => {
|
||||
it("registers the catalog read permission", () => {
|
||||
expect(guard).toHaveBeenCalledWith({ permission: PERMS.CATALOG_VIEW });
|
||||
});
|
||||
it("forwards normal or BC without widening scope and returns results", async () => {
|
||||
search.mockResolvedValue([]);
|
||||
const response = await GET(
|
||||
new NextRequest(
|
||||
"https://cms.test/api/admin/catalog/search?catalog=bc&q=chair",
|
||||
),
|
||||
);
|
||||
expect(search).toHaveBeenCalledWith("bc", "chair");
|
||||
expect(await response.json()).toEqual({ ok: true, results: [] });
|
||||
});
|
||||
it("returns a validation error without a success payload", async () => {
|
||||
search.mockRejectedValue(Error("Invalid search"));
|
||||
const response = await GET(
|
||||
new NextRequest("https://cms.test/api/admin/catalog/search?q=a"),
|
||||
);
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.json()).toEqual({ error: "Invalid search" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { catalogFailure } from "@/features/catalog/server/errors";
|
||||
import { searchCatalog } from "@/features/catalog/server/search";
|
||||
import { apiError, apiOk } from "@/lib/api";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
export const GET = withAdmin(
|
||||
{ permission: PERMS.CATALOG_VIEW },
|
||||
async (request) => {
|
||||
const params = request.nextUrl.searchParams;
|
||||
try {
|
||||
return apiOk({
|
||||
results: await searchCatalog(
|
||||
params.get("catalog") ?? "normal",
|
||||
params.get("q") ?? "",
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
const failure = catalogFailure(error);
|
||||
return apiError(failure.message, failure.status);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
readCatalogHotelStatus,
|
||||
sendCatalogUpdate,
|
||||
} from "@/features/catalog/server/sync-status";
|
||||
import { apiOk } from "@/lib/api";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { catalogExportStatus } from "@/lib/services/catalog-git-queue";
|
||||
export const GET = withAdmin({ permission: PERMS.CATALOG_VIEW }, async () => {
|
||||
const [hotel, git] = await Promise.all([
|
||||
readCatalogHotelStatus(),
|
||||
catalogExportStatus(),
|
||||
]);
|
||||
return apiOk({ hotel, git });
|
||||
});
|
||||
export const POST = withAdmin({ permission: PERMS.CATALOG_EDIT }, async () =>
|
||||
apiOk({ hotel: await sendCatalogUpdate() }),
|
||||
);
|
||||
@@ -1,16 +1,20 @@
|
||||
import { asc, count, eq, inArray, like, or } from "drizzle-orm";
|
||||
import { catalogFailure } from "@/features/catalog/server/errors";
|
||||
import {
|
||||
deletePageCommand,
|
||||
togglePageCommand,
|
||||
} from "@/features/catalog/server/page-commands";
|
||||
import { sendCatalogUpdate } from "@/features/catalog/server/sync-status";
|
||||
import { apiError, apiOk } from "@/lib/api";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { CatalogItemsBc, CatalogPages, CatalogPagesBc, db } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import {
|
||||
deletePage,
|
||||
getAncestors,
|
||||
getCatalogItemCounts,
|
||||
getTreeFlat,
|
||||
toInt,
|
||||
} from "@/lib/services/catalog-tree";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import type { TreeNode } from "@/types/catalog";
|
||||
|
||||
type PageRow = {
|
||||
@@ -40,7 +44,7 @@ async function getBcTreeFlat(): Promise<TreeNode[]> {
|
||||
enabled: CatalogPagesBc.enabled,
|
||||
})
|
||||
.from(CatalogPagesBc)
|
||||
.orderBy(asc(CatalogPagesBc.orderNum)),
|
||||
.orderBy(asc(CatalogPagesBc.orderNum), asc(CatalogPagesBc.id)),
|
||||
db
|
||||
.select({
|
||||
pageId: CatalogItemsBc.pageId,
|
||||
@@ -56,14 +60,16 @@ async function getBcTreeFlat(): Promise<TreeNode[]> {
|
||||
}
|
||||
const pageMap = new Map(allPages.map((p) => [p.id, p]));
|
||||
const depthCache = new Map<number, number>();
|
||||
function computeDepth(pageId: number): number {
|
||||
function computeDepth(pageId: number, visiting = new Set<number>()): number {
|
||||
if (visiting.has(pageId)) return 0;
|
||||
visiting.add(pageId);
|
||||
if (depthCache.has(pageId)) return depthCache.get(pageId) ?? 0;
|
||||
const page = pageMap.get(pageId);
|
||||
if (!page || page.parentId <= 0) {
|
||||
depthCache.set(pageId, 0);
|
||||
return 0;
|
||||
}
|
||||
const d = computeDepth(page.parentId) + 1;
|
||||
const d = computeDepth(page.parentId, visiting) + 1;
|
||||
depthCache.set(pageId, d);
|
||||
return d;
|
||||
}
|
||||
@@ -134,7 +140,7 @@ async function getChildren(
|
||||
})
|
||||
.from(CatalogPagesBc)
|
||||
.where(parentFilterBc)
|
||||
.orderBy(asc(CatalogPagesBc.orderNum));
|
||||
.orderBy(asc(CatalogPagesBc.orderNum), asc(CatalogPagesBc.id));
|
||||
const ids = pages.map((p) => toInt(p.id));
|
||||
const [childCounts, itemCounts] = await Promise.all([
|
||||
ids.length
|
||||
@@ -199,7 +205,7 @@ async function getChildren(
|
||||
})
|
||||
.from(CatalogPages)
|
||||
.where(parentFilter)
|
||||
.orderBy(asc(CatalogPages.orderNum));
|
||||
.orderBy(asc(CatalogPages.orderNum), asc(CatalogPages.id));
|
||||
const ids = pages.map((p) => toInt(p.id));
|
||||
const [childCounts, itemMap] = await Promise.all([
|
||||
ids.length
|
||||
@@ -259,7 +265,7 @@ async function searchPages(q: string, isBc: boolean): Promise<TreeNode[]> {
|
||||
})
|
||||
.from(CatalogPagesBc)
|
||||
.where(or(...conditions))
|
||||
.orderBy(asc(CatalogPagesBc.orderNum))
|
||||
.orderBy(asc(CatalogPagesBc.orderNum), asc(CatalogPagesBc.id))
|
||||
.limit(50);
|
||||
return pages.map((p) =>
|
||||
toTreeNode(
|
||||
@@ -296,7 +302,7 @@ async function searchPages(q: string, isBc: boolean): Promise<TreeNode[]> {
|
||||
})
|
||||
.from(CatalogPages)
|
||||
.where(or(...conditions))
|
||||
.orderBy(asc(CatalogPages.orderNum))
|
||||
.orderBy(asc(CatalogPages.orderNum), asc(CatalogPages.id))
|
||||
.limit(50);
|
||||
const ids = pages.map((p) => toInt(p.id));
|
||||
const [childCounts, itemMap] = await Promise.all([
|
||||
@@ -430,39 +436,13 @@ export const PATCH = withAdmin(
|
||||
return apiError("Invalid pageId");
|
||||
}
|
||||
const isBc = body.catalog === "bc";
|
||||
if (isBc) {
|
||||
const [page] = await db
|
||||
.select({
|
||||
enabled: CatalogPagesBc.enabled,
|
||||
visible: CatalogPagesBc.visible,
|
||||
})
|
||||
.from(CatalogPagesBc)
|
||||
.where(eq(CatalogPagesBc.id, pageId))
|
||||
.limit(1);
|
||||
if (!page) return apiError("Page not found", 404);
|
||||
const current = field === "enabled" ? page.enabled : page.visible;
|
||||
await db
|
||||
.update(CatalogPagesBc)
|
||||
.set({ [field]: current === "1" ? "0" : "1" })
|
||||
.where(eq(CatalogPagesBc.id, pageId));
|
||||
await rcon.updateCatalog();
|
||||
return apiOk({});
|
||||
try {
|
||||
await togglePageCommand(isBc ? "bc" : "normal", pageId, field);
|
||||
} catch (error) {
|
||||
const failure = catalogFailure(error);
|
||||
return apiError(failure.message, failure.status);
|
||||
}
|
||||
const [page] = await db
|
||||
.select({
|
||||
enabled: CatalogPages.enabled,
|
||||
visible: CatalogPages.visible,
|
||||
})
|
||||
.from(CatalogPages)
|
||||
.where(eq(CatalogPages.id, pageId))
|
||||
.limit(1);
|
||||
if (!page) return apiError("Page not found", 404);
|
||||
const current = field === "enabled" ? page.enabled : page.visible;
|
||||
await db
|
||||
.update(CatalogPages)
|
||||
.set({ [field]: current === "1" ? "0" : "1" })
|
||||
.where(eq(CatalogPages.id, pageId));
|
||||
await rcon.updateCatalog();
|
||||
await sendCatalogUpdate();
|
||||
return apiOk({});
|
||||
},
|
||||
);
|
||||
@@ -477,53 +457,13 @@ export const DELETE = withAdmin(
|
||||
if (!Number.isFinite(pageId) || pageId <= 0) {
|
||||
return apiError("Invalid pageId");
|
||||
}
|
||||
if (isBc) {
|
||||
const [page] = await db
|
||||
.select({ parentId: CatalogPagesBc.parentId })
|
||||
.from(CatalogPagesBc)
|
||||
.where(eq(CatalogPagesBc.id, pageId))
|
||||
.limit(1);
|
||||
if (!page) return apiError("Page not found", 404);
|
||||
if (mode === "reparent") {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(CatalogPagesBc)
|
||||
.set({ parentId: page.parentId })
|
||||
.where(eq(CatalogPagesBc.parentId, pageId));
|
||||
await tx
|
||||
.delete(CatalogItemsBc)
|
||||
.where(eq(CatalogItemsBc.pageId, pageId));
|
||||
await tx.delete(CatalogPagesBc).where(eq(CatalogPagesBc.id, pageId));
|
||||
});
|
||||
} else {
|
||||
// Deep cascade: collect all descendants
|
||||
const toDelete: number[] = [pageId];
|
||||
const queue: number[] = [pageId];
|
||||
while (queue.length > 0) {
|
||||
const children = await db
|
||||
.select({ id: CatalogPagesBc.id })
|
||||
.from(CatalogPagesBc)
|
||||
.where(inArray(CatalogPagesBc.parentId, queue));
|
||||
queue.length = 0;
|
||||
for (const child of children) {
|
||||
toDelete.push(child.id);
|
||||
queue.push(child.id);
|
||||
}
|
||||
}
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.delete(CatalogItemsBc)
|
||||
.where(inArray(CatalogItemsBc.pageId, toDelete));
|
||||
await tx
|
||||
.delete(CatalogPagesBc)
|
||||
.where(inArray(CatalogPagesBc.id, toDelete));
|
||||
});
|
||||
}
|
||||
await rcon.updateCatalog();
|
||||
return apiOk({});
|
||||
try {
|
||||
await deletePageCommand(isBc ? "bc" : "normal", pageId, mode);
|
||||
} catch (error) {
|
||||
const failure = catalogFailure(error);
|
||||
return apiError(failure.message, failure.status);
|
||||
}
|
||||
await deletePage(pageId, mode);
|
||||
await rcon.updateCatalog();
|
||||
await sendCatalogUpdate();
|
||||
return apiOk({});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user