refactor(admin): remove legacy menu editor

This commit is contained in:
Simo committed 2026-09-05 20:08:19 +02:00
1 parent 98b0009206
commit 667637f8da
12 files changed
+13 -461

No files matched your search

-43
View File
@@ -1,43 +0,0 @@
"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 });
},
);
File diff suppressed because it is too large. Load diff
-49
View File
@@ -1,49 +0,0 @@
import { ListOrdered } from "lucide-react";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { AdminPageShell } from "@/components/admin/admin-page-shell";
import { ADMIN_NAV_GROUPS } from "@/lib/admin-nav";
import {
ADMIN_NAV_CONFIG_KEY,
parseAdminNavConfig,
} from "@/lib/admin-nav-config";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { siteSettings } from "@/lib/services/site-settings";
import { AdminMenuEditor } from "./admin-menu-editor";
export const metadata = { title: "Admin menu" };
export default async function AdminMenuPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SETTINGS_VIEW, session.user.rank)) {
redirect("/admin");
}
const canEdit = canAccess(
permissions,
PERMS.SETTINGS_EDIT,
session.user.rank,
);
const t = await getTranslations("pages.admin.menu");
const catalog = ADMIN_NAV_GROUPS.map((g) => ({
labelKey: g.labelKey,
items: g.items.map((i) => ({ href: i.href, labelKey: i.labelKey })),
}));
const initialConfig = parseAdminNavConfig(
await siteSettings.get(ADMIN_NAV_CONFIG_KEY, ""),
);
return (
<AdminPageShell
icon={ListOrdered}
title={t("title")}
subtitle={t("subtitle")}
>
<AdminMenuEditor
catalog={catalog}
initialConfig={initialConfig}
canEdit={canEdit}
/>
</AdminPageShell>
);
}
@@ -612,8 +612,8 @@ describe("housekeeping foundation completion contracts", () => {
HOUSEKEEPING_MIGRATION_MATRIX, HOUSEKEEPING_MIGRATION_MATRIX,
); );
expect(HOUSEKEEPING_MIGRATION_MATRIX).toHaveLength(139); expect(HOUSEKEEPING_MIGRATION_MATRIX).toHaveLength(138);
expect(discovered).toHaveLength(139); expect(discovered).toHaveLength(138);
expect(issues).toEqual([]); expect(issues).toEqual([]);
}); });
}); });
@@ -29,7 +29,7 @@ describe("discoverLegacyPages", () => {
it("discovers the exact legacy administration inventory", () => { it("discovers the exact legacy administration inventory", () => {
const pages = discoverLegacyPages(); const pages = discoverLegacyPages();
expect(pages).toHaveLength(139); expect(pages).toHaveLength(138);
expect(pages).toContainEqual({ expect(pages).toContainEqual({
surface: "admin", surface: "admin",
legacyPath: "/admin/users/:id/edit", legacyPath: "/admin/users/:id/edit",
@@ -4,10 +4,10 @@ import { HOUSEKEEPING_MIGRATION_MATRIX } from "./matrix";
import { validateMigrationEntries } from "./validate-matrix"; import { validateMigrationEntries } from "./validate-matrix";
describe("HOUSEKEEPING_MIGRATION_MATRIX", () => { describe("HOUSEKEEPING_MIGRATION_MATRIX", () => {
it("covers all 139 legacy pages exactly once", () => { it("covers all 138 legacy pages exactly once", () => {
const discovered = discoverLegacyPages(); const discovered = discoverLegacyPages();
expect(HOUSEKEEPING_MIGRATION_MATRIX).toHaveLength(139); expect(HOUSEKEEPING_MIGRATION_MATRIX).toHaveLength(138);
expect( expect(
validateMigrationEntries(discovered, HOUSEKEEPING_MIGRATION_MATRIX), validateMigrationEntries(discovered, HOUSEKEEPING_MIGRATION_MATRIX),
).toEqual([]); ).toEqual([]);
@@ -13,14 +13,13 @@ const SYSTEM_PREFIXES = [
"/admin/housekeeping", "/admin/housekeeping",
"/admin/logs", "/admin/logs",
"/admin/maintenance", "/admin/maintenance",
"/admin/menu",
"/admin/permissions", "/admin/permissions",
"/admin/settings", "/admin/settings",
] as const; ] as const;
describe("systemMigrationEntries", () => { describe("systemMigrationEntries", () => {
it("covers all 20 System pages exactly once", () => { it("covers all 19 System pages exactly once", () => {
expect(systemMigrationEntries).toHaveLength(20); expect(systemMigrationEntries).toHaveLength(19);
expect( expect(
validateMigrationEntries( validateMigrationEntries(
ownedLegacyPages(SYSTEM_PREFIXES), ownedLegacyPages(SYSTEM_PREFIXES),
@@ -37,11 +36,6 @@ describe("systemMigrationEntries", () => {
decision: "REMOVE", decision: "REMOVE",
targetPath: null, targetPath: null,
}), }),
expect.objectContaining({
legacyPath: "/admin/menu",
decision: "REMOVE",
targetPath: null,
}),
]), ]),
); );
}); });
@@ -346,28 +346,6 @@ export const systemMigrationEntries: readonly MigrationEntry[] = [
accessibility: "PARTIAL", accessibility: "PARTIAL",
notes: ["Maintenance changes global login availability and splash content"], notes: ["Maintenance changes global login availability and splash content"],
}), }),
plannedSystemEntry({
surface: "admin",
legacyPath: "/admin/menu",
sourceFile: "src/app/admin/menu/page.tsx",
targetPath: null,
decision: "REMOVE",
capabilities: {
read: [PERMS.SETTINGS_VIEW],
mutate: [PERMS.SETTINGS_EDIT],
},
dependencies: {
queries: ["ADMIN_NAV_GROUPS", "siteSettings.get"],
mutations: ["saveAdminNavConfig", "siteSettings.update"],
},
auditRequirement: "PRIVILEGED_MUTATION",
localization: "PARTIAL",
accessibility: "PARTIAL",
requiredTests: ["integration", "e2e"],
notes: [
"Remove the manual legacy menu editor because foundation manifests own navigation",
],
}),
plannedSystemEntry({ plannedSystemEntry({
surface: "admin", surface: "admin",
legacyPath: "/admin/permissions", legacyPath: "/admin/permissions",
+1 -4
View File
@@ -64,10 +64,7 @@ describe("admin-nav-config", () => {
hiddenItems: ["/admin/photos", "/admin", "/admin/menu"], hiddenItems: ["/admin/photos", "/admin", "/admin/menu"],
}); });
expect(applied.map((g) => g.labelKey)).toEqual(["overview", "content"]); expect(applied.map((g) => g.labelKey)).toEqual(["overview", "content"]);
expect(applied[0]?.items.map((i) => i.href)).toEqual([ expect(applied[0]?.items.map((i) => i.href)).toEqual(["/admin"]);
"/admin",
"/admin/menu",
]);
expect(applied[1]?.items.map((i) => i.href)).toEqual(["/admin/articles"]); expect(applied[1]?.items.map((i) => i.href)).toEqual(["/admin/articles"]);
expect(ADMIN_NAV_PINNED_HREFS.has("/admin")).toBe(true); expect(ADMIN_NAV_PINNED_HREFS.has("/admin")).toBe(true);
}); });
+2 -2
View File
@@ -3,8 +3,8 @@ import type { AdminNavGroup } from "@/lib/admin-nav";
/** website_settings key storing JSON overlay for the admin sidebar. */ /** website_settings key storing JSON overlay for the admin sidebar. */
export const ADMIN_NAV_CONFIG_KEY = "admin_nav_config"; export const ADMIN_NAV_CONFIG_KEY = "admin_nav_config";
/** Hrefs that cannot be hidden (dashboard + menu editor). */ /** Hrefs that cannot be hidden (dashboard). */
export const ADMIN_NAV_PINNED_HREFS = new Set(["/admin", "/admin/menu"]); export const ADMIN_NAV_PINNED_HREFS = new Set(["/admin"]);
export type AdminNavConfig = { export type AdminNavConfig = {
/** Group labelKeys in display order. Missing groups append in catalog order. */ /** Group labelKeys in display order. Missing groups append in catalog order. */
-7
View File
@@ -16,7 +16,6 @@ import {
KeyRound, KeyRound,
Languages, Languages,
LayoutDashboard, LayoutDashboard,
ListOrdered,
type LucideIcon, type LucideIcon,
Megaphone, Megaphone,
Monitor, Monitor,
@@ -541,12 +540,6 @@ export const ADMIN_NAV_GROUPS: AdminNavGroup[] = [
icon: Cog, icon: Cog,
permission: PERMS.SETTINGS_VIEW, permission: PERMS.SETTINGS_VIEW,
}, },
{
href: "/admin/menu",
labelKey: "adminMenu",
icon: ListOrdered,
permission: PERMS.SETTINGS_VIEW,
},
{ {
href: "/admin/theme", href: "/admin/theme",
labelKey: "theme", labelKey: "theme",
+3 -3
View File
@@ -75,10 +75,10 @@ describe("staff smoke contract", () => {
expect(existsSync("src/lib/admin/ops-online-users.ts")).toBe(true); expect(existsSync("src/lib/admin/ops-online-users.ts")).toBe(true);
}); });
it("ships dynamic admin menu overlay", () => { it("keeps sidebar configuration without exposing the retired menu editor", () => {
expect(existsSync("src/lib/admin-nav-config.ts")).toBe(true); expect(existsSync("src/lib/admin-nav-config.ts")).toBe(true);
expect(existsSync("src/app/admin/menu/page.tsx")).toBe(true); expect(existsSync("src/app/admin/menu/page.tsx")).toBe(false);
expect(readFileSync("src/lib/admin-nav.ts", "utf8")).toContain( expect(readFileSync("src/lib/admin-nav.ts", "utf8")).not.toContain(
"/admin/menu", "/admin/menu",
); );
expect( expect(