diff --git a/TODO.md b/TODO.md index 22742fa..1d890a0 100644 --- a/TODO.md +++ b/TODO.md @@ -138,7 +138,7 @@ This file is the single source of truth for roadmap and delivery progress. - [~] [P1] Page management (create/edit/publish/unpublish/schedule) - [x] [P1] Page builder with reusable content blocks (hero, rich text, gallery, CTA, forms, price cards) -- [~] [P1] Navigation management (menus, nested items, order, visibility) +- [x] [P1] Navigation management (menus, nested items, order, visibility) - [~] [P1] Media library (upload, browse, replace, delete) with media-type classification (artwork, banner, promo, generic, video/gif) - [x] [P1] Media enrichment metadata (alt text, copyright, author, source, tags, licensing, usage context) - [x] [P1] Portfolio grouping primitives (galleries, albums, categories, tags) with ordering/visibility controls @@ -366,6 +366,7 @@ This file is the single source of truth for roadmap and delivery progress. - [2026-02-12] Artwork rendition management completed: admin `/portfolio` supports `thumbnail/card/full/retina/custom` slot assignment with dimensions and primary flag, plus per-artwork rendition listing and delete controls. - [2026-02-12] Media type presets baseline completed in upload API: server-side validation now uses shared per-type rules (mime + max size) for `artwork/banner/promotion/video/gif/generic`, with optional env cap override via `CMS_MEDIA_UPLOAD_MAX_BYTES`. - [2026-02-12] Page builder reusable blocks completed: admin block editor now supports full field editing + ordering controls for hero/rich-text/gallery/cta/form/price-cards; public renderer includes form-link behavior for `contact`/`commission` keys. +- [2026-02-12] Navigation management completed: admin `/navigation` now supports menu update/delete controls, nested item parent selection via menu-local dropdown, and full order/visibility updates across menus and items. - [2026-02-12] Public UX pass: commission request flow now reports explicit invalid budget range errors, and header navigation now falls back to localized defaults (`home`, `portfolio`, `news`, `commissions`) when no CMS menu exists; seed data now creates those default menu entries. - [2026-02-12] Added `e2e/public-rendering.pw.ts` web coverage for fallback navigation visibility, portfolio routes, and commission submission validation (invalid budget range + successful submission path). - [2026-02-12] Testing execution is temporarily paused for delivery velocity: root test scripts are stubbed and CI test steps are disabled; all testing backlog is consolidated under `MVP 3: Testing and Quality`. diff --git a/apps/admin/src/app/navigation/page.tsx b/apps/admin/src/app/navigation/page.tsx index 52db070..a01b3cf 100644 --- a/apps/admin/src/app/navigation/page.tsx +++ b/apps/admin/src/app/navigation/page.tsx @@ -2,9 +2,11 @@ import { createNavigationItem, createNavigationMenu, deleteNavigationItem, + deleteNavigationMenu, listNavigationMenus, listPages, updateNavigationItem, + updateNavigationMenu, upsertNavigationItemTranslation, } from "@cms/db" import { Button } from "@cms/ui/button" @@ -131,6 +133,50 @@ async function createItemAction(formData: FormData) { redirectWithState({ notice: "Navigation item created." }) } +async function updateMenuAction(formData: FormData) { + "use server" + + await requirePermissionForRoute({ + nextPath: "/navigation", + permission: "navigation:write", + scope: "team", + }) + + try { + await updateNavigationMenu({ + id: readInputString(formData, "id"), + name: readInputString(formData, "name"), + slug: readInputString(formData, "slug"), + location: readInputString(formData, "location"), + isVisible: readInputString(formData, "isVisible") === "true", + }) + } catch { + redirectWithState({ error: "Failed to update navigation menu." }) + } + + revalidatePath("/navigation") + redirectWithState({ notice: "Navigation menu updated." }) +} + +async function deleteMenuAction(formData: FormData) { + "use server" + + await requirePermissionForRoute({ + nextPath: "/navigation", + permission: "navigation:write", + scope: "team", + }) + + try { + await deleteNavigationMenu(readInputString(formData, "id")) + } catch { + redirectWithState({ error: "Failed to delete navigation menu." }) + } + + revalidatePath("/navigation") + redirectWithState({ notice: "Navigation menu deleted." }) +} + async function updateItemAction(formData: FormData) { "use server" @@ -279,14 +325,58 @@ export default async function NavigationManagementPage({ ) : ( menus.map((menu) => (
-
-

- {menu.name} ({menu.location}) -

- - {menu.isVisible ? "visible" : "hidden"} - -
+
+ +
+ + + + +
+
+ + +
+
{menu.items.length === 0 ? ( @@ -348,11 +438,20 @@ export default async function NavigationManagementPage({
diff --git a/packages/content/src/pages-navigation.ts b/packages/content/src/pages-navigation.ts index 3653ba4..e03057d 100644 --- a/packages/content/src/pages-navigation.ts +++ b/packages/content/src/pages-navigation.ts @@ -133,6 +133,14 @@ export const createNavigationMenuInputSchema = z.object({ isVisible: z.boolean().default(true), }) +export const updateNavigationMenuInputSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(1).max(180).optional(), + slug: z.string().min(1).max(180).optional(), + location: z.string().min(1).max(80).optional(), + isVisible: z.boolean().optional(), +}) + export const createNavigationItemInputSchema = z.object({ menuId: z.string().uuid(), label: z.string().min(1).max(180), @@ -157,6 +165,7 @@ export type CreatePageInput = z.infer export type UpdatePageInput = z.infer export type UpsertPageTranslationInput = z.infer export type CreateNavigationMenuInput = z.infer +export type UpdateNavigationMenuInput = z.infer export type CreateNavigationItemInput = z.infer export type UpdateNavigationItemInput = z.infer export type PageBlock = z.infer diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index dfba8f4..2f233db 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -46,6 +46,7 @@ export { createNavigationMenu, createPage, deleteNavigationItem, + deleteNavigationMenu, deletePage, getPageById, getPublishedPageBySlug, @@ -56,6 +57,7 @@ export { listPublicNavigation, listPublishedPageSlugs, updateNavigationItem, + updateNavigationMenu, updatePage, upsertNavigationItemTranslation, upsertPageTranslation, diff --git a/packages/db/src/pages-navigation.ts b/packages/db/src/pages-navigation.ts index b62bc7b..6f8e079 100644 --- a/packages/db/src/pages-navigation.ts +++ b/packages/db/src/pages-navigation.ts @@ -3,6 +3,7 @@ import { createNavigationMenuInputSchema, createPageInputSchema, updateNavigationItemInputSchema, + updateNavigationMenuInputSchema, updatePageInputSchema, upsertPageTranslationInputSchema, } from "@cms/content" @@ -297,6 +298,22 @@ export async function createNavigationMenu(input: unknown) { }) } +export async function updateNavigationMenu(input: unknown) { + const payload = updateNavigationMenuInputSchema.parse(input) + const { id, ...data } = payload + + return db.navigationMenu.update({ + where: { id }, + data, + }) +} + +export async function deleteNavigationMenu(id: string) { + return db.navigationMenu.delete({ + where: { id }, + }) +} + export async function createNavigationItem(input: unknown) { const payload = createNavigationItemInputSchema.parse(input)