112 lines
3.9 KiB
TypeScript
112 lines
3.9 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
|
|
import { prisma } from "@/lib/db";
|
|
import { requireUserId } from "@/lib/auth-helpers";
|
|
import { boardPath, categoryAccessFilter, projectAccessFilter } from "@/lib/access";
|
|
import { CategoryNameSchema } from "@/lib/validation/category";
|
|
import type { CategoryDTO } from "@/types/board";
|
|
|
|
/**
|
|
* Creates a Category in the caller's Home board, or inside a Project the
|
|
* caller owns when `projectId` is given -- verified against
|
|
* projectAccessFilter() rather than trusted from the argument.
|
|
*/
|
|
export async function createCategory(name: string, projectId?: string): Promise<CategoryDTO> {
|
|
const userId = await requireUserId();
|
|
const parsedName = CategoryNameSchema.parse(name);
|
|
|
|
if (projectId) {
|
|
const project = await prisma.project.findFirst({
|
|
where: { id: projectId, ...projectAccessFilter(userId) },
|
|
select: { id: true },
|
|
});
|
|
if (!project) throw new Error("Project not found");
|
|
}
|
|
|
|
const count = await prisma.category.count({
|
|
where: projectId ? { projectId } : { userId, projectId: null },
|
|
});
|
|
|
|
const category = await prisma.category.create({
|
|
data: projectId
|
|
? { name: parsedName, order: count, projectId }
|
|
: { name: parsedName, order: count, userId },
|
|
});
|
|
|
|
revalidatePath(boardPath(projectId));
|
|
return { id: category.id, name: category.name, order: category.order, groups: [] };
|
|
}
|
|
|
|
export async function renameCategory(categoryId: string, name: string): Promise<void> {
|
|
const userId = await requireUserId();
|
|
const parsedName = CategoryNameSchema.parse(name);
|
|
|
|
const category = await prisma.category.findFirst({
|
|
where: { id: categoryId, ...categoryAccessFilter(userId) },
|
|
select: { id: true, projectId: true },
|
|
});
|
|
if (!category) throw new Error("Category not found");
|
|
|
|
await prisma.category.update({ where: { id: categoryId }, data: { name: parsedName } });
|
|
|
|
revalidatePath(boardPath(category.projectId));
|
|
}
|
|
|
|
export type DeleteCategoryResult = { error?: string };
|
|
|
|
/**
|
|
* Deleting a non-empty category would cascade-delete every group (and
|
|
* their to-dos) inside it -- so this is an expected, user-facing
|
|
* validation rather than an exceptional failure, and is modeled as a
|
|
* return value rather than a thrown error (Next.js redacts thrown Server
|
|
* Action error messages in production, so a specific message needs to
|
|
* come back this way to actually reach the client).
|
|
*/
|
|
export async function deleteCategory(categoryId: string): Promise<DeleteCategoryResult> {
|
|
const userId = await requireUserId();
|
|
|
|
const category = await prisma.category.findFirst({
|
|
where: { id: categoryId, ...categoryAccessFilter(userId) },
|
|
select: { projectId: true, _count: { select: { groups: true } } },
|
|
});
|
|
if (!category) return { error: "Category not found." };
|
|
if (category._count.groups > 0) {
|
|
return { error: "This category still has groups in it. Delete or move them out first." };
|
|
}
|
|
|
|
await prisma.category.delete({ where: { id: categoryId } });
|
|
|
|
revalidatePath(boardPath(category.projectId));
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* Persists a new lane order for every category in one board -- the
|
|
* caller's Home board, or a Project they own when `projectId` is given.
|
|
* `orderedIds` must contain every category id in that scope.
|
|
*/
|
|
export async function reorderCategories(orderedIds: string[], projectId?: string): Promise<void> {
|
|
const userId = await requireUserId();
|
|
|
|
const owned = await prisma.category.findMany({
|
|
where: projectId
|
|
? { projectId, project: projectAccessFilter(userId) }
|
|
: { userId, projectId: null },
|
|
select: { id: true },
|
|
});
|
|
const ownedIds = new Set(owned.map((c) => c.id));
|
|
if (orderedIds.length !== ownedIds.size || !orderedIds.every((id) => ownedIds.has(id))) {
|
|
throw new Error("Category list does not match this board's categories");
|
|
}
|
|
|
|
await prisma.$transaction(
|
|
orderedIds.map((id, index) =>
|
|
prisma.category.update({ where: { id }, data: { order: index } })
|
|
)
|
|
);
|
|
|
|
revalidatePath(boardPath(projectId));
|
|
}
|