Harden catalog admin: fix translate/quick-add, RCON sync, and confirm UX.
Local Build and Deploy / deploy (push) Successful in 1m18s

Restore broken Quick Add search, correct Translate saves to items_base and FurnitureData, reparent page deletes, sync RCON on create/toggle/BC mutations, and replace native confirms/prompts with dialogs.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-17 21:12:58 +02:00
1 parent de9f837da1
commit b52e25578d
20 files changed
+557 -216

No files matched your search

+10 -1
View File
@@ -28,6 +28,7 @@ export async function updateBcPage({
export async function deleteBcItem({ id }: { id: number }) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogItemsBc.delete({ where: { id } });
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
action: "bc_item_delete",
@@ -36,6 +37,7 @@ export async function deleteBcItem({ id }: { id: number }) {
targetId: id,
});
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const };
}
export async function updateBcItem({
@@ -50,6 +52,7 @@ export async function updateBcItem({
}) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogItemsBc.update({ where: { id }, data: data as any });
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
action: "bc_item_update",
@@ -58,6 +61,7 @@ export async function updateBcItem({
targetId: id,
});
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const };
}
export async function createBcItem({
@@ -74,6 +78,7 @@ export async function createBcItem({
const created = await prisma.catalogItemsBc.create({
data: { pageId, ...data },
});
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
action: "bc_item_create",
@@ -82,6 +87,7 @@ export async function createBcItem({
targetId: created.id,
});
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const, data: { id: created.id } };
}
export async function toggleBcPage({
@@ -96,12 +102,14 @@ export async function toggleBcPage({
where: { id },
select: { enabled: true, visible: true },
});
if (!page) return;
if (!page) return { ok: false as const, error: "Page not found" };
await prisma.catalogPagesBc.update({
where: { id },
data: { [field]: page[field] === "1" ? "0" : "1" },
});
await rcon.updateCatalog();
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const };
}
export async function createBcPage(input: {
@@ -124,6 +132,7 @@ export async function createBcPage(input: {
pageTeaser: "",
},
});
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
action: "bc_page_create",
+71 -23
View File
@@ -118,45 +118,93 @@ export async function updateCatalogItem({
export async function translateCatalogItems({
items,
}: {
/** `id` is items_base.id (not catalog_items.id) */
items: Array<{ id: number; publicName: string; description: string }>;
}) {
await requirePermission(PERMS.CATALOG_EDIT);
const { invalidateFurniDataCache } = await import(
"@/lib/services/catalog-items-loader"
);
const { patchFurniEntryNames } = await import("@/lib/services/furni-data");
let namesUpdated = 0;
let descriptionsUpdated = 0;
let furniDataUpdated = 0;
const _furniDataInserted = 0;
const furniPatches: Array<{
classname: string;
itemType: string;
name?: string;
description?: string;
spriteId?: number;
createIfMissing?: boolean;
}> = [];
for (const item of items) {
const existing = await prisma.catalogItems.findUnique({
const base = await prisma.itemsBase.findUnique({
where: { id: item.id },
select: { catalogName: true },
select: {
id: true,
publicName: true,
itemName: true,
type: true,
spriteId: true,
},
});
if (!existing) continue;
if (!base) continue;
if (item.publicName && item.publicName !== existing.catalogName) {
await prisma.catalogItems.update({
where: { id: item.id },
data: { catalogName: item.publicName },
const nextName = item.publicName?.trim() ?? "";
const nextDesc = item.description ?? "";
const nameChanged = nextName !== "" && nextName !== (base.publicName ?? "");
if (nameChanged) {
await prisma.itemsBase.update({
where: { id: base.id },
data: { publicName: nextName },
});
// Sync catalogName for catalog rows that reference this base item
const idStr = String(base.id);
const related = await prisma.catalogItems.findMany({
where: {
OR: [
{ itemIds: idStr },
{ itemIds: { startsWith: `${idStr};` } },
{ itemIds: { contains: `;${idStr};` } },
{ itemIds: { endsWith: `;${idStr}` } },
],
},
select: { id: true, catalogName: true },
});
for (const row of related) {
if (row.catalogName !== nextName) {
await prisma.catalogItems.update({
where: { id: row.id },
data: { catalogName: nextName },
});
}
}
namesUpdated++;
}
if (item.description) {
const baseItem = await prisma.itemsBase.findFirst({
where: { itemName: existing.catalogName },
select: { id: true, publicName: true },
if (nextDesc !== "" || nameChanged) {
descriptionsUpdated += nextDesc !== "" ? 1 : 0;
furniPatches.push({
classname: base.itemName,
itemType: base.type || "s",
name: nextName || base.publicName || base.itemName,
description: nextDesc,
spriteId: base.spriteId,
createIfMissing: true,
});
if (baseItem) {
await prisma.itemsBase.update({
where: { id: baseItem.id },
data: { publicName: item.publicName || baseItem.publicName },
});
furniDataUpdated++;
}
descriptionsUpdated++;
}
}
const furniResult =
furniPatches.length > 0
? await patchFurniEntryNames(furniPatches)
: { updated: 0, inserted: 0 };
if (furniResult.updated > 0 || furniResult.inserted > 0) {
invalidateFurniDataCache();
}
await rcon.updateCatalog();
revalidatePath("/admin/catalog");
return {
@@ -164,8 +212,8 @@ export async function translateCatalogItems({
data: {
namesUpdated,
descriptionsUpdated,
furniDataUpdated,
furniDataInserted: 0,
furniDataUpdated: furniResult.updated,
furniDataInserted: furniResult.inserted,
updated: items.length,
},
};
+16 -32
View File
@@ -5,6 +5,7 @@ import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import type { ActionResult } from "@/lib/safe-action-shared";
import { deletePage, movePage } from "@/lib/services/catalog-tree";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
@@ -28,7 +29,7 @@ export async function updateCatalogPage({
export async function deleteCatalogPage({ id }: { id: number }) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogPages.delete({ where: { id } });
await deletePage(id, "reparent");
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
@@ -60,6 +61,7 @@ export async function toggleCatalogPage({
where: { id },
data: { [field]: current === "1" ? "0" : "1" },
});
await rcon.updateCatalog();
revalidatePath("/admin/catalog");
return { ok: true as const, data: {} };
}
@@ -95,6 +97,7 @@ export async function createCatalogPage(input: {
includes: "",
},
});
await rcon.updateCatalog();
await logStaffActivity({
staffId: staff.id,
action: "catalog_page_create",
@@ -112,14 +115,19 @@ export async function reorderTreePage(input: {
newOrderNum: number;
}) {
await requirePermission(PERMS.CATALOG_EDIT);
if (input.newParentId !== undefined) {
try {
await movePage(input.pageId, input.newParentId);
} catch (err) {
return {
ok: false as const,
error: err instanceof Error ? err.message : "Invalid move",
};
}
}
await prisma.catalogPages.update({
where: { id: input.pageId },
data: {
orderNum: input.newOrderNum,
...(input.newParentId === undefined
? {}
: { parentId: input.newParentId }),
},
data: { orderNum: input.newOrderNum },
});
await rcon.updateCatalog();
revalidatePath("/admin/catalog");
@@ -131,31 +139,7 @@ export async function deleteTreePage(input: {
mode: "reparent" | "cascade";
}) {
await requirePermission(PERMS.CATALOG_EDIT);
if (input.mode === "cascade") {
const children = await prisma.catalogPages.findMany({
where: { parentId: input.pageId },
select: { id: true },
});
const pageIds = [input.pageId, ...children.map((child) => child.id)];
await prisma.$transaction([
prisma.catalogItems.deleteMany({ where: { pageId: { in: pageIds } } }),
prisma.catalogPages.deleteMany({ where: { id: { in: pageIds } } }),
]);
} else {
const page = await prisma.catalogPages.findUnique({
where: { id: input.pageId },
select: { parentId: true },
});
if (!page) return { ok: false as const, error: "Catalog page not found" };
await prisma.$transaction([
prisma.catalogPages.updateMany({
where: { parentId: input.pageId },
data: { parentId: page.parentId },
}),
prisma.catalogItems.deleteMany({ where: { pageId: input.pageId } }),
prisma.catalogPages.delete({ where: { id: input.pageId } }),
]);
}
await deletePage(input.pageId, input.mode);
await rcon.updateCatalog();
revalidatePath("/admin/catalog");
return { ok: true as const, data: {} };