feat: add template pages

This commit is contained in:
Henry
2025-10-08 21:29:24 +01:00
parent 734e0bbfc5
commit 18e85ef6be
12 changed files with 472 additions and 226 deletions

View File

@@ -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;
});
};