Organize/lib/actions/groups.ts

178 lines
5.8 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/db";
import { requireUserId } from "@/lib/auth-helpers";
import { boardPath, categoryAccessFilter } from "@/lib/access";
import { GroupColorSchema, GroupTitleSchema } from "@/lib/validation/group";
import type { GroupDTO } from "@/types/board";
export async function createGroup(
categoryId: string,
title: string,
color: string
): Promise<GroupDTO> {
const userId = await requireUserId();
const parsedTitle = GroupTitleSchema.parse(title);
const parsedColor = GroupColorSchema.parse(color);
const category = await prisma.category.findFirst({
where: { id: categoryId, ...categoryAccessFilter(userId) },
select: { id: true, projectId: true },
});
if (!category) throw new Error("Category not found");
const count = await prisma.group.count({ where: { categoryId } });
const group = await prisma.group.create({
data: { title: parsedTitle, color: parsedColor, order: count, categoryId },
});
revalidatePath(boardPath(category.projectId));
return {
id: group.id,
title: group.title,
color: group.color,
order: group.order,
noteContent: group.noteContent,
categoryId: group.categoryId,
todos: [],
};
}
export async function updateGroup(
groupId: string,
data: { title?: string; color?: string }
): Promise<void> {
const userId = await requireUserId();
const update: { title?: string; color?: string } = {};
if (data.title !== undefined) update.title = GroupTitleSchema.parse(data.title);
if (data.color !== undefined) update.color = GroupColorSchema.parse(data.color);
const group = await prisma.group.findFirst({
where: { id: groupId, category: categoryAccessFilter(userId) },
select: { category: { select: { projectId: true } } },
});
if (!group) throw new Error("Group not found");
await prisma.group.update({ where: { id: groupId }, data: update });
revalidatePath(boardPath(group.category.projectId));
}
export async function deleteGroup(groupId: string): Promise<void> {
const userId = await requireUserId();
const group = await prisma.group.findFirst({
where: { id: groupId, category: categoryAccessFilter(userId) },
select: { category: { select: { projectId: true } } },
});
if (!group) throw new Error("Group not found");
await prisma.group.delete({ where: { id: groupId } });
revalidatePath(boardPath(group.category.projectId));
}
/**
* Archives a group instead of deleting it -- the row (and its to-dos) is
* kept, just excluded from the board query going forward.
*/
export async function archiveGroup(groupId: string): Promise<void> {
const userId = await requireUserId();
const group = await prisma.group.findFirst({
where: { id: groupId, category: categoryAccessFilter(userId) },
select: { category: { select: { projectId: true } } },
});
if (!group) throw new Error("Group not found");
await prisma.group.update({ where: { id: groupId }, data: { archivedAt: new Date() } });
revalidatePath(boardPath(group.category.projectId));
}
/** Persists a new card order within a single lane. */
export async function reorderGroupsInCategory(
categoryId: string,
orderedGroupIds: string[]
): Promise<void> {
const userId = await requireUserId();
const category = await prisma.category.findFirst({
where: { id: categoryId, ...categoryAccessFilter(userId) },
include: { groups: { select: { id: true } } },
});
if (!category) throw new Error("Category not found");
const ownedIds = new Set(category.groups.map((g) => g.id));
if (
orderedGroupIds.length !== ownedIds.size ||
!orderedGroupIds.every((id) => ownedIds.has(id))
) {
throw new Error("Group list does not match this category's groups");
}
await prisma.$transaction(
orderedGroupIds.map((id, index) =>
prisma.group.update({ where: { id }, data: { order: index } })
)
);
revalidatePath(boardPath(category.projectId));
}
/**
* Moves a card to a different lane and persists the resulting order for
* both the source and destination lanes. Both lanes must be in the same
* board (Home, or the same Project) -- boards are isolated spaces, so a
* group can't be dragged from one into another.
*/
export async function moveGroupToCategory(
groupId: string,
targetCategoryId: string,
orderedGroupIdsInTargetLane: string[],
orderedGroupIdsInSourceLane: string[]
): Promise<void> {
const userId = await requireUserId();
const [group, targetCategory] = await Promise.all([
prisma.group.findFirst({
where: { id: groupId, category: categoryAccessFilter(userId) },
select: { id: true, categoryId: true, category: { select: { projectId: true } } },
}),
prisma.category.findFirst({
where: { id: targetCategoryId, ...categoryAccessFilter(userId) },
select: { id: true, projectId: true },
}),
]);
if (!group) throw new Error("Group not found");
if (!targetCategory) throw new Error("Target category not found");
if (group.category.projectId !== targetCategory.projectId) {
throw new Error("Can't move a group into a different board");
}
const targetIndex = orderedGroupIdsInTargetLane.indexOf(groupId);
if (targetIndex === -1) {
throw new Error("Moved group must be present in the target lane's order list");
}
await prisma.$transaction([
prisma.group.update({
where: { id: groupId },
data: { categoryId: targetCategoryId, order: targetIndex },
}),
...orderedGroupIdsInTargetLane
.map((id, index) => ({ id, index }))
.filter(({ id }) => id !== groupId)
.map(({ id, index }) => prisma.group.update({ where: { id }, data: { order: index } })),
...orderedGroupIdsInSourceLane.map((id, index) =>
prisma.group.update({ where: { id }, data: { order: index } })
),
]);
revalidatePath(boardPath(targetCategory.projectId));
}