Add 17 features: My Work view, card copy/numbering, calendar, bulk ops, custom fields, automations, webhooks, invites, import/export, and more
Features implemented across 6 batches: - Batch A: Fix ColorPicker gradient sync, tenant custom logo, unarchive cards, board backgrounds - Batch B: My Work view, card copy/duplicate, card numbering (BOARD-123) - Batch C: Due date reminders (cron), card watching with notifications - Batch D: Bulk card operations, column sorting, calendar view - Batch E: Invite by link, board import/export (Pounce + Trello JSON) - Batch F: Webhooks with HMAC signing, custom fields, automation rules engine Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import { withTenant } from '$lib/server/rls.js';
|
||||
import { requireAuth } from '$lib/server/guards.js';
|
||||
import { generatePositions } from '$lib/utils/fractional-index.js';
|
||||
|
||||
const MAX_CARDS = 500;
|
||||
|
||||
interface ImportColumn {
|
||||
title: string;
|
||||
cards: ImportCard[];
|
||||
}
|
||||
|
||||
interface ImportCard {
|
||||
title: string;
|
||||
description?: string;
|
||||
dueDate?: string | null;
|
||||
labels?: { name: string; color: string }[];
|
||||
checklists?: {
|
||||
title: string;
|
||||
items: { title: string; completed?: boolean }[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const tenant = locals.tenant;
|
||||
if (!tenant) throw error(400, 'Unknown tenant');
|
||||
requireAuth(locals.user);
|
||||
|
||||
const body = await request.json();
|
||||
const { data: importData, format } = body;
|
||||
|
||||
if (!importData) throw error(400, 'No import data provided');
|
||||
|
||||
let columns: ImportColumn[];
|
||||
let boardTitle: string;
|
||||
|
||||
if (format === 'trello') {
|
||||
// Trello JSON import
|
||||
boardTitle = importData.name || 'Imported Board';
|
||||
const listsById = new Map<string, string>();
|
||||
columns = (importData.lists || []).map((list: any) => {
|
||||
listsById.set(list.id, list.name);
|
||||
return { title: list.name, cards: [] };
|
||||
});
|
||||
|
||||
const trelloCards = importData.cards || [];
|
||||
if (trelloCards.length > MAX_CARDS) {
|
||||
throw error(400, `Too many cards (max ${MAX_CARDS})`);
|
||||
}
|
||||
|
||||
for (const tc of trelloCards) {
|
||||
if (tc.closed) continue;
|
||||
const colIdx = columns.findIndex((c: ImportColumn) => c.title === listsById.get(tc.idList));
|
||||
if (colIdx === -1) continue;
|
||||
|
||||
const card: ImportCard = {
|
||||
title: tc.name,
|
||||
description: tc.desc || undefined,
|
||||
dueDate: tc.due || null,
|
||||
labels: (tc.labels || []).map((l: any) => ({
|
||||
name: l.name || l.color,
|
||||
color: l.color ? `#${l.color}` : '#6b7280'
|
||||
})),
|
||||
checklists: []
|
||||
};
|
||||
|
||||
// Map Trello checklists
|
||||
if (importData.checklists) {
|
||||
const cardChecklists = importData.checklists.filter((cl: any) => cl.idCard === tc.id);
|
||||
card.checklists = cardChecklists.map((cl: any) => ({
|
||||
title: cl.name,
|
||||
items: (cl.checkItems || []).map((item: any) => ({
|
||||
title: item.name,
|
||||
completed: item.state === 'complete'
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
columns[colIdx].cards.push(card);
|
||||
}
|
||||
} else {
|
||||
// Pounce JSON import
|
||||
boardTitle = importData.title || 'Imported Board';
|
||||
columns = importData.columns || [];
|
||||
|
||||
const totalCards = columns.reduce((sum: number, col: ImportColumn) => sum + col.cards.length, 0);
|
||||
if (totalCards > MAX_CARDS) {
|
||||
throw error(400, `Too many cards (max ${MAX_CARDS})`);
|
||||
}
|
||||
}
|
||||
|
||||
const board = await withTenant(tenant.id, async (tx) => {
|
||||
const cardPrefix = boardTitle.replace(/[^a-zA-Z]/g, '').toUpperCase().slice(0, 4) || 'CARD';
|
||||
|
||||
// Create the board
|
||||
const newBoard = await tx.board.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
title: boardTitle,
|
||||
cardPrefix,
|
||||
members: {
|
||||
create: { userId: locals.user!.id, role: 'OWNER' }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get existing tags for this tenant
|
||||
const existingTags = await tx.tag.findMany({
|
||||
where: { tenantId: tenant.id }
|
||||
});
|
||||
const tagMap = new Map(existingTags.map((t) => [t.name.toLowerCase(), t]));
|
||||
|
||||
const colPositions = generatePositions(columns.length);
|
||||
let nextCardNum = 1;
|
||||
|
||||
for (let ci = 0; ci < columns.length; ci++) {
|
||||
const col = columns[ci];
|
||||
const newCol = await tx.column.create({
|
||||
data: {
|
||||
boardId: newBoard.id,
|
||||
title: col.title,
|
||||
position: colPositions[ci]
|
||||
}
|
||||
});
|
||||
|
||||
const cardPositions = generatePositions(col.cards.length);
|
||||
|
||||
for (let cri = 0; cri < col.cards.length; cri++) {
|
||||
const card = col.cards[cri];
|
||||
const newCard = await tx.card.create({
|
||||
data: {
|
||||
columnId: newCol.id,
|
||||
title: card.title,
|
||||
description: card.description,
|
||||
dueDate: card.dueDate ? new Date(card.dueDate) : null,
|
||||
position: cardPositions[cri],
|
||||
cardNumber: nextCardNum++
|
||||
}
|
||||
});
|
||||
|
||||
// Create labels
|
||||
if (card.labels && card.labels.length > 0) {
|
||||
for (const label of card.labels) {
|
||||
let tag = tagMap.get(label.name.toLowerCase());
|
||||
if (!tag) {
|
||||
tag = await tx.tag.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
name: label.name,
|
||||
color: label.color || '#6b7280'
|
||||
}
|
||||
});
|
||||
tagMap.set(label.name.toLowerCase(), tag);
|
||||
}
|
||||
await tx.cardLabel.create({
|
||||
data: { cardId: newCard.id, tagId: tag.id }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create checklists
|
||||
if (card.checklists && card.checklists.length > 0) {
|
||||
const clPositions = generatePositions(card.checklists.length);
|
||||
for (let cli = 0; cli < card.checklists.length; cli++) {
|
||||
const cl = card.checklists[cli];
|
||||
const newCl = await tx.checklist.create({
|
||||
data: {
|
||||
cardId: newCard.id,
|
||||
title: cl.title,
|
||||
position: clPositions[cli]
|
||||
}
|
||||
});
|
||||
|
||||
if (cl.items.length > 0) {
|
||||
const itemPositions = generatePositions(cl.items.length);
|
||||
await tx.checklistItem.createMany({
|
||||
data: cl.items.map((item, idx) => ({
|
||||
checklistId: newCl.id,
|
||||
title: item.title,
|
||||
completed: item.completed || false,
|
||||
position: itemPositions[idx]
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the board's nextCardNum
|
||||
await tx.board.update({
|
||||
where: { id: newBoard.id },
|
||||
data: { nextCardNum: nextCardNum }
|
||||
});
|
||||
|
||||
return newBoard;
|
||||
});
|
||||
|
||||
return json({ board: { id: board.id, title: board.title } }, { status: 201 });
|
||||
};
|
||||
Reference in New Issue
Block a user