feat: create and assign labels on trello import
This commit is contained in:
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
packages/db/migrations/0007_redundant_silver_fox.sql
Normal file
2
packages/db/migrations/0007_redundant_silver_fox.sql
Normal 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;
|
||||||
1651
packages/db/migrations/meta/0007_snapshot.json
Normal file
1651
packages/db/migrations/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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[],
|
||||||
|
|||||||
@@ -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`),
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user