feat: create and assign labels on trello import

This commit is contained in:
Henry
2025-01-29 18:00:44 +00:00
parent 34f8ad4def
commit 80067767ce
9 changed files with 1796 additions and 38 deletions

View File

@@ -5,9 +5,11 @@ import * as boardRepo from "@kan/db/repository/board.repo";
import * as cardRepo from "@kan/db/repository/card.repo"; import * as cardRepo from "@kan/db/repository/card.repo";
import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo"; import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo";
import * as importRepo from "@kan/db/repository/import.repo"; import * as importRepo from "@kan/db/repository/import.repo";
import * as labelRepo from "@kan/db/repository/label.repo";
import * as listRepo from "@kan/db/repository/list.repo"; import * as listRepo from "@kan/db/repository/list.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { generateSlug, generateUID } from "@kan/utils"; import { colours } from "@kan/shared/constants";
import { generateUID } from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
@@ -16,10 +18,16 @@ const TRELLO_API_URL = "https://api.trello.com/1";
interface TrelloBoard { interface TrelloBoard {
id: string; id: string;
name: string; name: string;
labels: TrelloLabel[];
lists: TrelloList[]; lists: TrelloList[];
cards: TrelloCard[]; cards: TrelloCard[];
} }
interface TrelloLabel {
id: string;
name: string;
}
interface TrelloList { interface TrelloList {
id: string; id: string;
name: string; name: string;
@@ -30,6 +38,7 @@ interface TrelloCard {
name: string; name: string;
desc: string; desc: string;
idList: string; idList: string;
labels: TrelloLabel[];
} }
interface MemberData { interface MemberData {
@@ -66,16 +75,12 @@ export const importRouter = createTRPCRouter({
const boardIds = member.idBoards; const boardIds = member.idBoards;
const fetchBoard = async (boardId: string) => { const fetchBoard = async (boardId: string) => {
try { const response = await fetch(
const response = await fetch( `${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}`,
`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}`, );
); const data = (await response.json()) as TrelloBoard;
const data = (await response.json()) as TrelloBoard;
return data; return data;
} catch (error) {
throw error;
}
}; };
const boards = []; const boards = [];
@@ -142,36 +147,40 @@ export const importRouter = createTRPCRouter({
for (const boardId of input.boardIds) { for (const boardId of input.boardIds) {
const response = await fetch( const response = await fetch(
`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}&lists=open&cards=open`, `${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}&lists=open&cards=open&labels=all`,
); );
const data = (await response.json()) as TrelloBoard; const data = (await response.json()) as TrelloBoard;
const formattedData = { const formattedData = {
name: data.name, name: data.name,
labels: data.labels
.map((label) => ({
sourceId: label.id,
name: label.name,
}))
.filter((_label) => !!_label.name),
lists: data.lists.map((list) => ({ lists: data.lists.map((list) => ({
name: list.name, name: list.name,
cards: data.cards cards: data.cards
.filter((card) => card.idList === list.id) .filter((card) => card.idList === list.id)
.map((_card) => ({ .map((_card) => ({
sourceId: _card.id,
name: _card.name, name: _card.name,
description: _card.desc, description: _card.desc,
labels: _card.labels.map((label) => ({
sourceId: label.id,
name: label.name,
})),
})), })),
})), })),
}; };
let slug = generateSlug(formattedData.name); const boardPublicId = generateUID();
const isSlugUnique = await boardRepo.isSlugUnique(ctx.db, {
slug,
workspaceId: workspace.id,
});
if (!isSlugUnique) slug = `${slug}-${generateUID()}`;
const newBoard = await boardRepo.create(ctx.db, { const newBoard = await boardRepo.create(ctx.db, {
publicId: generateUID(), publicId: boardPublicId,
name: formattedData.name, name: formattedData.name,
slug, slug: boardPublicId,
createdBy: userId, createdBy: userId,
importId: newImportId, importId: newImportId,
workspaceId: workspace.id, workspaceId: workspace.id,
@@ -185,6 +194,30 @@ export const importRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
}); });
let createdLabels: { id: number; sourceId: string }[] = [];
let createdCards: { id: number; sourceId: string }[] = [];
if (formattedData.labels.length) {
const labelsInsert = formattedData.labels.map((label, index) => ({
publicId: generateUID(),
name: label.name,
colourCode: colours[index % colours.length]?.code ?? "#0d9488",
createdBy: userId,
boardId: newBoardId,
importId: newImportId,
}));
const newLabels = await labelRepo.bulkCreate(ctx.db, labelsInsert);
if (newLabels?.length)
createdLabels = newLabels
.map((label, index) => ({
id: label.id,
sourceId: formattedData.labels[index]?.sourceId ?? "",
}))
.filter((label) => !!label.sourceId);
}
let listIndex = 0; let listIndex = 0;
for (const list of formattedData.lists) { for (const list of formattedData.lists) {
@@ -209,30 +242,70 @@ export const importRouter = createTRPCRouter({
importId: newImportId, importId: newImportId,
})); }));
const createdCards = await cardRepo.bulkCreate( const newCards = await cardRepo.bulkCreate(ctx.db, cardsInsert);
ctx.db,
cardsInsert,
);
if (!createdCards?.length) if (!newCards?.length)
throw new TRPCError({ throw new TRPCError({
message: "Failed to create new cards", message: "Failed to create new cards",
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
}); });
const activities = createdCards.map((card) => ({ createdCards = createdCards.concat(
newCards
.map((card, index) => ({
id: card.id,
sourceId: list.cards[index]?.sourceId ?? "",
}))
.filter((card) => !!card.sourceId),
);
const activities = newCards.map((card) => ({
type: "card.created" as const, type: "card.created" as const,
cardId: card.id, cardId: card.id,
createdBy: userId, createdBy: userId,
})); }));
if (createdCards.length > 0) { if (newCards.length > 0) {
await cardActivityRepo.bulkCreate(ctx.db, activities); await cardActivityRepo.bulkCreate(ctx.db, activities);
} }
if (createdLabels.length && createdCards.length) {
const cardLabelRelations: {
cardId: number;
labelId: number;
}[] = [];
for (const card of list.cards) {
const _card = createdCards.find(
(c) => c.sourceId === card.sourceId,
);
for (const label of card.labels) {
const _label = createdLabels.find(
(l) => l.sourceId === label.sourceId,
);
if (_card && _label) {
cardLabelRelations.push({
cardId: _card.id,
labelId: _label.id,
});
}
}
}
if (cardLabelRelations.length) {
await cardRepo.bulkCreateCardLabelRelationship(
ctx.db,
cardLabelRelations,
);
}
}
} }
listIndex++; listIndex++;
} }
boardsCreated++; boardsCreated++;
} }

View File

@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS "unique_slug_per_workspace";--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "unique_slug_per_workspace" ON "board" USING btree ("workspaceId","slug") WHERE "board"."deletedAt" IS NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,13 @@
"when": 1738149668712, "when": 1738149668712,
"tag": "0006_spicy_mach_iv", "tag": "0006_spicy_mach_iv",
"breakpoints": true "breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1738168854269,
"tag": "0007_redundant_silver_fox",
"breakpoints": true
} }
] ]
} }

