feat: add template pages
This commit is contained in:
@@ -14,51 +14,6 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { assertUserInWorkspace } from "../utils/auth";
|
||||
|
||||
export const boardRouter = createTRPCRouter({
|
||||
templates: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: "GET",
|
||||
path: "/workspaces/{workspacePublicId}/templates",
|
||||
summary: "Get templates",
|
||||
description: "Retrieves all templates for a given workspace",
|
||||
tags: ["Boards"],
|
||||
protect: true,
|
||||
},
|
||||
})
|
||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||
.output(
|
||||
z.custom<
|
||||
Awaited<ReturnType<typeof boardRepo.getTemplatesByWorkspaceId>>
|
||||
>(),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId)
|
||||
throw new TRPCError({
|
||||
message: `User not authenticated`,
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
|
||||
const workspace = await workspaceRepo.getByPublicId(
|
||||
ctx.db,
|
||||
input.workspacePublicId,
|
||||
);
|
||||
|
||||
if (!workspace)
|
||||
throw new TRPCError({
|
||||
message: `Workspace with public ID ${input.workspacePublicId} not found`,
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
|
||||
await assertUserInWorkspace(ctx.db, userId, workspace.id);
|
||||
|
||||
const result = boardRepo.getAllByWorkspaceId(ctx.db, workspace.id, {
|
||||
type: "template",
|
||||
});
|
||||
|
||||
return result;
|
||||
}),
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
@@ -70,7 +25,12 @@ export const boardRouter = createTRPCRouter({
|
||||
protect: true,
|
||||
},
|
||||
})
|
||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||
.input(
|
||||
z.object({
|
||||
workspacePublicId: z.string().min(12),
|
||||
type: z.enum(["regular", "template"]).optional(),
|
||||
}),
|
||||
)
|
||||
.output(
|
||||
z.custom<Awaited<ReturnType<typeof boardRepo.getAllByWorkspaceId>>>(),
|
||||
)
|
||||
@@ -96,7 +56,9 @@ export const boardRouter = createTRPCRouter({
|
||||
|
||||
await assertUserInWorkspace(ctx.db, userId, workspace.id);
|
||||
|
||||
const result = boardRepo.getAllByWorkspaceId(ctx.db, workspace.id);
|
||||
const result = boardRepo.getAllByWorkspaceId(ctx.db, workspace.id, {
|
||||
type: input.type,
|
||||
});
|
||||
|
||||
return result;
|
||||
}),
|
||||
@@ -116,6 +78,7 @@ export const boardRouter = createTRPCRouter({
|
||||
boardPublicId: z.string().min(12),
|
||||
members: z.array(z.string().min(12)).optional(),
|
||||
labels: z.array(z.string().min(12)).optional(),
|
||||
type: z.enum(["regular", "template"]).optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.getByPublicId>>>())
|
||||
@@ -147,6 +110,7 @@ export const boardRouter = createTRPCRouter({
|
||||
{
|
||||
members: input.members ?? [],
|
||||
labels: input.labels ?? [],
|
||||
type: input.type,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -222,6 +186,7 @@ export const boardRouter = createTRPCRouter({
|
||||
workspacePublicId: z.string().min(12),
|
||||
lists: z.array(z.string().min(1)),
|
||||
labels: z.array(z.string().min(1)),
|
||||
type: z.enum(["regular", "template"]).optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.create>>>())
|
||||
@@ -262,6 +227,7 @@ export const boardRouter = createTRPCRouter({
|
||||
name: input.name,
|
||||
createdBy: userId,
|
||||
workspaceId: workspace.id,
|
||||
type: input.type,
|
||||
});
|
||||
|
||||
if (!result)
|
||||
|
||||
@@ -15,13 +15,21 @@ import {
|
||||
} from "@kan/db/schema";
|
||||
import { generateUID } from "@kan/shared/utils";
|
||||
|
||||
export const getAllByWorkspaceId = (db: dbClient, workspaceId: number) => {
|
||||
export const getAllByWorkspaceId = (
|
||||
db: dbClient,
|
||||
workspaceId: number,
|
||||
opts?: { type?: "regular" | "template" },
|
||||
) => {
|
||||
return db.query.boards.findMany({
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
where: and(eq(boards.workspaceId, workspaceId), isNull(boards.deletedAt)),
|
||||
where: and(
|
||||
eq(boards.workspaceId, workspaceId),
|
||||
isNull(boards.deletedAt),
|
||||
opts?.type ? eq(boards.type, opts.type) : undefined,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -42,6 +50,7 @@ export const getByPublicId = async (
|
||||
filters: {
|
||||
members: string[];
|
||||
labels: string[];
|
||||
type: "regular" | "template" | undefined;
|
||||
},
|
||||
) => {
|
||||
let cardIds: string[] = [];
|
||||
@@ -199,7 +208,11 @@ export const getByPublicId = async (
|
||||
orderBy: [asc(lists.index)],
|
||||
},
|
||||
},
|
||||
where: and(eq(boards.publicId, boardPublicId), isNull(boards.deletedAt)),
|
||||
where: and(
|
||||
eq(boards.publicId, boardPublicId),
|
||||
isNull(boards.deletedAt),
|
||||
eq(boards.type, filters.type ?? "regular"),
|
||||
),
|
||||
});
|
||||
|
||||
if (!board) return null;
|
||||
@@ -412,6 +425,8 @@ export const create = async (
|
||||
workspaceId: number;
|
||||
importId?: number;
|
||||
slug: string;
|
||||
type?: "regular" | "template";
|
||||
sourceBoardId?: number;
|
||||
},
|
||||
) => {
|
||||
const [result] = await db
|
||||
@@ -423,6 +438,8 @@ export const create = async (
|
||||
workspaceId: boardInput.workspaceId,
|
||||
importId: boardInput.importId,
|
||||
slug: boardInput.slug,
|
||||
type: boardInput.type ?? "regular",
|
||||
sourceBoardId: boardInput.sourceBoardId,
|
||||
})
|
||||
.returning({
|
||||
id: boards.id,
|
||||
@@ -542,3 +559,186 @@ export const isBoardSlugAvailable = async (
|
||||
|
||||
return result === undefined;
|
||||
};
|
||||
|
||||
// Create a new board (regular/template) from a full board snapshot
|
||||
export const createFromSnapshot = async (
|
||||
db: dbClient,
|
||||
args: {
|
||||
source: {
|
||||
name: string;
|
||||
labels: { publicId: string; name: string; colourCode: string | null }[];
|
||||
lists: {
|
||||
name: string;
|
||||
index: number;
|
||||
cards: {
|
||||
title: string;
|
||||
description: string | null;
|
||||
index: number;
|
||||
labels: {
|
||||
publicId: string;
|
||||
name: string;
|
||||
colourCode: string | null;
|
||||
}[];
|
||||
checklists?: {
|
||||
publicId: string;
|
||||
name: string;
|
||||
index: number;
|
||||
items: {
|
||||
publicId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
index: number;
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
workspaceId: number;
|
||||
createdBy: string;
|
||||
slug: string;
|
||||
name?: string;
|
||||
type: "regular" | "template";
|
||||
sourceBoardId?: number;
|
||||
},
|
||||
) => {
|
||||
return db.transaction(async (tx) => {
|
||||
const [newBoard] = await tx
|
||||
.insert(boards)
|
||||
.values({
|
||||
publicId: generateUID(),
|
||||
name: args.name ?? args.source.name,
|
||||
slug: args.slug,
|
||||
createdBy: args.createdBy,
|
||||
workspaceId: args.workspaceId,
|
||||
type: args.type,
|
||||
sourceBoardId: args.sourceBoardId,
|
||||
})
|
||||
.returning({
|
||||
id: boards.id,
|
||||
publicId: boards.publicId,
|
||||
name: boards.name,
|
||||
});
|
||||
|
||||
if (!newBoard) throw new Error("Failed to create board");
|
||||
|
||||
// Labels
|
||||
const srcLabels = args.source.labels;
|
||||
const labelMap = new Map<string, number>();
|
||||
|
||||
if (srcLabels.length) {
|
||||
const inserted = await tx
|
||||
.insert(labels)
|
||||
.values(
|
||||
srcLabels.map((l) => ({
|
||||
publicId: generateUID(),
|
||||
name: l.name,
|
||||
colourCode: l.colourCode ?? null,
|
||||
createdBy: args.createdBy,
|
||||
boardId: newBoard.id,
|
||||
})),
|
||||
)
|
||||
.returning({ id: labels.id });
|
||||
|
||||
for (let i = 0; i < srcLabels.length; i++) {
|
||||
const src = srcLabels[i];
|
||||
|
||||
if (!src) throw new Error("Source label not found");
|
||||
|
||||
const created = inserted[i];
|
||||
if (created) labelMap.set(src.publicId, created.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Lists
|
||||
const listIndexToId = new Map<number, number>();
|
||||
const srcLists = [...args.source.lists].sort((a, b) => a.index - b.index);
|
||||
if (srcLists.length) {
|
||||
const insertedLists = await tx
|
||||
.insert(lists)
|
||||
.values(
|
||||
srcLists.map((list) => ({
|
||||
publicId: generateUID(),
|
||||
name: list.name,
|
||||
createdBy: args.createdBy,
|
||||
boardId: newBoard.id,
|
||||
index: list.index,
|
||||
})),
|
||||
)
|
||||
.returning({ id: lists.id, index: lists.index });
|
||||
|
||||
for (const list of insertedLists) listIndexToId.set(list.index, list.id);
|
||||
}
|
||||
|
||||
// Cards, card-labels, checklists
|
||||
for (const list of srcLists) {
|
||||
const newListId = listIndexToId.get(list.index);
|
||||
if (!newListId) continue;
|
||||
const sortedCards = [...list.cards].sort((a, b) => a.index - b.index);
|
||||
|
||||
for (const card of sortedCards) {
|
||||
const [createdCard] = await tx
|
||||
.insert(cards)
|
||||
.values({
|
||||
publicId: generateUID(),
|
||||
title: card.title,
|
||||
description: card.description ?? "",
|
||||
createdBy: args.createdBy,
|
||||
listId: newListId,
|
||||
index: card.index,
|
||||
})
|
||||
.returning({ id: cards.id });
|
||||
|
||||
if (!createdCard) throw new Error("Failed to create card");
|
||||
|
||||
if (card.labels.length) {
|
||||
const cardLabels: { cardId: number; labelId: number }[] = [];
|
||||
for (const label of card.labels) {
|
||||
const newLabelId = labelMap.get(label.publicId);
|
||||
if (newLabelId)
|
||||
cardLabels.push({ cardId: createdCard.id, labelId: newLabelId });
|
||||
}
|
||||
if (cardLabels.length)
|
||||
await tx.insert(cardsToLabels).values(cardLabels);
|
||||
}
|
||||
|
||||
if (card.checklists?.length) {
|
||||
const sortedChecklists = [...card.checklists].sort(
|
||||
(a, b) => a.index - b.index,
|
||||
);
|
||||
for (const checklist of sortedChecklists) {
|
||||
const [createdChecklist] = await tx
|
||||
.insert(checklists)
|
||||
.values({
|
||||
publicId: generateUID(),
|
||||
name: checklist.name,
|
||||
createdBy: args.createdBy,
|
||||
cardId: createdCard.id,
|
||||
index: checklist.index,
|
||||
})
|
||||
.returning({ id: checklists.id });
|
||||
|
||||
if (!createdChecklist) continue;
|
||||
|
||||
if (checklist.items.length) {
|
||||
const itemValues = [...checklist.items]
|
||||
.sort((a, b) => a.index - b.index)
|
||||
.map((checklistItem) => ({
|
||||
publicId: generateUID(),
|
||||
title: checklistItem.title,
|
||||
createdBy: args.createdBy,
|
||||
checklistId: createdChecklist.id,
|
||||
index: checklistItem.index,
|
||||
completed: !!checklistItem.completed,
|
||||
}));
|
||||
|
||||
if (itemValues.length)
|
||||
await tx.insert(checklistItems).values(itemValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newBoard;
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user