add import/export for JSON, CSV, and Trello formats

This commit is contained in:
2026-02-15 19:14:06 +02:00
parent 940a020d72
commit 37627688b8
3 changed files with 370 additions and 8 deletions

230
src/lib/import-export.ts Normal file
View File

@@ -0,0 +1,230 @@
import { ulid } from "ulid";
import { boardSchema } from "@/lib/schemas";
import type { Board, Card, Column, Label, ChecklistItem } from "@/types/board";
// ---------------------------------------------------------------------------
// Export: JSON
// ---------------------------------------------------------------------------
export function exportBoardAsJson(board: Board): string {
return JSON.stringify(board, null, 2);
}
// ---------------------------------------------------------------------------
// Export: CSV
// ---------------------------------------------------------------------------
function escapeCsv(value: string): string {
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}
export function exportBoardAsCsv(board: Board): string {
const headers = [
"board",
"column",
"title",
"description",
"labels",
"dueDate",
"checklistTotal",
"checklistDone",
"createdAt",
"updatedAt",
];
const rows: string[] = [headers.join(",")];
for (const column of board.columns) {
for (const cardId of column.cardIds) {
const card = board.cards[cardId];
if (!card) continue;
const labelNames = card.labels
.map((lid) => board.labels.find((l) => l.id === lid)?.name ?? lid)
.join(";");
const checklistTotal = card.checklist.length;
const checklistDone = card.checklist.filter((item) => item.checked).length;
const row = [
escapeCsv(board.title),
escapeCsv(column.title),
escapeCsv(card.title),
escapeCsv(card.description),
escapeCsv(labelNames),
card.dueDate ?? "",
String(checklistTotal),
String(checklistDone),
card.createdAt,
card.updatedAt,
];
rows.push(row.join(","));
}
}
return rows.join("\n");
}
// ---------------------------------------------------------------------------
// Import: JSON (OpenPylon format)
// ---------------------------------------------------------------------------
export function importBoardFromJson(jsonString: string): Board {
const data = JSON.parse(jsonString);
const board = boardSchema.parse(data) as Board;
return board;
}
// ---------------------------------------------------------------------------
// Import: Trello JSON
// ---------------------------------------------------------------------------
interface TrelloCard {
name: string;
desc?: string;
due?: string | null;
labels?: { name: string; color: string }[];
idList: string;
closed?: boolean;
checklists?: {
name: string;
checkItems: { name: string; state: "complete" | "incomplete" }[];
}[];
}
interface TrelloList {
id: string;
name: string;
closed?: boolean;
}
interface TrelloBoard {
name: string;
lists?: TrelloList[];
cards?: TrelloCard[];
labels?: { name: string; color: string }[];
}
const TRELLO_COLOR_MAP: Record<string, string> = {
green: "#22c55e",
yellow: "#eab308",
orange: "#f97316",
red: "#ef4444",
purple: "#8b5cf6",
blue: "#3b82f6",
sky: "#0ea5e9",
lime: "#84cc16",
pink: "#ec4899",
black: "#1e293b",
};
export function importFromTrelloJson(jsonString: string): Board {
const data = JSON.parse(jsonString) as TrelloBoard;
const ts = new Date().toISOString();
const boardId = ulid();
// Map Trello labels
const labels: Label[] = [];
const trelloLabelMap = new Map<string, string>(); // trello label key -> our label id
if (data.labels) {
for (const tLabel of data.labels) {
if (!tLabel.name && !tLabel.color) continue;
const id = ulid();
const color = TRELLO_COLOR_MAP[tLabel.color] ?? "#6366f1";
labels.push({ id, name: tLabel.name || tLabel.color, color });
trelloLabelMap.set(`${tLabel.name}-${tLabel.color}`, id);
}
}
// Map Trello lists -> columns, filtering out archived lists
const openLists = (data.lists ?? []).filter((l) => !l.closed);
const listIdToColumnId = new Map<string, string>();
const columns: Column[] = openLists.map((list) => {
const colId = ulid();
listIdToColumnId.set(list.id, colId);
return {
id: colId,
title: list.name,
cardIds: [],
width: "standard" as const,
};
});
// Map Trello cards
const cards: Record<string, Card> = {};
for (const tCard of data.cards ?? []) {
if (tCard.closed) continue;
const columnId = listIdToColumnId.get(tCard.idList);
if (!columnId) continue;
const cardId = ulid();
// Map labels
const cardLabelIds: string[] = [];
if (tCard.labels) {
for (const tl of tCard.labels) {
const key = `${tl.name}-${tl.color}`;
const existingId = trelloLabelMap.get(key);
if (existingId) {
cardLabelIds.push(existingId);
}
}
}
// Map checklists
const checklist: ChecklistItem[] = [];
if (tCard.checklists) {
for (const cl of tCard.checklists) {
for (const item of cl.checkItems) {
checklist.push({
id: ulid(),
text: item.name,
checked: item.state === "complete",
});
}
}
}
const card: Card = {
id: cardId,
title: tCard.name,
description: tCard.desc ?? "",
labels: cardLabelIds,
checklist,
dueDate: tCard.due ?? null,
attachments: [],
createdAt: ts,
updatedAt: ts,
};
cards[cardId] = card;
// Add card to column
const col = columns.find((c) => c.id === columnId);
if (col) {
col.cardIds.push(cardId);
}
}
const board: Board = {
id: boardId,
title: data.name || "Imported Board",
color: "#6366f1",
createdAt: ts,
updatedAt: ts,
columns,
cards,
labels,
settings: { attachmentMode: "link" },
};
return board;
}