feat(catalog): Visual Manager for Builder Club with honest canEdit

Mount BC Visual Manager, thread catalogType through tree/editor actions, and stop hardcoding canEdit=true in items/translate tabs.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-18 21:03:07 +02:00
1 parent 8ccade73cc
commit 7d50b72ade
8 files changed
+367 -61

No files matched your search

File diff suppressed because it is too large. Load diff
+15
View File
@@ -208,6 +208,19 @@ export default async function CatalogPagesPage({
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CatalogViewToggle currentView={viewMode} catalogType="bc" /> <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" />} {canEdit && <CatalogActions catalogType="bc" />}
</div> </div>
</div> </div>
@@ -383,6 +396,8 @@ export default async function CatalogPagesPage({
<CatalogViewToggle currentView={viewMode} catalogType="normal" /> <CatalogViewToggle currentView={viewMode} catalogType="normal" />
{canEdit && ( {canEdit && (
<CatalogManagerDialog <CatalogManagerDialog
catalogType="normal"
canEdit={canEdit}
initialRoots={rootPages.map((p) => ({ initialRoots={rootPages.map((p) => ({
id: p.id, id: p.id,
caption: p.caption, caption: p.caption,
@@ -5,8 +5,9 @@ import { Fragment, useEffect } from "react";
import { useCatalogManager } from "./use-catalog-store"; import { useCatalogManager } from "./use-catalog-store";
export function BreadcrumbBar() { export function BreadcrumbBar() {
const { state, dispatch } = useCatalogManager(); const { state, dispatch, catalogType } = useCatalogManager();
const { selectedPageId, ancestors } = state; const { selectedPageId, ancestors } = state;
const catQs = catalogType === "bc" ? "&catalog=bc" : "";
useEffect(() => { useEffect(() => {
if (!selectedPageId || selectedPageId <= 0) { if (!selectedPageId || selectedPageId <= 0) {
@@ -14,7 +15,7 @@ export function BreadcrumbBar() {
return; return;
} }
const ac = new AbortController(); const ac = new AbortController();
fetch(`/api/admin/catalog/tree?ancestors=${selectedPageId}`, { fetch(`/api/admin/catalog/tree?ancestors=${selectedPageId}${catQs}`, {
signal: ac.signal, signal: ac.signal,
}) })
.then((r) => r.json()) .then((r) => r.json())
@@ -25,7 +26,7 @@ export function BreadcrumbBar() {
}) })
.catch(() => {}); .catch(() => {});
return () => ac.abort(); return () => ac.abort();
}, [selectedPageId, dispatch]); }, [selectedPageId, dispatch, catQs]);
if (!selectedPageId || ancestors.length === 0) return null; if (!selectedPageId || ancestors.length === 0) return null;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -25,6 +25,11 @@ import {
reorderTreePage, reorderTreePage,
toggleCatalogPage, toggleCatalogPage,
} from "@/actions/catalog"; } from "@/actions/catalog";
import {
deleteBcTreePage,
reorderBcTreePage,
toggleBcPage,
} from "@/actions/catalog-bc";
import { useConfirmDialog } from "@/components/admin/confirm-dialog"; import { useConfirmDialog } from "@/components/admin/confirm-dialog";
import { CatalogIcon } from "@/components/admin/catalog-tree"; import { CatalogIcon } from "@/components/admin/catalog-tree";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -36,6 +41,7 @@ import {
ContextMenuTrigger, ContextMenuTrigger,
} from "@/components/ui/context-menu"; } from "@/components/ui/context-menu";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useCatalogManager } from "./use-catalog-store";
export type { RootTab } from "@/types/catalog"; export type { RootTab } from "@/types/catalog";
@@ -58,6 +64,7 @@ export function RootTabs({
onCreateRoot, onCreateRoot,
loading, loading,
}: RootTabsProps) { }: RootTabsProps) {
const { catalogType, canEdit } = useCatalogManager();
const { confirm, dialog: confirmDialog } = useConfirmDialog(); const { confirm, dialog: confirmDialog } = useConfirmDialog();
const [activeDragId, setActiveDragId] = useState<number | null>(null); const [activeDragId, setActiveDragId] = useState<number | null>(null);
const sensors = useSensors( const sensors = useSensors(
@@ -78,13 +85,16 @@ export function RootTabs({
const newIndex = tabs.findIndex((t) => t.id === over.id); const newIndex = tabs.findIndex((t) => t.id === over.id);
if (oldIndex === -1 || newIndex === -1) return; if (oldIndex === -1 || newIndex === -1) return;
if (!canEdit) return;
const newOrder = arrayMove(tabs, oldIndex, newIndex); const newOrder = arrayMove(tabs, oldIndex, newIndex);
// Batch reorder // Batch reorder
try { try {
const reorder =
catalogType === "bc" ? reorderBcTreePage : reorderTreePage;
const results = await Promise.allSettled( const results = await Promise.allSettled(
newOrder.map((tab, idx) => 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; const failed = results.filter((r) => r.status === "rejected").length;
@@ -94,12 +104,17 @@ export function RootTabs({
toast.error("Failed to reorder tabs"); toast.error("Failed to reorder tabs");
} }
}, },
[tabs], [tabs, canEdit, catalogType],
); );
const handleToggle = async (tab: RootTab) => { const handleToggle = async (tab: RootTab) => {
if (!canEdit) return;
try { try {
if (catalogType === "bc") {
await toggleBcPage({ id: tab.id, field: "enabled" });
} else {
await toggleCatalogPage({ id: tab.id, action: "toggleEnabled" }); await toggleCatalogPage({ id: tab.id, action: "toggleEnabled" });
}
toast.success( toast.success(
`${tab.caption} ${tab.enabled === "1" ? "disabled" : "enabled"}`, `${tab.caption} ${tab.enabled === "1" ? "disabled" : "enabled"}`,
); );
@@ -109,6 +124,7 @@ export function RootTabs({
}; };
const handleDelete = async (tab: RootTab) => { const handleDelete = async (tab: RootTab) => {
if (!canEdit) return;
const ok = await confirm({ const ok = await confirm({
title: "Delete root page", title: "Delete root page",
description: `Delete root page "${tab.caption}"? Children will be reparented.`, description: `Delete root page "${tab.caption}"? Children will be reparented.`,
@@ -116,7 +132,11 @@ export function RootTabs({
}); });
if (!ok) return; if (!ok) return;
try { try {
if (catalogType === "bc") {
await deleteBcTreePage({ pageId: tab.id, mode: "reparent" });
} else {
await deleteTreePage({ pageId: tab.id, mode: "reparent" }); await deleteTreePage({ pageId: tab.id, mode: "reparent" });
}
toast.success(`"${tab.caption}" deleted`); toast.success(`"${tab.caption}" deleted`);
} catch { } catch {
toast.error("Failed to delete"); toast.error("Failed to delete");
@@ -168,6 +188,7 @@ export function RootTabs({
)} )}
</DragOverlay> </DragOverlay>
</DndContext> </DndContext>
{canEdit && (
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -178,6 +199,7 @@ export function RootTabs({
> >
<Plus className="h-3.5 w-3.5" /> <Plus className="h-3.5 w-3.5" />
</Button> </Button>
)}
</div> </div>
); );
} }
File diff suppressed because it is too large. Load diff
@@ -314,19 +314,33 @@ function reducer(
// ── Context ─────────────────────────────────────────────────── // ── Context ───────────────────────────────────────────────────
export type CatalogManagerMode = "normal" | "bc";
interface CatalogManagerContextValue { interface CatalogManagerContextValue {
state: CatalogManagerState; state: CatalogManagerState;
dispatch: Dispatch<CatalogAction>; dispatch: Dispatch<CatalogAction>;
catalogType: CatalogManagerMode;
canEdit: boolean;
} }
const CatalogManagerContext = createContext<CatalogManagerContextValue | null>( const CatalogManagerContext = createContext<CatalogManagerContextValue | null>(
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); const [state, dispatch] = useReducer(reducer, initialState);
return ( return (
<CatalogManagerContext.Provider value={{ state, dispatch }}> <CatalogManagerContext.Provider
value={{ state, dispatch, catalogType, canEdit }}
>
{children} {children}
</CatalogManagerContext.Provider> </CatalogManagerContext.Provider>
); );
@@ -340,3 +354,8 @@ export function useCatalogManager() {
); );
return ctx; return ctx;
} }
/** Append to tree API query strings when in Builder Club mode. */
export function catalogTreeQs(catalogType: CatalogManagerMode): string {
return catalogType === "bc" ? "&catalog=bc" : "";
}