View File

@@ -284,8 +284,7 @@ export const isSlugUnique = async (
.eq("slug", args.slug) .eq("slug", args.slug)
.eq("workspaceId", args.workspaceId) .eq("workspaceId", args.workspaceId)
.is("deletedAt", null) .is("deletedAt", null)
.limit(1) .limit(1);
.single();
return !data; return data?.length === 0;
}; };

View File

@@ -161,6 +161,18 @@ export const createCardLabelRelationship = async (
return data; return data;
}; };
export const bulkCreateCardLabelRelationship = async (
db: SupabaseClient<Database>,
cardLabelRelationshipInput: { cardId: number; labelId: number }[],
) => {
const { data } = await db
.from("_card_labels")
.insert(cardLabelRelationshipInput)
.select();
return data;
};
export const getCardMemberRelationship = async ( export const getCardMemberRelationship = async (
db: SupabaseClient<Database>, db: SupabaseClient<Database>,
args: { cardId: number; memberId: number }, args: { cardId: number; memberId: number },

View File

@@ -1,7 +1,7 @@
import type { SupabaseClient } from "@supabase/supabase-js"; import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "@kan/db/types/database.types"; import type { Database } from "@kan/db/types/database.types";
import { generateUID } from "@kan/utils"; import { generateUID } from "@kan/shared/utils";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: SupabaseClient<Database>,

View File

@@ -1,7 +1,7 @@
import type { SupabaseClient } from "@supabase/supabase-js"; import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "@kan/db/types/database.types"; import type { Database } from "@kan/db/types/database.types";
import { generateUID } from "@kan/utils"; import { generateUID } from "@kan/shared/utils";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: SupabaseClient<Database>,
@@ -35,6 +35,21 @@ export const create = async (
return data; return data;
}; };
export const bulkCreate = async (
db: SupabaseClient<Database>,
labels: {
publicId: string;
name: string;
colourCode: string;
boardId: number;
createdBy: string;
}[],
) => {
const { data } = await db.from("label").insert(labels).select(`id`);
return data;
};
export const getAllByPublicIds = async ( export const getAllByPublicIds = async (
db: SupabaseClient<Database>, db: SupabaseClient<Database>,
labelPublicIds: string[], labelPublicIds: string[],

View File

@@ -1,4 +1,4 @@
import { relations } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import { import {
bigint, bigint,
bigserial, bigserial,
@@ -78,10 +78,9 @@ export const boards = pgTable(
}, },
(table) => ({ (table) => ({
visibilityIndex: index("board_visibility_idx").on(table.visibility), visibilityIndex: index("board_visibility_idx").on(table.visibility),
uniqueSlugPerWorkspace: uniqueIndex("unique_slug_per_workspace").on( uniqueSlugPerWorkspace: uniqueIndex("unique_slug_per_workspace")
table.workspaceId, .on(table.workspaceId, table.slug)
table.slug, .where(sql`${table.deletedAt} IS NULL`),
),
}), }),
); );