69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
// Admin: apply a new menu_order to a set of sibling categories. The
|
|
// request body is the drag-and-drop order the admin committed in the
|
|
// UI: `{ parentId: string | null, categoryIds: [firstId, ...] }`.
|
|
// `parentId` scopes the move — null reorders the root level, otherwise
|
|
// it reorders the children of that parent. The handler writes index
|
|
// positions into `categories.menu_order` in a single transaction, then
|
|
// purges the affected subtree's cached pages (the sidebar nav tree and
|
|
// the parent landing-page grid both reflect category order).
|
|
//
|
|
// Lives at /api/admin/category-order rather than under categories/ on
|
|
// purpose: a static sibling of categories/[id] would make the typed
|
|
// $fetch for /api/admin/categories/${id} ambiguous and narrow its
|
|
// allowed methods. This mirrors page-order, which sidesteps that by
|
|
// living under the [id] segment — reorder can't (parentId may be null).
|
|
|
|
import { defineEventHandler, readBody, createError } from 'h3'
|
|
import { requirePermission } from '../../utils/rbac'
|
|
import { reorderCategories } from '../../database/categories'
|
|
import { purgeCmsCategory } from '../../utils/cmsCache'
|
|
import { logUpdate } from '../../utils/activity-logger'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
await requirePermission(event, 'pages.write')
|
|
|
|
const body = await readBody<{ parentId?: unknown; categoryIds?: unknown }>(event)
|
|
if (!Array.isArray(body?.categoryIds)) {
|
|
throw createError({ statusCode: 400, statusMessage: 'categoryIds array required' })
|
|
}
|
|
|
|
const parentId =
|
|
body.parentId === undefined || body.parentId === null
|
|
? null
|
|
: String(body.parentId)
|
|
|
|
const categoryIds = body.categoryIds
|
|
.map(v => (typeof v === 'string' ? v : null))
|
|
.filter((v): v is string => Boolean(v))
|
|
|
|
if (categoryIds.length === 0) {
|
|
throw createError({ statusCode: 400, statusMessage: 'categoryIds cannot be empty' })
|
|
}
|
|
|
|
try {
|
|
await reorderCategories(parentId, categoryIds)
|
|
} catch (e: any) {
|
|
if (e?.statusCode) {
|
|
throw createError({ statusCode: e.statusCode, statusMessage: e.message })
|
|
}
|
|
throw e
|
|
}
|
|
|
|
// Category order feeds the per-page sidebar (shared across the whole
|
|
// top-level subtree) and the parent's landing-page grid. purgeCmsCategory
|
|
// walks up to the top-level ancestor and purges that entire subtree, so
|
|
// one call per affected top-level tree covers it. Reordering children of
|
|
// a parent shares one tree; reordering roots touches each root's tree.
|
|
if (parentId) {
|
|
await purgeCmsCategory(parentId)
|
|
} else {
|
|
await Promise.all(categoryIds.map(id => purgeCmsCategory(id)))
|
|
}
|
|
|
|
logUpdate('categories', parentId ?? 'root', event, {
|
|
event: 'reorder',
|
|
parentId,
|
|
categoryCount: categoryIds.length
|
|
})
|
|
return { ok: true, categoryIds }
|
|
})
|