feat: add tiptap markdown support, add checklist support from trello (#245)
* feat: add tiptap markdown support, add checklist support from trello * feat(api): implement bulk creation of checklists and items
This commit is contained in:
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
import * as boardRepo from "@kan/db/repository/board.repo";
|
||||
import * as cardRepo from "@kan/db/repository/card.repo";
|
||||
import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo";
|
||||
import * as checklistRepo from "@kan/db/repository/checklist.repo";
|
||||
import * as importRepo from "@kan/db/repository/import.repo";
|
||||
import * as integrationsRepo from "@kan/db/repository/integration.repo";
|
||||
import * as labelRepo from "@kan/db/repository/label.repo";
|
||||
@@ -22,6 +23,7 @@ export interface TrelloBoard {
|
||||
labels: TrelloLabel[];
|
||||
lists: TrelloList[];
|
||||
cards: TrelloCard[];
|
||||
checklists: TrelloChecklist[];
|
||||
}
|
||||
|
||||
interface TrelloLabel {
|
||||
@@ -34,12 +36,34 @@ interface TrelloList {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface TrelloChecklist {
|
||||
id: string;
|
||||
idCard: string;
|
||||
name: string;
|
||||
checkItems: TrelloCheckItem[];
|
||||
}
|
||||
|
||||
interface TrelloCheckItem {
|
||||
id: string;
|
||||
name: string;
|
||||
state: "complete" | "incomplete";
|
||||
pos: number;
|
||||
}
|
||||
|
||||
interface TrelloCard {
|
||||
id: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
idList: string;
|
||||
labels: TrelloLabel[];
|
||||
idChecklists: string[];
|
||||
checkItemStates: TrelloCheckItemState[];
|
||||
}
|
||||
|
||||
interface TrelloCheckItemState {
|
||||
idChecklist: string;
|
||||
idCheckItem: string;
|
||||
state: "complete" | "incomplete";
|
||||
}
|
||||
|
||||
export const importRouter = createTRPCRouter({
|
||||
@@ -170,7 +194,7 @@ export const importRouter = createTRPCRouter({
|
||||
|
||||
for (const boardId of input.boardIds) {
|
||||
const response = await fetch(
|
||||
`${urls.trello}/boards/${boardId}?key=${apiKey}&token=${integration.accessToken}&lists=open&cards=open&labels=all`,
|
||||
`${urls.trello}/boards/${boardId}?key=${apiKey}&token=${integration.accessToken}&lists=open&cards=open&labels=all&checklists=all&checkItemStates=all`,
|
||||
);
|
||||
const data = (await response.json()) as TrelloBoard;
|
||||
|
||||
@@ -194,6 +218,18 @@ export const importRouter = createTRPCRouter({
|
||||
sourceId: label.id,
|
||||
name: label.name,
|
||||
})),
|
||||
checklists: data.checklists
|
||||
.filter((checklist) => checklist.idCard === _card.id)
|
||||
.map((_checklist) => ({
|
||||
sourceId: _checklist.id,
|
||||
name: _checklist.name,
|
||||
items: _checklist.checkItems.map((_item) => ({
|
||||
sourceId: _item.id,
|
||||
title: _item.name,
|
||||
completed: _item.state === "complete",
|
||||
index: _item.pos,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
@@ -289,6 +325,97 @@ export const importRouter = createTRPCRouter({
|
||||
await cardActivityRepo.bulkCreate(ctx.db, activities);
|
||||
}
|
||||
|
||||
const checklistsToCreate: {
|
||||
cardId: number;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
sourceId: string;
|
||||
items: {
|
||||
sourceId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
index: number;
|
||||
}[];
|
||||
}[] = [];
|
||||
|
||||
for (const card of list.cards) {
|
||||
const _card = createdCards.find(
|
||||
(c) => c.sourceId === card.sourceId,
|
||||
);
|
||||
|
||||
if (!_card || !card.checklists.length) continue;
|
||||
|
||||
for (
|
||||
let checklistIndex = 0;
|
||||
checklistIndex < card.checklists.length;
|
||||
checklistIndex++
|
||||
) {
|
||||
const checklist = card.checklists[checklistIndex];
|
||||
if (!checklist) continue;
|
||||
|
||||
checklistsToCreate.push({
|
||||
cardId: _card.id,
|
||||
name: checklist.name,
|
||||
createdBy: userId,
|
||||
index: checklistIndex,
|
||||
sourceId: checklist.sourceId,
|
||||
items: checklist.items.map((item) => ({
|
||||
sourceId: item.sourceId,
|
||||
title: item.title,
|
||||
completed: item.completed,
|
||||
index: item.index,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (checklistsToCreate.length > 0) {
|
||||
const newChecklists = await checklistRepo.bulkCreate(
|
||||
ctx.db,
|
||||
checklistsToCreate.map((checklist) => ({
|
||||
cardId: checklist.cardId,
|
||||
name: checklist.name,
|
||||
createdBy: checklist.createdBy,
|
||||
index: checklist.index,
|
||||
})),
|
||||
);
|
||||
|
||||
const itemsToCreate: {
|
||||
checklistId: number;
|
||||
title: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
completed: boolean;
|
||||
}[] = [];
|
||||
|
||||
for (let i = 0; i < checklistsToCreate.length; i++) {
|
||||
const checklistData = checklistsToCreate[i];
|
||||
const newChecklist = newChecklists[i];
|
||||
|
||||
if (!newChecklist || !checklistData?.items.length) continue;
|
||||
|
||||
// NOTE: Sorting here to prevent checklist items being out of order
|
||||
const sortedItems = [...checklistData.items].sort(
|
||||
(a, b) => a.index - b.index,
|
||||
);
|
||||
|
||||
for (const item of sortedItems) {
|
||||
itemsToCreate.push({
|
||||
checklistId: newChecklist.id,
|
||||
title: item.title,
|
||||
createdBy: userId,
|
||||
index: item.index,
|
||||
completed: item.completed,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsToCreate.length > 0) {
|
||||
await checklistRepo.bulkCreateItems(ctx.db, itemsToCreate);
|
||||
}
|
||||
}
|
||||
|
||||
if (createdLabels.length && createdCards.length) {
|
||||
const cardLabelRelations: {
|
||||
cardId: number;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const createItem = async (
|
||||
checklistId: number;
|
||||
title: string;
|
||||
createdBy: string;
|
||||
completed?: boolean;
|
||||
},
|
||||
) => {
|
||||
return db.transaction(async (tx) => {
|
||||
@@ -65,7 +66,7 @@ export const createItem = async (
|
||||
createdBy: checklistItemInput.createdBy,
|
||||
checklistId: checklistItemInput.checklistId,
|
||||
index: lastItem ? lastItem.index + 1 : 0,
|
||||
completed: false,
|
||||
completed: checklistItemInput.completed ?? false,
|
||||
})
|
||||
.returning({
|
||||
id: checklistItems.id,
|
||||
@@ -214,3 +215,129 @@ export const updateChecklistById = async (
|
||||
.returning({ publicId: checklists.publicId, name: checklists.name });
|
||||
return result;
|
||||
};
|
||||
|
||||
export const bulkCreate = async (
|
||||
db: dbClient,
|
||||
checklistInput: {
|
||||
cardId: number;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
}[],
|
||||
) => {
|
||||
if (checklistInput.length === 0) return [];
|
||||
|
||||
return db.transaction(async (tx) => {
|
||||
const byCard = groupByKey(checklistInput, "cardId");
|
||||
|
||||
const allValuesToInsert: {
|
||||
publicId: string;
|
||||
cardId: number;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
}[] = [];
|
||||
|
||||
for (const [cardId, items] of byCard.entries()) {
|
||||
const last = await tx.query.checklists.findFirst({
|
||||
columns: { index: true },
|
||||
where: and(eq(checklists.cardId, cardId), isNull(checklists.deletedAt)),
|
||||
orderBy: [desc(checklists.index)],
|
||||
});
|
||||
|
||||
let nextIndex = last ? last.index + 1 : 0;
|
||||
|
||||
const sorted = [...items].sort((a, b) => a.index - b.index);
|
||||
|
||||
for (const item of sorted) {
|
||||
allValuesToInsert.push({
|
||||
publicId: generateUID(),
|
||||
...item,
|
||||
index: nextIndex++,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const inserted = await tx
|
||||
.insert(checklists)
|
||||
.values(allValuesToInsert)
|
||||
.returning({ id: checklists.id, publicId: checklists.publicId });
|
||||
|
||||
return inserted;
|
||||
});
|
||||
};
|
||||
|
||||
export const bulkCreateItems = async (
|
||||
db: dbClient,
|
||||
checklistItemInput: {
|
||||
checklistId: number;
|
||||
title: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
completed: boolean;
|
||||
}[],
|
||||
) => {
|
||||
if (checklistItemInput.length === 0) return [];
|
||||
|
||||
return db.transaction(async (tx) => {
|
||||
const byChecklist = groupByKey(checklistItemInput, "checklistId");
|
||||
|
||||
const allValuesToInsert: {
|
||||
publicId: string;
|
||||
checklistId: number;
|
||||
title: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
completed: boolean;
|
||||
}[] = [];
|
||||
|
||||
for (const [checklistId, items] of byChecklist.entries()) {
|
||||
const last = await tx.query.checklistItems.findFirst({
|
||||
columns: { index: true },
|
||||
where: and(
|
||||
eq(checklistItems.checklistId, checklistId),
|
||||
isNull(checklistItems.deletedAt),
|
||||
),
|
||||
orderBy: [desc(checklistItems.index)],
|
||||
});
|
||||
|
||||
let nextIndex = last ? last.index + 1 : 0;
|
||||
|
||||
const sorted = [...items].sort((a, b) => a.index - b.index);
|
||||
|
||||
for (const item of sorted) {
|
||||
allValuesToInsert.push({
|
||||
publicId: generateUID(),
|
||||
...item,
|
||||
index: nextIndex++,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const inserted = await tx
|
||||
.insert(checklistItems)
|
||||
.values(allValuesToInsert)
|
||||
.returning({
|
||||
id: checklistItems.id,
|
||||
publicId: checklistItems.publicId,
|
||||
title: checklistItems.title,
|
||||
completed: checklistItems.completed,
|
||||
});
|
||||
|
||||
return inserted;
|
||||
});
|
||||
};
|
||||
|
||||
const groupByKey = <T extends Record<string, unknown>>(
|
||||
items: T[],
|
||||
keyField: keyof T,
|
||||
): Map<number, T[]> => {
|
||||
const grouped = new Map<number, T[]>();
|
||||
for (const item of items) {
|
||||
const key = item[keyField] as number;
|
||||
const arr = grouped.get(key) ?? [];
|
||||
arr.push(item);
|
||||
grouped.set(key, arr);
|
||||
}
|
||||
return grouped;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user