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:
1 parent
8ccade73cc
commit
7d50b72ade
8 files changed
+367
-61
No files matched your search
+110
-7
File diff suppressed because it is too large.
Load diff
@@ -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" : "";
|
||||||
|
}
|
||||||
Reference in new issue
Block a user