add import/export for JSON, CSV, and Trello formats
This commit is contained in:
230
src/lib/import-export.ts
Normal file
230
src/lib/import-export.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user