From 0224b34f15f93c213af9921bf25fbde6edc5474a Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Thu, 30 Jul 2026 21:45:53 +0200 Subject: [PATCH] feat(admin): configurable sidebar menu order and visibility Co-authored-by: Cursor --- src/actions/admin-nav-menu.ts | 43 +++ src/app/admin/layout.tsx | 9 + src/app/admin/menu/admin-menu-editor.tsx | 318 +++++++++++++++++++++ src/app/admin/menu/page.tsx | 49 ++++ src/components/admin/admin-sidebar-nav.tsx | 19 +- src/lib/admin-nav-config.test.ts | 88 ++++++ src/lib/admin-nav-config.ts | 120 ++++++++ src/lib/admin-nav.ts | 7 + src/lib/staff-smoke-contract.test.ts | 14 + src/messages/en.json | 19 ++ src/messages/it.json | 19 ++ 11 files changed, 700 insertions(+), 5 deletions(-) create mode 100644 src/actions/admin-nav-menu.ts create mode 100644 src/app/admin/menu/admin-menu-editor.tsx create mode 100644 src/app/admin/menu/page.tsx create mode 100644 src/lib/admin-nav-config.test.ts create mode 100644 src/lib/admin-nav-config.ts diff --git a/src/actions/admin-nav-menu.ts b/src/actions/admin-nav-menu.ts new file mode 100644 index 00000000..7460a81f --- /dev/null +++ b/src/actions/admin-nav-menu.ts @@ -0,0 +1,43 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { z } from "zod"; +import { + ADMIN_NAV_CONFIG_KEY, + type AdminNavConfig, + serializeAdminNavConfig, +} from "@/lib/admin-nav-config"; +import { actionOk, adminAction } from "@/lib/foundation/action"; +import { PERMS } from "@/lib/permissions"; +import { siteSettings } from "@/lib/services/site-settings"; + +const schema = z.object({ + groupOrder: z.array(z.string()), + hiddenGroups: z.array(z.string()), + hiddenItems: z.array(z.string()), + itemOrder: z.record(z.string(), z.array(z.string())), +}); + +export const saveAdminNavConfig = adminAction( + { + permission: PERMS.SETTINGS_EDIT, + schema, + rateLimitKey: "admin-nav-config-save", + rateLimitMax: 30, + }, + async (ctx) => { + const config: AdminNavConfig = { + groupOrder: ctx.data.groupOrder, + hiddenGroups: ctx.data.hiddenGroups, + hiddenItems: ctx.data.hiddenItems, + itemOrder: ctx.data.itemOrder, + }; + await siteSettings.update( + ADMIN_NAV_CONFIG_KEY, + serializeAdminNavConfig(config), + ); + revalidatePath("/admin", "layout"); + revalidatePath("/admin/menu"); + return actionOk({ saved: true }); + }, +); diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index bc5806d0..41762864 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -11,6 +11,10 @@ import { LanguageSwitcher } from "@/components/language-switcher"; import { ThemeSwitcher } from "@/components/theme-switcher"; import { requireStaff } from "@/lib/admin/guard"; import { collectNavPermissionSlugs } from "@/lib/admin-nav"; +import { + ADMIN_NAV_CONFIG_KEY, + parseAdminNavConfig, +} from "@/lib/admin-nav-config"; import { setCsrfCookie } from "@/lib/foundation/security"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; @@ -86,6 +90,10 @@ async function Sidebar({ return slug.endsWith(".view") || slug === PERMS.ADMIN_DASHBOARD; }); + const navConfig = parseAdminNavConfig( + await siteSettings.get(ADMIN_NAV_CONFIG_KEY, ""), + ); + return (