Merge pull request 'feat(catalog): Visual Manager BC + honest canEdit' (#2) from feat/visual-manager-bc-canedit into main
Local Build and Deploy / deploy (push) Successful in 1m27s
Local Build and Deploy / deploy (push) Successful in 1m27s
Reviewed-on: #2
This commit is contained in:
commit
b7fd19b6d9
8 files changed
+457
-151
No files matched your search
+110
-7
@@ -169,19 +169,24 @@ export async function toggleBcPage({
|
||||
export async function createBcPage(input: {
|
||||
caption: string;
|
||||
parentId: number;
|
||||
pageLayout: string;
|
||||
pageLayout?: string;
|
||||
iconImage?: number;
|
||||
iconColor?: number;
|
||||
enabled?: "0" | "1";
|
||||
visible?: "0" | "1";
|
||||
orderNum?: number;
|
||||
}) {
|
||||
const staff = await requirePermission(PERMS.CATALOG_EDIT);
|
||||
const created = await prisma.catalogPagesBc.create({
|
||||
data: {
|
||||
caption: input.caption,
|
||||
parentId: input.parentId,
|
||||
pageLayout: input.pageLayout,
|
||||
iconColor: 0,
|
||||
iconImage: 0,
|
||||
orderNum: 0,
|
||||
visible: "1",
|
||||
enabled: "1",
|
||||
pageLayout: input.pageLayout ?? "default_3x3",
|
||||
iconColor: input.iconColor ?? 0,
|
||||
iconImage: input.iconImage ?? 0,
|
||||
orderNum: input.orderNum ?? 0,
|
||||
visible: input.visible ?? "1",
|
||||
enabled: input.enabled ?? "1",
|
||||
pageHeadline: "",
|
||||
pageTeaser: "",
|
||||
},
|
||||
@@ -194,6 +199,104 @@ export async function createBcPage(input: {
|
||||
targetType: "catalog_page_bc",
|
||||
targetId: created.id,
|
||||
});
|
||||
revalidatePath("/admin/catalog");
|
||||
revalidatePath("/admin/catalog/builder-club");
|
||||
return { ok: true as const, data: { id: created.id } };
|
||||
}
|
||||
|
||||
async function moveBcPage(pageId: number, newParentId: number): Promise<void> {
|
||||
if (newParentId > 0) {
|
||||
let currentId = newParentId;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
if (currentId === pageId) {
|
||||
throw new Error("Cannot move page: would create a circular hierarchy");
|
||||
}
|
||||
const parent = await prisma.catalogPagesBc.findUnique({
|
||||
where: { id: currentId },
|
||||
select: { parentId: true },
|
||||
});
|
||||
if (!parent || parent.parentId <= 0) break;
|
||||
currentId = parent.parentId;
|
||||
}
|
||||
}
|
||||
await prisma.catalogPagesBc.update({
|
||||
where: { id: pageId },
|
||||
data: { parentId: newParentId },
|
||||
});
|
||||
}
|
||||
|
||||
export async function reorderBcTreePage(input: {
|
||||
pageId: number;
|
||||
newParentId?: number;
|
||||
newOrderNum: number;
|
||||
}) {
|
||||
await requirePermission(PERMS.CATALOG_EDIT);
|
||||
if (input.newParentId !== undefined) {
|
||||
try {
|
||||
await moveBcPage(input.pageId, input.newParentId);
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false as const,
|
||||
error: err instanceof Error ? err.message : "Invalid move",
|
||||
};
|
||||
}
|
||||
}
|
||||
await prisma.catalogPagesBc.update({
|
||||
where: { id: input.pageId },
|
||||
data: { orderNum: input.newOrderNum },
|
||||
});
|
||||
await rcon.updateCatalog();
|
||||
revalidatePath("/admin/catalog");
|
||||
revalidatePath("/admin/catalog/builder-club");
|
||||
return { ok: true as const, data: {} };
|
||||
}
|
||||
|
||||
export async function deleteBcTreePage(input: {
|
||||
pageId: number;
|
||||
mode: "reparent" | "cascade";
|
||||
}) {
|
||||
await requirePermission(PERMS.CATALOG_EDIT);
|
||||
const page = await prisma.catalogPagesBc.findUnique({
|
||||
where: { id: input.pageId },
|
||||
select: { parentId: true },
|
||||
});
|
||||
if (!page) return { ok: false as const, error: "Page not found" };
|
||||
|
||||
if (input.mode === "reparent") {
|
||||
await prisma.$transaction([
|
||||
prisma.catalogPagesBc.updateMany({
|
||||
where: { parentId: input.pageId },
|
||||
data: { parentId: page.parentId },
|
||||
}),
|
||||
prisma.catalogItemsBc.deleteMany({ where: { pageId: input.pageId } }),
|
||||
prisma.catalogPagesBc.delete({ where: { id: input.pageId } }),
|
||||
]);
|
||||
} else {
|
||||
const toDelete: number[] = [input.pageId];
|
||||
const queue: number[] = [input.pageId];
|
||||
while (queue.length > 0) {
|
||||
const children = await prisma.catalogPagesBc.findMany({
|
||||
where: { parentId: { in: queue } },
|
||||
select: { id: true },
|
||||
});
|
||||
queue.length = 0;
|
||||
for (const child of children) {
|
||||
toDelete.push(child.id);
|
||||
queue.push(child.id);
|
||||
}
|
||||
}
|
||||
await prisma.$transaction([
|
||||
prisma.catalogItemsBc.deleteMany({
|
||||
where: { pageId: { in: toDelete } },
|
||||
}),
|
||||
prisma.catalogPagesBc.deleteMany({
|
||||
where: { id: { in: toDelete } },
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
await rcon.updateCatalog();
|
||||
revalidatePath("/admin/catalog");
|
||||
revalidatePath("/admin/catalog/builder-club");
|
||||
return { ok: true as const, data: {} };
|
||||
}
|
||||
@@ -208,6 +208,19 @@ export default async function CatalogPagesPage({
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<CatalogViewToggle currentView={viewMode} catalogType="bc" />
|
||||
{canEdit && (
|
||||
<CatalogManagerDialog
|
||||
catalogType="bc"
|
||||
canEdit={canEdit}
|
||||
initialRoots={bcRootPages.map((p) => ({
|
||||
id: p.id,
|
||||
caption: p.caption,
|
||||
iconImage: p.iconImage,
|
||||
enabled: p.enabled,
|
||||
orderNum: 0,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
{canEdit && <CatalogActions catalogType="bc" />}
|
||||
</div>
|
||||
</div>
|
||||
@@ -383,6 +396,8 @@ export default async function CatalogPagesPage({
|
||||
<CatalogViewToggle currentView={viewMode} catalogType="normal" />
|
||||
{canEdit && (
|
||||
<CatalogManagerDialog
|
||||
catalogType="normal"
|
||||
canEdit={canEdit}
|
||||
initialRoots={rootPages.map((p) => ({
|
||||
id: p.id,
|
||||
caption: p.caption,
|
||||
|
||||
@@ -5,8 +5,9 @@ import { Fragment, useEffect } from "react";
|
||||
import { useCatalogManager } from "./use-catalog-store";
|
||||
|
||||
export function BreadcrumbBar() {
|
||||
const { state, dispatch } = useCatalogManager();
|
||||
const { state, dispatch, catalogType } = useCatalogManager();
|
||||
const { selectedPageId, ancestors } = state;
|
||||
const catQs = catalogType === "bc" ? "&catalog=bc" : "";
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedPageId || selectedPageId <= 0) {
|
||||
@@ -14,7 +15,7 @@ export function BreadcrumbBar() {
|
||||
return;
|
||||
}
|
||||
const ac = new AbortController();
|
||||
fetch(`/api/admin/catalog/tree?ancestors=${selectedPageId}`, {
|
||||
fetch(`/api/admin/catalog/tree?ancestors=${selectedPageId}${catQs}`, {
|
||||
signal: ac.signal,
|
||||
})
|
||||
.then((r) => r.json())
|
||||
@@ -25,7 +26,7 @@ export function BreadcrumbBar() {
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => ac.abort();
|
||||
}, [selectedPageId, dispatch]);
|
||||
}, [selectedPageId, dispatch, catQs]);
|
||||
|
||||
if (!selectedPageId || ancestors.length === 0) return null;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { toast } from "sonner";
|
||||
import { createCatalogPage } from "@/actions/catalog";
|
||||
import { createBcPage } from "@/actions/catalog-bc";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import type { RootTab } from "@/types/catalog";
|
||||
@@ -13,7 +14,12 @@ import { InlineEditor, type PageUpdateData } from "./inline-editor";
|
||||
import { RootTabs } from "./root-tabs";
|
||||
import { SortableTree } from "./sortable-tree";
|
||||
import { StatsBar } from "./stats-bar";
|
||||
import { CatalogManagerProvider, useCatalogManager } from "./use-catalog-store";
|
||||
import {
|
||||
CatalogManagerProvider,
|
||||
type CatalogManagerMode,
|
||||
catalogTreeQs,
|
||||
useCatalogManager,
|
||||
} from "./use-catalog-store";
|
||||
|
||||
/**
|
||||
* Visual Catalog Manager — matches habbo-next UX (full-viewport editor).
|
||||
@@ -21,8 +27,12 @@ import { CatalogManagerProvider, useCatalogManager } from "./use-catalog-store";
|
||||
*/
|
||||
export function CatalogManagerDialog({
|
||||
initialRoots = [],
|
||||
catalogType = "normal",
|
||||
canEdit = false,
|
||||
}: {
|
||||
initialRoots?: RootTab[];
|
||||
catalogType?: CatalogManagerMode;
|
||||
canEdit?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
@@ -46,6 +56,9 @@ export function CatalogManagerDialog({
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const title =
|
||||
catalogType === "bc" ? "BC Visual Manager" : "Visual Manager";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
@@ -55,7 +68,7 @@ export function CatalogManagerDialog({
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
Visual Manager
|
||||
{title}
|
||||
</Button>
|
||||
{mounted && open
|
||||
? createPortal(
|
||||
@@ -68,7 +81,10 @@ export function CatalogManagerDialog({
|
||||
className="fixed top-[2.5vh] left-[1vw] z-[201] flex h-[95vh] max-h-[95vh] w-[98vw] max-w-[98vw] flex-col overflow-hidden rounded-lg border border-[var(--admin-border)] bg-[var(--admin-canvas)] text-[var(--admin-text)] shadow-2xl"
|
||||
style={{ backgroundColor: "var(--admin-canvas)" }}
|
||||
>
|
||||
<CatalogManagerProvider>
|
||||
<CatalogManagerProvider
|
||||
catalogType={catalogType}
|
||||
canEdit={canEdit}
|
||||
>
|
||||
<CatalogManagerInner
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
@@ -93,12 +109,15 @@ function CatalogManagerInner({
|
||||
onOpenChange: (v: boolean) => void;
|
||||
initialRoots: RootTab[];
|
||||
}) {
|
||||
const { state, dispatch } = useCatalogManager();
|
||||
const { state, dispatch, catalogType, canEdit } = useCatalogManager();
|
||||
const catQs = catalogTreeQs(catalogType);
|
||||
|
||||
const loadRootPages = useCallback(async () => {
|
||||
dispatch({ type: "SET_LOADING_ROOT", loading: true });
|
||||
try {
|
||||
const res = await fetch("/api/admin/catalog/tree?parentId=-1");
|
||||
const res = await fetch(
|
||||
`/api/admin/catalog/tree?parentId=-1${catQs}`,
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "Failed to load roots");
|
||||
const pages: RootTab[] = (data.pages || []).map(
|
||||
@@ -122,7 +141,7 @@ function CatalogManagerInner({
|
||||
toast.error("Failed to load catalog root pages");
|
||||
dispatch({ type: "SET_LOADING_ROOT", loading: false });
|
||||
}
|
||||
}, [dispatch, state.activeTabId]);
|
||||
}, [dispatch, state.activeTabId, catQs]);
|
||||
|
||||
// Seed from SSR immediately, then refresh from API.
|
||||
useEffect(() => {
|
||||
@@ -157,13 +176,20 @@ function CatalogManagerInner({
|
||||
};
|
||||
|
||||
const handleCreateRoot = async () => {
|
||||
if (!canEdit) return;
|
||||
const caption = prompt("Root page name:");
|
||||
if (!caption?.trim()) return;
|
||||
try {
|
||||
const result = await createCatalogPage({
|
||||
parentId: -1,
|
||||
caption: caption.trim(),
|
||||
});
|
||||
const result =
|
||||
catalogType === "bc"
|
||||
? await createBcPage({
|
||||
parentId: -1,
|
||||
caption: caption.trim(),
|
||||
})
|
||||
: await createCatalogPage({
|
||||
parentId: -1,
|
||||
caption: caption.trim(),
|
||||
});
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
await loadRootPages();
|
||||
toast.success("Root page created");
|
||||
@@ -211,6 +237,11 @@ function CatalogManagerInner({
|
||||
}
|
||||
};
|
||||
|
||||
const heading =
|
||||
catalogType === "bc"
|
||||
? "BC Visual Catalog Manager"
|
||||
: "Visual Catalog Manager";
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<div
|
||||
@@ -222,7 +253,7 @@ function CatalogManagerInner({
|
||||
className="flex items-center gap-2 text-base font-semibold leading-none"
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
Visual Catalog Manager
|
||||
{heading}
|
||||
</h2>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -15,6 +15,7 @@ import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { updateCatalogPage } from "@/actions/catalog";
|
||||
import { updateBcPage } from "@/actions/catalog-bc";
|
||||
import type { CatalogItemData } from "@/app/admin/catalog/[id]/catalog-items-table";
|
||||
import { CatalogItemsTable } from "@/app/admin/catalog/[id]/catalog-items-table";
|
||||
import { CatalogTranslateTab } from "@/app/admin/catalog/[id]/catalog-translate-tab";
|
||||
@@ -45,7 +46,7 @@ import {
|
||||
} from "@/lib/catalog-layouts";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ImagePreview } from "./image-preview";
|
||||
import { useCatalogManager } from "./use-catalog-store";
|
||||
import { catalogTreeQs, useCatalogManager } from "./use-catalog-store";
|
||||
|
||||
interface PageDetail {
|
||||
id: number;
|
||||
@@ -104,7 +105,12 @@ interface InlineEditorProps {
|
||||
type FormState = Record<string, string | number | null>;
|
||||
|
||||
export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
const { dispatch } = useCatalogManager();
|
||||
const { dispatch, catalogType, canEdit } = useCatalogManager();
|
||||
const catQs = catalogTreeQs(catalogType);
|
||||
const pageHref =
|
||||
catalogType === "bc"
|
||||
? `/admin/catalog/builder-club/${pageId}`
|
||||
: `/admin/catalog/${pageId}`;
|
||||
const [page, setPage] = useState<PageDetail | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -133,48 +139,53 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
||||
e.preventDefault();
|
||||
if (isDirty && !saving && page) handleSave();
|
||||
if (canEdit && isDirty && !saving && page) handleSave();
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const loadPage = useCallback(async (id: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/admin/catalog/tree?pageId=${id}`);
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
const p = data.page as PageDetail;
|
||||
setPage(p);
|
||||
const formData: FormState = {
|
||||
caption: p.caption,
|
||||
pageLayout: p.pageLayout,
|
||||
enabled: String(p.enabled),
|
||||
visible: String(p.visible),
|
||||
minRank: p.minRank,
|
||||
clubOnly: String(p.clubOnly),
|
||||
orderNum: p.orderNum,
|
||||
iconImage: p.iconImage,
|
||||
iconColor: p.iconColor,
|
||||
pageHeadline: p.pageHeadline || "",
|
||||
pageTeaser: p.pageTeaser || "",
|
||||
pageSpecial: p.pageSpecial || "",
|
||||
pageText1: p.pageText1 || "",
|
||||
pageText2: p.pageText2 || "",
|
||||
pageTextDetails: p.pageTextDetails || "",
|
||||
pageTextTeaser: p.pageTextTeaser || "",
|
||||
};
|
||||
setForm(formData);
|
||||
setOriginal(formData);
|
||||
} catch {
|
||||
toast.error("Failed to load page");
|
||||
setPage(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
const loadPage = useCallback(
|
||||
async (id: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/admin/catalog/tree?pageId=${id}${catQs}`,
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
const p = data.page as PageDetail;
|
||||
setPage(p);
|
||||
const formData: FormState = {
|
||||
caption: p.caption,
|
||||
pageLayout: p.pageLayout,
|
||||
enabled: String(p.enabled),
|
||||
visible: String(p.visible),
|
||||
minRank: p.minRank ?? 0,
|
||||
clubOnly: String(p.clubOnly ?? "0"),
|
||||
orderNum: p.orderNum,
|
||||
iconImage: p.iconImage,
|
||||
iconColor: p.iconColor,
|
||||
pageHeadline: p.pageHeadline || "",
|
||||
pageTeaser: p.pageTeaser || "",
|
||||
pageSpecial: p.pageSpecial || "",
|
||||
pageText1: p.pageText1 || "",
|
||||
pageText2: p.pageText2 || "",
|
||||
pageTextDetails: p.pageTextDetails || "",
|
||||
pageTextTeaser: p.pageTextTeaser || "",
|
||||
};
|
||||
setForm(formData);
|
||||
setOriginal(formData);
|
||||
} catch {
|
||||
toast.error("Failed to load page");
|
||||
setPage(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[catQs],
|
||||
);
|
||||
|
||||
const loadItemsData = useCallback(async (id: number) => {
|
||||
setItemsLoading(true);
|
||||
@@ -211,8 +222,9 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
}
|
||||
}, [pageId, loadPage]);
|
||||
|
||||
// Lazy-load items
|
||||
// Lazy-load items (shop catalog only — BC items live on the detail page)
|
||||
useEffect(() => {
|
||||
if (catalogType === "bc") return;
|
||||
if (
|
||||
(activeTab === "items" || activeTab === "translate") &&
|
||||
!itemsLoaded &&
|
||||
@@ -222,17 +234,28 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
) {
|
||||
loadItemsData(pageId);
|
||||
}
|
||||
}, [activeTab, itemsLoaded, itemsLoading, pageId, loadItemsData]);
|
||||
}, [
|
||||
activeTab,
|
||||
itemsLoaded,
|
||||
itemsLoading,
|
||||
pageId,
|
||||
loadItemsData,
|
||||
catalogType,
|
||||
]);
|
||||
|
||||
function update(key: string, value: string | number | null) {
|
||||
if (!canEdit) return;
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!page) return;
|
||||
if (!page || !canEdit) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const result = await updateCatalogPage({ id: page.id, ...form });
|
||||
const result =
|
||||
catalogType === "bc"
|
||||
? await updateBcPage({ id: page.id, ...form })
|
||||
: await updateCatalogPage({ id: page.id, ...form });
|
||||
if (!result.ok) throw new Error(result.error || "Failed to save");
|
||||
toast.success("Page updated & catalog synced");
|
||||
setOriginal({ ...form });
|
||||
@@ -323,7 +346,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link href={`/admin/catalog/${page.id}`} target="_blank">
|
||||
<Link href={pageHref} target="_blank">
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs gap-1">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Full editor
|
||||
@@ -347,7 +370,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
variant="outline"
|
||||
className="h-6 text-[11px] px-2"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
disabled={saving || !canEdit}
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
||||
@@ -609,7 +632,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving || !isDirty}
|
||||
disabled={saving || !isDirty || !canEdit}
|
||||
className="w-full"
|
||||
>
|
||||
{saving ? (
|
||||
@@ -628,7 +651,19 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
value="items"
|
||||
className="flex-1 overflow-y-auto px-4 pb-4 mt-2"
|
||||
>
|
||||
{itemsLoading ? (
|
||||
{catalogType === "bc" ? (
|
||||
<div className="rounded-md border border-dashed px-4 py-8 text-center text-sm text-muted-foreground space-y-3">
|
||||
<p>
|
||||
BC items are managed on the full Builder Club page editor.
|
||||
</p>
|
||||
<Link href={pageHref} target="_blank">
|
||||
<Button variant="outline" size="sm" className="gap-1">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Open BC page editor
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : itemsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
@@ -636,11 +671,18 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
<div className="space-y-3">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<QuickAddFurni
|
||||
pageId={page.id}
|
||||
furniRevisionMap={itemsData.furniRevisionMap}
|
||||
/>
|
||||
<BulkImportItems pageId={page.id} onImported={reloadItems} />
|
||||
{canEdit && (
|
||||
<>
|
||||
<QuickAddFurni
|
||||
pageId={page.id}
|
||||
furniRevisionMap={itemsData.furniRevisionMap}
|
||||
/>
|
||||
<BulkImportItems
|
||||
pageId={page.id}
|
||||
onImported={reloadItems}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -657,7 +699,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
items={itemsData.items}
|
||||
furniRevisionMap={itemsData.furniRevisionMap}
|
||||
layout={page.pageLayout}
|
||||
canEdit={true}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -666,7 +708,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
items={itemsData.items}
|
||||
pageId={page.id}
|
||||
pageLayout={page.pageLayout}
|
||||
canEdit={true}
|
||||
canEdit={canEdit}
|
||||
interactionTypes={itemsData.interactionTypes}
|
||||
furniRevisionMap={itemsData.furniRevisionMap}
|
||||
allPages={itemsData.allPages}
|
||||
@@ -680,7 +722,11 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
value="translate"
|
||||
className="flex-1 overflow-y-auto px-4 pb-4 mt-2"
|
||||
>
|
||||
{itemsLoading ? (
|
||||
{catalogType === "bc" ? (
|
||||
<div className="rounded-md border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
Translate tools are available for the shop catalog only.
|
||||
</div>
|
||||
) : itemsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
@@ -688,7 +734,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
||||
<CatalogTranslateTab
|
||||
baseItems={itemsData.baseItems}
|
||||
catalogNameMap={itemsData.catalogNameMap}
|
||||
canEdit={true}
|
||||
canEdit={canEdit}
|
||||
furniDataIdList={itemsData.furniDataIdList}
|
||||
furniDescriptionMap={itemsData.furniDescriptionMap}
|
||||
furniRevisionMap={itemsData.furniRevisionMap}
|
||||
|
||||
@@ -25,6 +25,11 @@ import {
|
||||
reorderTreePage,
|
||||
toggleCatalogPage,
|
||||
} from "@/actions/catalog";
|
||||
import {
|
||||
deleteBcTreePage,
|
||||
reorderBcTreePage,
|
||||
toggleBcPage,
|
||||
} from "@/actions/catalog-bc";
|
||||
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
|
||||
import { CatalogIcon } from "@/components/admin/catalog-tree";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -36,6 +41,7 @@ import {
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCatalogManager } from "./use-catalog-store";
|
||||
|
||||
export type { RootTab } from "@/types/catalog";
|
||||
|
||||
@@ -58,6 +64,7 @@ export function RootTabs({
|
||||
onCreateRoot,
|
||||
loading,
|
||||
}: RootTabsProps) {
|
||||
const { catalogType, canEdit } = useCatalogManager();
|
||||
const { confirm, dialog: confirmDialog } = useConfirmDialog();
|
||||
const [activeDragId, setActiveDragId] = useState<number | null>(null);
|
||||
const sensors = useSensors(
|
||||
@@ -78,13 +85,16 @@ export function RootTabs({
|
||||
const newIndex = tabs.findIndex((t) => t.id === over.id);
|
||||
if (oldIndex === -1 || newIndex === -1) return;
|
||||
|
||||
if (!canEdit) return;
|
||||
const newOrder = arrayMove(tabs, oldIndex, newIndex);
|
||||
|
||||
// Batch reorder
|
||||
try {
|
||||
const reorder =
|
||||
catalogType === "bc" ? reorderBcTreePage : reorderTreePage;
|
||||
const results = await Promise.allSettled(
|
||||
newOrder.map((tab, idx) =>
|
||||
reorderTreePage({ pageId: tab.id, newOrderNum: idx + 1 }),
|
||||
reorder({ pageId: tab.id, newOrderNum: idx + 1 }),
|
||||
),
|
||||
);
|
||||
const failed = results.filter((r) => r.status === "rejected").length;
|
||||
@@ -94,12 +104,17 @@ export function RootTabs({
|
||||
toast.error("Failed to reorder tabs");
|
||||
}
|
||||
},
|
||||
[tabs],
|
||||
[tabs, canEdit, catalogType],
|
||||
);
|
||||
|
||||
const handleToggle = async (tab: RootTab) => {
|
||||
if (!canEdit) return;
|
||||
try {
|
||||
await toggleCatalogPage({ id: tab.id, action: "toggleEnabled" });
|
||||
if (catalogType === "bc") {
|
||||
await toggleBcPage({ id: tab.id, field: "enabled" });
|
||||
} else {
|
||||
await toggleCatalogPage({ id: tab.id, action: "toggleEnabled" });
|
||||
}
|
||||
toast.success(
|
||||
`${tab.caption} ${tab.enabled === "1" ? "disabled" : "enabled"}`,
|
||||
);
|
||||
@@ -109,6 +124,7 @@ export function RootTabs({
|
||||
};
|
||||
|
||||
const handleDelete = async (tab: RootTab) => {
|
||||
if (!canEdit) return;
|
||||
const ok = await confirm({
|
||||
title: "Delete root page",
|
||||
description: `Delete root page "${tab.caption}"? Children will be reparented.`,
|
||||
@@ -116,7 +132,11 @@ export function RootTabs({
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteTreePage({ pageId: tab.id, mode: "reparent" });
|
||||
if (catalogType === "bc") {
|
||||
await deleteBcTreePage({ pageId: tab.id, mode: "reparent" });
|
||||
} else {
|
||||
await deleteTreePage({ pageId: tab.id, mode: "reparent" });
|
||||
}
|
||||
toast.success(`"${tab.caption}" deleted`);
|
||||
} catch {
|
||||
toast.error("Failed to delete");
|
||||
@@ -168,16 +188,18 @@ export function RootTabs({
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 shrink-0 ml-1"
|
||||
title="New root page"
|
||||
onClick={onCreateRoot}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 shrink-0 ml-1"
|
||||
title="New root page"
|
||||
onClick={onCreateRoot}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,13 @@ import {
|
||||
toggleCatalogPage,
|
||||
updateCatalogPage,
|
||||
} from "@/actions/catalog";
|
||||
import {
|
||||
createBcPage,
|
||||
deleteBcTreePage,
|
||||
reorderBcTreePage,
|
||||
toggleBcPage,
|
||||
updateBcPage,
|
||||
} from "@/actions/catalog-bc";
|
||||
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -56,7 +63,7 @@ import {
|
||||
TreeItemContent,
|
||||
type TreeItemProps,
|
||||
} from "./tree-item";
|
||||
import { useCatalogManager } from "./use-catalog-store";
|
||||
import { catalogTreeQs, useCatalogManager } from "./use-catalog-store";
|
||||
|
||||
/* ─── Types ──────────────────────────────────────────────── */
|
||||
|
||||
@@ -83,7 +90,10 @@ export function SortableTree({
|
||||
onTreeMutation,
|
||||
nodeUpdate,
|
||||
}: SortableTreeProps) {
|
||||
const { state, dispatch } = useCatalogManager();
|
||||
const { state, dispatch, catalogType, canEdit } = useCatalogManager();
|
||||
const catQs = catalogTreeQs(catalogType);
|
||||
const pageBase =
|
||||
catalogType === "bc" ? "/admin/catalog/builder-club" : "/admin/catalog";
|
||||
const { confirm, dialog: confirmDialog } = useConfirmDialog();
|
||||
const {
|
||||
nodes,
|
||||
@@ -137,13 +147,13 @@ export function SortableTree({
|
||||
const loadChildren = useCallback(
|
||||
async (parentId: number): Promise<TreeNodeData[]> => {
|
||||
const res = await fetch(
|
||||
`/api/admin/catalog/tree?parentId=${parentId}`,
|
||||
`/api/admin/catalog/tree?parentId=${parentId}${catQs}`,
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "Failed to load pages");
|
||||
return normalizePages(data.pages || []);
|
||||
},
|
||||
[normalizePages],
|
||||
[normalizePages, catQs],
|
||||
);
|
||||
|
||||
const loadTabTree = useCallback(async () => {
|
||||
@@ -188,7 +198,7 @@ export function SortableTree({
|
||||
setSearching(true);
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/admin/catalog/tree?search=${encodeURIComponent(searchQuery.trim())}`,
|
||||
`/api/admin/catalog/tree?search=${encodeURIComponent(searchQuery.trim())}${catQs}`,
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
@@ -201,7 +211,7 @@ export function SortableTree({
|
||||
return () => {
|
||||
if (searchTimeout.current) clearTimeout(searchTimeout.current);
|
||||
};
|
||||
}, [searchQuery]);
|
||||
}, [searchQuery, catQs]);
|
||||
|
||||
// Apply external node updates
|
||||
useEffect(() => {
|
||||
@@ -435,10 +445,13 @@ export function SortableTree({
|
||||
children: newOrder.map((id) => nodes.get(id)!).filter(Boolean),
|
||||
});
|
||||
|
||||
if (!canEdit) return;
|
||||
const reorder =
|
||||
catalogType === "bc" ? reorderBcTreePage : reorderTreePage;
|
||||
try {
|
||||
const results = await Promise.allSettled(
|
||||
newOrder.map((id, idx) =>
|
||||
reorderTreePage({ pageId: id, newOrderNum: idx + 1 }),
|
||||
reorder({ pageId: id, newOrderNum: idx + 1 }),
|
||||
),
|
||||
);
|
||||
const failed = results.filter((r) => r.status === "rejected").length;
|
||||
@@ -450,11 +463,14 @@ export function SortableTree({
|
||||
loadFullTree();
|
||||
}
|
||||
} else {
|
||||
if (!canEdit) return;
|
||||
const reorder =
|
||||
catalogType === "bc" ? reorderBcTreePage : reorderTreePage;
|
||||
const newParentId = overNode.parentId;
|
||||
const siblings = childrenMap.get(newParentId) || [];
|
||||
const overIndex = siblings.indexOf(over.id as number);
|
||||
try {
|
||||
const result = await reorderTreePage({
|
||||
const result = await reorder({
|
||||
pageId: active.id as number,
|
||||
newParentId,
|
||||
newOrderNum: overIndex >= 0 ? overIndex + 1 : siblings.length + 1,
|
||||
@@ -471,18 +487,25 @@ export function SortableTree({
|
||||
|
||||
// ── Actions ─────────────────────────────────────────────────
|
||||
const handleAddSubpage = (parentId: number) => {
|
||||
if (!canEdit) return;
|
||||
setSubpageParentId(parentId);
|
||||
setSubpageName("");
|
||||
};
|
||||
|
||||
const handleCreateSubpage = async () => {
|
||||
if (subpageParentId === null || !subpageName.trim()) return;
|
||||
if (!canEdit || subpageParentId === null || !subpageName.trim()) return;
|
||||
setCreatingSubpage(true);
|
||||
try {
|
||||
const result = await createCatalogPage({
|
||||
parentId: subpageParentId,
|
||||
caption: subpageName.trim(),
|
||||
});
|
||||
const result =
|
||||
catalogType === "bc"
|
||||
? await createBcPage({
|
||||
parentId: subpageParentId,
|
||||
caption: subpageName.trim(),
|
||||
})
|
||||
: await createCatalogPage({
|
||||
parentId: subpageParentId,
|
||||
caption: subpageName.trim(),
|
||||
});
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
dispatch({ type: "SET_EXPANDED", id: subpageParentId, expanded: true });
|
||||
await loadFullTree();
|
||||
@@ -500,9 +523,13 @@ export function SortableTree({
|
||||
};
|
||||
|
||||
const handleToggleEnabled = async (id: number, currentEnabled: string) => {
|
||||
if (!canEdit) return;
|
||||
const newEnabled = currentEnabled === "1" ? "0" : "1";
|
||||
try {
|
||||
const result = await toggleCatalogPage({ id, action: "toggleEnabled" });
|
||||
const result =
|
||||
catalogType === "bc"
|
||||
? await toggleBcPage({ id, field: "enabled" })
|
||||
: await toggleCatalogPage({ id, action: "toggleEnabled" });
|
||||
if (!result.ok) throw new Error();
|
||||
dispatch({ type: "UPDATE_NODE", update: { id, enabled: newEnabled } });
|
||||
onTreeMutation();
|
||||
@@ -512,9 +539,13 @@ export function SortableTree({
|
||||
};
|
||||
|
||||
const handleToggleVisible = async (id: number, currentVisible: string) => {
|
||||
if (!canEdit) return;
|
||||
const newVisible = currentVisible === "1" ? "0" : "1";
|
||||
try {
|
||||
const result = await toggleCatalogPage({ id, action: "toggleVisible" });
|
||||
const result =
|
||||
catalogType === "bc"
|
||||
? await toggleBcPage({ id, field: "visible" })
|
||||
: await toggleCatalogPage({ id, action: "toggleVisible" });
|
||||
if (!result.ok) throw new Error();
|
||||
dispatch({ type: "UPDATE_NODE", update: { id, visible: newVisible } });
|
||||
onTreeMutation();
|
||||
@@ -524,6 +555,7 @@ export function SortableTree({
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!canEdit) return;
|
||||
const node = nodes.get(id);
|
||||
const ok = await confirm({
|
||||
title: "Delete page",
|
||||
@@ -532,7 +564,10 @@ export function SortableTree({
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
const result = await deleteTreePage({ pageId: id, mode: "reparent" });
|
||||
const result =
|
||||
catalogType === "bc"
|
||||
? await deleteBcTreePage({ pageId: id, mode: "reparent" })
|
||||
: await deleteTreePage({ pageId: id, mode: "reparent" });
|
||||
if (!result.ok) throw new Error();
|
||||
dispatch({ type: "REMOVE_NODE", id });
|
||||
if (selectedPageId === id) onSelectPage(-1);
|
||||
@@ -544,32 +579,59 @@ export function SortableTree({
|
||||
};
|
||||
|
||||
const handleDuplicate = async (id: number) => {
|
||||
if (!canEdit) return;
|
||||
try {
|
||||
const res = await fetch(`/api/admin/catalog/tree?pageId=${id}`);
|
||||
const res = await fetch(
|
||||
`/api/admin/catalog/tree?pageId=${id}${catQs}`,
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
const { page } = await res.json();
|
||||
const result = await createCatalogPage({
|
||||
parentId: page.parentId ?? -1,
|
||||
caption: `${page.caption} (copy)`,
|
||||
pageLayout: page.pageLayout ?? "default_3x3",
|
||||
iconImage: page.iconImage ?? 0,
|
||||
iconColor: page.iconColor ?? 0,
|
||||
enabled: "0",
|
||||
visible: page.visible ?? "1",
|
||||
minRank: page.minRank ?? 1,
|
||||
orderNum: (page.orderNum ?? 0) + 1,
|
||||
});
|
||||
const result =
|
||||
catalogType === "bc"
|
||||
? await createBcPage({
|
||||
parentId: page.parentId ?? -1,
|
||||
caption: `${page.caption} (copy)`,
|
||||
pageLayout: page.pageLayout ?? "default_3x3",
|
||||
iconImage: page.iconImage ?? 0,
|
||||
iconColor: page.iconColor ?? 0,
|
||||
enabled: "0",
|
||||
visible: page.visible ?? "1",
|
||||
orderNum: (page.orderNum ?? 0) + 1,
|
||||
})
|
||||
: await createCatalogPage({
|
||||
parentId: page.parentId ?? -1,
|
||||
caption: `${page.caption} (copy)`,
|
||||
pageLayout: page.pageLayout ?? "default_3x3",
|
||||
iconImage: page.iconImage ?? 0,
|
||||
iconColor: page.iconColor ?? 0,
|
||||
enabled: "0",
|
||||
visible: page.visible ?? "1",
|
||||
minRank: page.minRank ?? 1,
|
||||
orderNum: (page.orderNum ?? 0) + 1,
|
||||
});
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
const newId = (result.data as Record<string, unknown>).id as number;
|
||||
await updateCatalogPage({
|
||||
id: newId,
|
||||
pageHeadline: page.pageHeadline ?? "",
|
||||
pageTeaser: page.pageTeaser ?? "",
|
||||
pageText1: page.pageText1,
|
||||
pageText2: page.pageText2,
|
||||
pageTextDetails: page.pageTextDetails,
|
||||
pageTextTeaser: page.pageTextTeaser,
|
||||
});
|
||||
if (catalogType === "bc") {
|
||||
await updateBcPage({
|
||||
id: newId,
|
||||
pageHeadline: page.pageHeadline ?? "",
|
||||
pageTeaser: page.pageTeaser ?? "",
|
||||
pageText1: page.pageText1,
|
||||
pageText2: page.pageText2,
|
||||
pageTextDetails: page.pageTextDetails,
|
||||
pageTextTeaser: page.pageTextTeaser,
|
||||
});
|
||||
} else {
|
||||
await updateCatalogPage({
|
||||
id: newId,
|
||||
pageHeadline: page.pageHeadline ?? "",
|
||||
pageTeaser: page.pageTeaser ?? "",
|
||||
pageText1: page.pageText1,
|
||||
pageText2: page.pageText2,
|
||||
pageTextDetails: page.pageTextDetails,
|
||||
pageTextTeaser: page.pageTextTeaser,
|
||||
});
|
||||
}
|
||||
await loadFullTree();
|
||||
toast.success(`Duplicated "${page.caption}" (items not copied)`);
|
||||
onSelectPage(newId);
|
||||
@@ -580,7 +642,7 @@ export function SortableTree({
|
||||
};
|
||||
|
||||
const handleOpenFull = (id: number) =>
|
||||
window.open(`/admin/catalog/${id}`, "_blank");
|
||||
window.open(`${pageBase}/${id}`, "_blank");
|
||||
|
||||
// ── Expand/Collapse all ─────────────────────────────────────
|
||||
const handleExpandAll = useCallback(() => {
|
||||
@@ -610,13 +672,16 @@ export function SortableTree({
|
||||
|
||||
// ── Bulk actions ────────────────────────────────────────────
|
||||
const handleBulkEnable = async (enable: boolean) => {
|
||||
if (!canEdit) return;
|
||||
const ids = [...multiSelectedIds];
|
||||
const results = await Promise.allSettled(
|
||||
ids.map((id) => {
|
||||
const node = nodes.get(id);
|
||||
if (!node || (node.enabled === "1") === enable)
|
||||
return Promise.resolve();
|
||||
return toggleCatalogPage({ id, action: "toggleEnabled" });
|
||||
return catalogType === "bc"
|
||||
? toggleBcPage({ id, field: "enabled" })
|
||||
: toggleCatalogPage({ id, action: "toggleEnabled" });
|
||||
}),
|
||||
);
|
||||
for (const id of ids)
|
||||
@@ -633,6 +698,7 @@ export function SortableTree({
|
||||
};
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
if (!canEdit) return;
|
||||
const ids = [...multiSelectedIds];
|
||||
const ok = await confirm({
|
||||
title: "Delete pages",
|
||||
@@ -640,8 +706,9 @@ export function SortableTree({
|
||||
confirmLabel: "Delete",
|
||||
});
|
||||
if (!ok) return;
|
||||
const del = catalogType === "bc" ? deleteBcTreePage : deleteTreePage;
|
||||
await Promise.allSettled(
|
||||
ids.map((id) => deleteTreePage({ pageId: id, mode: "reparent" })),
|
||||
ids.map((id) => del({ pageId: id, mode: "reparent" })),
|
||||
);
|
||||
dispatch({ type: "CLEAR_MULTI_SELECT" });
|
||||
await loadFullTree();
|
||||
@@ -768,25 +835,27 @@ export function SortableTree({
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={sortMode ? "default" : "outline"}
|
||||
size="icon"
|
||||
className="h-7 w-7 shrink-0"
|
||||
onClick={() => dispatch({ type: "TOGGLE_SORT_MODE" })}
|
||||
>
|
||||
{sortMode ? (
|
||||
<GripVertical className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<MousePointer className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{sortMode ? "Sort mode" : "Edit mode"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{canEdit && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={sortMode ? "default" : "outline"}
|
||||
size="icon"
|
||||
className="h-7 w-7 shrink-0"
|
||||
onClick={() => dispatch({ type: "TOGGLE_SORT_MODE" })}
|
||||
>
|
||||
{sortMode ? (
|
||||
<GripVertical className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<MousePointer className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{sortMode ? "Sort mode" : "Edit mode"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filters + expand/collapse */}
|
||||
|
||||
@@ -314,19 +314,33 @@ function reducer(
|
||||
|
||||
// ── Context ───────────────────────────────────────────────────
|
||||
|
||||
export type CatalogManagerMode = "normal" | "bc";
|
||||
|
||||
interface CatalogManagerContextValue {
|
||||
state: CatalogManagerState;
|
||||
dispatch: Dispatch<CatalogAction>;
|
||||
catalogType: CatalogManagerMode;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
const CatalogManagerContext = createContext<CatalogManagerContextValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function CatalogManagerProvider({ children }: { children: ReactNode }) {
|
||||
export function CatalogManagerProvider({
|
||||
children,
|
||||
catalogType = "normal",
|
||||
canEdit = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
catalogType?: CatalogManagerMode;
|
||||
canEdit?: boolean;
|
||||
}) {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
return (
|
||||
<CatalogManagerContext.Provider value={{ state, dispatch }}>
|
||||
<CatalogManagerContext.Provider
|
||||
value={{ state, dispatch, catalogType, canEdit }}
|
||||
>
|
||||
{children}
|
||||
</CatalogManagerContext.Provider>
|
||||
);
|
||||
@@ -340,3 +354,8 @@ export function useCatalogManager() {
|
||||
);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/** Append to tree API query strings when in Builder Club mode. */
|
||||
export function catalogTreeQs(catalogType: CatalogManagerMode): string {
|
||||
return catalogType === "bc" ? "&catalog=bc" : "";
|
||||
}
|
||||
Reference in new issue
Block a user