* init: schema migration, isArchived added to board table. refactor: board repo for isArchived filtering * init: archived, unarchived API procedures. refactor: all query * fix: migration error * feat: add tabbed navigation for boards view * Implemented a Listbox for mobile and a tabbed navigation for desktop to switch between "Boards" and "Archived" views. * Introduced state management for active tab selection. * Updated UI components to reflect the new navigation structure. * init: frontend/boards lists & tabs * chore: fixed font styling and spacing * init:boardDropdown / boardView. * chore:added translations * Remove .cursor plan file from repo * fix:build erros * revert: remove locales changes * fix:reverted changes under locales, replaced the archive and unarchive endpoints. Reorder migrations * fix:migration issue * fix: update journal.json --------- Co-authored-by: Henry <henry_ball@hotmail.co.uk>
995 lines
25 KiB
TypeScript
995 lines
25 KiB
TypeScript
import {
|
|
and,
|
|
asc,
|
|
count,
|
|
desc,
|
|
eq,
|
|
gte,
|
|
inArray,
|
|
isNotNull,
|
|
isNull,
|
|
lt,
|
|
or,
|
|
} from "drizzle-orm";
|
|
|
|
import type { dbClient } from "@kan/db/client";
|
|
import type { BoardVisibilityStatus } from "@kan/db/schema";
|
|
import {
|
|
boards,
|
|
cardActivities,
|
|
cardAttachments,
|
|
cards,
|
|
cardsToLabels,
|
|
cardToWorkspaceMembers,
|
|
checklistItems,
|
|
checklists,
|
|
comments,
|
|
labels,
|
|
lists,
|
|
userBoardFavorites,
|
|
workspaceMembers,
|
|
} from "@kan/db/schema";
|
|
import { generateUID } from "@kan/shared/utils";
|
|
|
|
export const getCount = async (db: dbClient) => {
|
|
const result = await db
|
|
.select({ count: count() })
|
|
.from(boards)
|
|
.where(isNull(boards.deletedAt));
|
|
|
|
return result[0]?.count ?? 0;
|
|
};
|
|
|
|
export const getAllByWorkspaceId = async (
|
|
db: dbClient,
|
|
workspaceId: number,
|
|
userId: string,
|
|
opts?: { type?: "regular" | "template"; archived?: boolean },
|
|
) => {
|
|
const boardsData = await db.query.boards.findMany({
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
},
|
|
with: {
|
|
userFavorites: {
|
|
where: eq(userBoardFavorites.userId, userId),
|
|
columns: {
|
|
userId: true,
|
|
},
|
|
},
|
|
lists: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
index: true,
|
|
},
|
|
orderBy: [asc(lists.index)],
|
|
},
|
|
labels: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
colourCode: true,
|
|
},
|
|
},
|
|
},
|
|
where: and(
|
|
eq(boards.workspaceId, workspaceId),
|
|
isNull(boards.deletedAt),
|
|
opts?.type ? eq(boards.type, opts.type) : undefined,
|
|
opts?.archived !== undefined ? eq(boards.isArchived, opts.archived) : undefined,
|
|
),
|
|
});
|
|
|
|
// Transform and sort: favorites first, then alphabetically
|
|
return boardsData
|
|
.map((board) => ({
|
|
...board,
|
|
favorite: board.userFavorites.length > 0,
|
|
userFavorites: undefined,
|
|
}))
|
|
.sort((a, b) => {
|
|
// Sort favorites first
|
|
if (a.favorite && !b.favorite) return -1;
|
|
if (!a.favorite && b.favorite) return 1;
|
|
// Then alphabetically by name
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
};
|
|
|
|
export const getIdByPublicId = async (db: dbClient, boardPublicId: string) => {
|
|
const board = await db.query.boards.findFirst({
|
|
columns: {
|
|
id: true,
|
|
type: true,
|
|
isArchived: true,
|
|
},
|
|
where: eq(boards.publicId, boardPublicId),
|
|
});
|
|
|
|
return board;
|
|
};
|
|
|
|
interface DueDateFilter {
|
|
startDate?: Date;
|
|
endDate?: Date;
|
|
hasNoDueDate?: boolean;
|
|
}
|
|
|
|
const buildDueDateWhere = (filters: DueDateFilter[]) => {
|
|
if (!filters.length) return undefined;
|
|
|
|
const clauses = filters
|
|
.map((filter) => {
|
|
const conditions: ReturnType<typeof and>[] = [];
|
|
|
|
if (filter.hasNoDueDate) {
|
|
conditions.push(isNull(cards.dueDate));
|
|
} else {
|
|
conditions.push(isNotNull(cards.dueDate));
|
|
|
|
if (filter.startDate)
|
|
conditions.push(gte(cards.dueDate, filter.startDate));
|
|
|
|
if (filter.endDate) conditions.push(lt(cards.dueDate, filter.endDate));
|
|
}
|
|
|
|
return conditions.length > 0 ? and(...conditions) : undefined;
|
|
})
|
|
.filter((clause): clause is NonNullable<typeof clause> => !!clause);
|
|
|
|
if (!clauses.length) return undefined;
|
|
|
|
return or(...clauses);
|
|
};
|
|
|
|
export const getByPublicId = async (
|
|
db: dbClient,
|
|
boardPublicId: string,
|
|
userId: string,
|
|
filters: {
|
|
members: string[];
|
|
labels: string[];
|
|
lists: string[];
|
|
dueDate: DueDateFilter[];
|
|
type: "regular" | "template" | undefined;
|
|
},
|
|
) => {
|
|
let cardIds: string[] = [];
|
|
|
|
if (filters.labels.length > 0 || filters.members.length > 0) {
|
|
const filteredCards = await db
|
|
.select({
|
|
publicId: cards.publicId,
|
|
})
|
|
.from(cards)
|
|
.leftJoin(cardsToLabels, eq(cards.id, cardsToLabels.cardId))
|
|
.leftJoin(labels, eq(cardsToLabels.labelId, labels.id))
|
|
.leftJoin(
|
|
cardToWorkspaceMembers,
|
|
eq(cards.id, cardToWorkspaceMembers.cardId),
|
|
)
|
|
.leftJoin(
|
|
workspaceMembers,
|
|
eq(cardToWorkspaceMembers.workspaceMemberId, workspaceMembers.id),
|
|
)
|
|
.where(
|
|
and(
|
|
isNull(cards.deletedAt),
|
|
or(
|
|
filters.labels.length > 0
|
|
? inArray(labels.publicId, filters.labels)
|
|
: undefined,
|
|
filters.members.length > 0
|
|
? inArray(workspaceMembers.publicId, filters.members)
|
|
: undefined,
|
|
),
|
|
),
|
|
);
|
|
|
|
cardIds = filteredCards.map((card) => card.publicId);
|
|
}
|
|
|
|
const board = await db.query.boards.findFirst({
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
slug: true,
|
|
visibility: true,
|
|
isArchived: true,
|
|
},
|
|
with: {
|
|
userFavorites: {
|
|
where: eq(userBoardFavorites.userId, userId),
|
|
columns: {
|
|
userId: true,
|
|
},
|
|
},
|
|
workspace: {
|
|
columns: {
|
|
publicId: true,
|
|
},
|
|
with: {
|
|
members: {
|
|
columns: {
|
|
publicId: true,
|
|
email: true,
|
|
status: true,
|
|
},
|
|
with: {
|
|
user: {
|
|
columns: {
|
|
name: true,
|
|
email: true,
|
|
image: true,
|
|
},
|
|
},
|
|
},
|
|
where: isNull(workspaceMembers.deletedAt),
|
|
},
|
|
},
|
|
},
|
|
labels: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
colourCode: true,
|
|
},
|
|
where: isNull(labels.deletedAt),
|
|
},
|
|
lists: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
boardId: true,
|
|
index: true,
|
|
},
|
|
with: {
|
|
cards: {
|
|
columns: {
|
|
publicId: true,
|
|
title: true,
|
|
description: true,
|
|
listId: true,
|
|
index: true,
|
|
dueDate: true,
|
|
},
|
|
with: {
|
|
labels: {
|
|
with: {
|
|
label: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
colourCode: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
members: {
|
|
with: {
|
|
member: {
|
|
columns: {
|
|
publicId: true,
|
|
email: true,
|
|
deletedAt: true,
|
|
},
|
|
with: {
|
|
user: {
|
|
columns: {
|
|
name: true,
|
|
email: true,
|
|
image: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
attachments: {
|
|
columns: {
|
|
publicId: true,
|
|
},
|
|
where: isNull(cardAttachments.deletedAt),
|
|
orderBy: asc(cardAttachments.createdAt),
|
|
},
|
|
checklists: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
index: true,
|
|
},
|
|
where: isNull(checklists.deletedAt),
|
|
orderBy: asc(checklists.index),
|
|
with: {
|
|
items: {
|
|
columns: {
|
|
publicId: true,
|
|
title: true,
|
|
completed: true,
|
|
index: true,
|
|
},
|
|
where: isNull(checklistItems.deletedAt),
|
|
orderBy: asc(checklistItems.index),
|
|
},
|
|
},
|
|
},
|
|
comments: {
|
|
columns: {
|
|
publicId: true,
|
|
},
|
|
where: isNull(comments.deletedAt),
|
|
limit: 1,
|
|
},
|
|
},
|
|
where: and(
|
|
cardIds.length > 0 ? inArray(cards.publicId, cardIds) : undefined,
|
|
isNull(cards.deletedAt),
|
|
buildDueDateWhere(filters.dueDate),
|
|
),
|
|
orderBy: [asc(cards.index)],
|
|
},
|
|
},
|
|
where: and(
|
|
isNull(lists.deletedAt),
|
|
filters.lists.length > 0
|
|
? inArray(lists.publicId, filters.lists)
|
|
: undefined,
|
|
),
|
|
orderBy: [asc(lists.index)],
|
|
},
|
|
allLists: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
},
|
|
where: isNull(lists.deletedAt),
|
|
orderBy: [asc(lists.index)],
|
|
},
|
|
},
|
|
where: and(
|
|
eq(boards.publicId, boardPublicId),
|
|
isNull(boards.deletedAt),
|
|
eq(boards.type, filters.type ?? "regular"),
|
|
),
|
|
});
|
|
|
|
if (!board) return null;
|
|
|
|
const formattedResult = {
|
|
...board,
|
|
favorite: board.userFavorites.length > 0,
|
|
userFavorites: undefined,
|
|
lists: board.lists.map((list) => ({
|
|
...list,
|
|
cards: list.cards.map((card) => ({
|
|
...card,
|
|
labels: card.labels.map((label) => label.label),
|
|
members: card.members
|
|
.map((member) => member.member)
|
|
.filter((member) => member.deletedAt === null),
|
|
})),
|
|
})),
|
|
};
|
|
|
|
return formattedResult;
|
|
};
|
|
|
|
export const getBySlug = async (
|
|
db: dbClient,
|
|
boardSlug: string,
|
|
workspaceId: number,
|
|
filters: {
|
|
members: string[];
|
|
labels: string[];
|
|
lists: string[];
|
|
dueDate: DueDateFilter[];
|
|
},
|
|
) => {
|
|
let cardIds: string[] = [];
|
|
|
|
if (filters.labels.length) {
|
|
const filteredCards = await db
|
|
.select({
|
|
publicId: cards.publicId,
|
|
})
|
|
.from(cards)
|
|
.leftJoin(cardsToLabels, eq(cards.id, cardsToLabels.cardId))
|
|
.leftJoin(labels, eq(cardsToLabels.labelId, labels.id))
|
|
.where(
|
|
and(
|
|
isNull(cards.deletedAt),
|
|
filters.labels.length > 0
|
|
? inArray(labels.publicId, filters.labels)
|
|
: undefined,
|
|
),
|
|
);
|
|
|
|
cardIds = filteredCards.map((card) => card.publicId);
|
|
}
|
|
|
|
const board = await db.query.boards.findFirst({
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
slug: true,
|
|
visibility: true,
|
|
},
|
|
with: {
|
|
workspace: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
slug: true,
|
|
},
|
|
},
|
|
labels: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
colourCode: true,
|
|
},
|
|
where: isNull(labels.deletedAt),
|
|
},
|
|
lists: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
boardId: true,
|
|
index: true,
|
|
},
|
|
with: {
|
|
cards: {
|
|
columns: {
|
|
publicId: true,
|
|
title: true,
|
|
description: true,
|
|
listId: true,
|
|
index: true,
|
|
dueDate: true,
|
|
},
|
|
with: {
|
|
labels: {
|
|
with: {
|
|
label: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
colourCode: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
attachments: {
|
|
columns: {
|
|
publicId: true,
|
|
},
|
|
where: isNull(cardAttachments.deletedAt),
|
|
orderBy: asc(cardAttachments.createdAt),
|
|
},
|
|
comments: {
|
|
columns: {
|
|
publicId: true,
|
|
},
|
|
where: isNull(comments.deletedAt),
|
|
limit: 1,
|
|
},
|
|
checklists: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
index: true,
|
|
},
|
|
where: isNull(checklists.deletedAt),
|
|
orderBy: asc(checklists.index),
|
|
with: {
|
|
items: {
|
|
columns: {
|
|
publicId: true,
|
|
title: true,
|
|
completed: true,
|
|
index: true,
|
|
},
|
|
where: isNull(checklistItems.deletedAt),
|
|
orderBy: asc(checklistItems.index),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
where: and(
|
|
cardIds.length > 0 ? inArray(cards.publicId, cardIds) : undefined,
|
|
isNull(cards.deletedAt),
|
|
buildDueDateWhere(filters.dueDate),
|
|
),
|
|
orderBy: [asc(cards.index)],
|
|
},
|
|
},
|
|
where: and(
|
|
isNull(lists.deletedAt),
|
|
filters.lists.length > 0
|
|
? inArray(lists.publicId, filters.lists)
|
|
: undefined,
|
|
),
|
|
orderBy: [asc(lists.index)],
|
|
},
|
|
allLists: {
|
|
columns: {
|
|
publicId: true,
|
|
name: true,
|
|
},
|
|
where: isNull(lists.deletedAt),
|
|
orderBy: [asc(lists.index)],
|
|
},
|
|
},
|
|
where: and(
|
|
eq(boards.slug, boardSlug),
|
|
eq(boards.workspaceId, workspaceId),
|
|
isNull(boards.deletedAt),
|
|
eq(boards.visibility, "public"),
|
|
),
|
|
});
|
|
|
|
if (!board) return null;
|
|
|
|
const formattedResult = {
|
|
...board,
|
|
lists: board.lists.map((list) => ({
|
|
...list,
|
|
cards: list.cards.map((card) => ({
|
|
...card,
|
|
labels: card.labels.map((label) => label.label),
|
|
})),
|
|
})),
|
|
};
|
|
|
|
return formattedResult;
|
|
};
|
|
|
|
export const getWithListIdsByPublicId = (
|
|
db: dbClient,
|
|
boardPublicId: string,
|
|
) => {
|
|
return db.query.boards.findFirst({
|
|
columns: {
|
|
id: true,
|
|
workspaceId: true,
|
|
createdBy: true,
|
|
},
|
|
with: {
|
|
lists: {
|
|
columns: {
|
|
id: true,
|
|
},
|
|
},
|
|
},
|
|
where: eq(boards.publicId, boardPublicId),
|
|
});
|
|
};
|
|
|
|
export const getWithLatestListIndexByPublicId = (
|
|
db: dbClient,
|
|
boardPublicId: string,
|
|
) => {
|
|
return db.query.boards.findFirst({
|
|
columns: {
|
|
id: true,
|
|
workspaceId: true,
|
|
},
|
|
with: {
|
|
lists: {
|
|
columns: {
|
|
index: true,
|
|
},
|
|
where: isNull(lists.deletedAt),
|
|
orderBy: [desc(lists.index)],
|
|
limit: 1,
|
|
},
|
|
},
|
|
where: eq(boards.publicId, boardPublicId),
|
|
});
|
|
};
|
|
|
|
export const create = async (
|
|
db: dbClient,
|
|
boardInput: {
|
|
publicId?: string;
|
|
name: string;
|
|
createdBy: string;
|
|
workspaceId: number;
|
|
importId?: number;
|
|
slug: string;
|
|
type?: "regular" | "template";
|
|
sourceBoardId?: number;
|
|
},
|
|
) => {
|
|
const [result] = await db
|
|
.insert(boards)
|
|
.values({
|
|
publicId: boardInput.publicId ?? generateUID(),
|
|
name: boardInput.name,
|
|
createdBy: boardInput.createdBy,
|
|
workspaceId: boardInput.workspaceId,
|
|
importId: boardInput.importId,
|
|
slug: boardInput.slug,
|
|
type: boardInput.type ?? "regular",
|
|
sourceBoardId: boardInput.sourceBoardId,
|
|
})
|
|
.returning({
|
|
id: boards.id,
|
|
publicId: boards.publicId,
|
|
name: boards.name,
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
export const update = async (
|
|
db: dbClient,
|
|
boardInput: {
|
|
name: string | undefined;
|
|
slug: string | undefined;
|
|
visibility: BoardVisibilityStatus | undefined;
|
|
boardPublicId: string;
|
|
isArchived?: boolean;
|
|
},
|
|
) => {
|
|
const [result] = await db
|
|
.update(boards)
|
|
.set({
|
|
name: boardInput.name,
|
|
slug: boardInput.slug,
|
|
visibility: boardInput.visibility,
|
|
updatedAt: new Date(),
|
|
...(boardInput.isArchived !== undefined && { isArchived: boardInput.isArchived })
|
|
})
|
|
.where(eq(boards.publicId, boardInput.boardPublicId))
|
|
.returning({
|
|
publicId: boards.publicId,
|
|
name: boards.name,
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
export const softDelete = async (
|
|
db: dbClient,
|
|
args: {
|
|
boardId: number;
|
|
deletedAt: Date;
|
|
deletedBy: string;
|
|
},
|
|
) => {
|
|
const [result] = await db
|
|
.update(boards)
|
|
.set({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
|
|
.where(and(eq(boards.id, args.boardId), isNull(boards.deletedAt)))
|
|
.returning({
|
|
publicId: boards.publicId,
|
|
name: boards.name,
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
export const hardDelete = async (db: dbClient, workspaceId: number) => {
|
|
const [result] = await db
|
|
.delete(boards)
|
|
.where(eq(boards.workspaceId, workspaceId))
|
|
.returning({
|
|
publicId: boards.publicId,
|
|
name: boards.name,
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
export const isSlugUnique = async (
|
|
db: dbClient,
|
|
args: { slug: string; workspaceId: number },
|
|
) => {
|
|
const result = await db.query.boards.findFirst({
|
|
columns: {
|
|
slug: true,
|
|
},
|
|
where: and(
|
|
eq(boards.slug, args.slug),
|
|
eq(boards.workspaceId, args.workspaceId),
|
|
isNull(boards.deletedAt),
|
|
),
|
|
});
|
|
|
|
return result === undefined;
|
|
};
|
|
|
|
export const getWorkspaceAndBoardIdByBoardPublicId = async (
|
|
db: dbClient,
|
|
boardPublicId: string,
|
|
) => {
|
|
const result = await db.query.boards.findFirst({
|
|
columns: {
|
|
id: true,
|
|
workspaceId: true,
|
|
createdBy: true,
|
|
},
|
|
where: eq(boards.publicId, boardPublicId),
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
export const isBoardSlugAvailable = async (
|
|
db: dbClient,
|
|
boardSlug: string,
|
|
workspaceId: number,
|
|
) => {
|
|
const result = await db.query.boards.findFirst({
|
|
columns: {
|
|
id: true,
|
|
},
|
|
where: and(
|
|
eq(boards.slug, boardSlug),
|
|
eq(boards.workspaceId, workspaceId),
|
|
isNull(boards.deletedAt),
|
|
),
|
|
});
|
|
|
|
return result === undefined;
|
|
};
|
|
|
|
// Create a new board (regular/template) from a full board snapshot
|
|
export const createFromSnapshot = async (
|
|
db: dbClient,
|
|
args: {
|
|
source: {
|
|
name: string;
|
|
labels: { publicId: string; name: string; colourCode: string | null }[];
|
|
lists: {
|
|
name: string;
|
|
index: number;
|
|
cards: {
|
|
title: string;
|
|
description: string | null;
|
|
index: number;
|
|
labels: {
|
|
publicId: string;
|
|
name: string;
|
|
colourCode: string | null;
|
|
}[];
|
|
checklists?: {
|
|
publicId: string;
|
|
name: string;
|
|
index: number;
|
|
items: {
|
|
publicId: string;
|
|
title: string;
|
|
completed: boolean;
|
|
index: number;
|
|
}[];
|
|
}[];
|
|
}[];
|
|
}[];
|
|
};
|
|
workspaceId: number;
|
|
createdBy: string;
|
|
slug: string;
|
|
name?: string;
|
|
type: "regular" | "template";
|
|
sourceBoardId?: number;
|
|
},
|
|
) => {
|
|
return db.transaction(async (tx) => {
|
|
const [newBoard] = await tx
|
|
.insert(boards)
|
|
.values({
|
|
publicId: generateUID(),
|
|
name: args.name ?? args.source.name,
|
|
slug: args.slug,
|
|
createdBy: args.createdBy,
|
|
workspaceId: args.workspaceId,
|
|
type: args.type,
|
|
sourceBoardId: args.sourceBoardId,
|
|
})
|
|
.returning({
|
|
id: boards.id,
|
|
publicId: boards.publicId,
|
|
name: boards.name,
|
|
});
|
|
|
|
if (!newBoard) throw new Error("Failed to create board");
|
|
|
|
// Labels
|
|
const srcLabels = args.source.labels;
|
|
const labelMap = new Map<string, number>();
|
|
|
|
if (srcLabels.length) {
|
|
const inserted = await tx
|
|
.insert(labels)
|
|
.values(
|
|
srcLabels.map((l) => ({
|
|
publicId: generateUID(),
|
|
name: l.name,
|
|
colourCode: l.colourCode ?? null,
|
|
createdBy: args.createdBy,
|
|
boardId: newBoard.id,
|
|
})),
|
|
)
|
|
.returning({ id: labels.id });
|
|
|
|
for (let i = 0; i < srcLabels.length; i++) {
|
|
const src = srcLabels[i];
|
|
|
|
if (!src) throw new Error("Source label not found");
|
|
|
|
const created = inserted[i];
|
|
if (created) labelMap.set(src.publicId, created.id);
|
|
}
|
|
}
|
|
|
|
// Lists
|
|
const listIndexToId = new Map<number, number>();
|
|
const srcLists = [...args.source.lists].sort((a, b) => a.index - b.index);
|
|
if (srcLists.length) {
|
|
const insertedLists = await tx
|
|
.insert(lists)
|
|
.values(
|
|
srcLists.map((list) => ({
|
|
publicId: generateUID(),
|
|
name: list.name,
|
|
createdBy: args.createdBy,
|
|
boardId: newBoard.id,
|
|
index: list.index,
|
|
})),
|
|
)
|
|
.returning({ id: lists.id, index: lists.index });
|
|
|
|
for (const list of insertedLists) listIndexToId.set(list.index, list.id);
|
|
}
|
|
|
|
// Cards, card-labels, checklists
|
|
for (const list of srcLists) {
|
|
const newListId = listIndexToId.get(list.index);
|
|
if (!newListId) continue;
|
|
const sortedCards = [...list.cards].sort((a, b) => a.index - b.index);
|
|
|
|
for (const card of sortedCards) {
|
|
const [createdCard] = await tx
|
|
.insert(cards)
|
|
.values({
|
|
publicId: generateUID(),
|
|
title: card.title,
|
|
description: card.description ?? "",
|
|
createdBy: args.createdBy,
|
|
listId: newListId,
|
|
index: card.index,
|
|
})
|
|
.returning({ id: cards.id });
|
|
|
|
if (!createdCard) throw new Error("Failed to create card");
|
|
|
|
// Create card.created activity
|
|
await tx.insert(cardActivities).values({
|
|
publicId: generateUID(),
|
|
type: "card.created",
|
|
cardId: createdCard.id,
|
|
createdBy: args.createdBy,
|
|
sourceBoardId: args.sourceBoardId,
|
|
});
|
|
|
|
if (card.labels.length) {
|
|
const cardLabels: { cardId: number; labelId: number }[] = [];
|
|
for (const label of card.labels) {
|
|
const newLabelId = labelMap.get(label.publicId);
|
|
if (newLabelId)
|
|
cardLabels.push({ cardId: createdCard.id, labelId: newLabelId });
|
|
}
|
|
if (cardLabels.length) {
|
|
await tx.insert(cardsToLabels).values(cardLabels);
|
|
|
|
// Create card.updated.label.added activities for each label
|
|
const labelActivities = cardLabels.map((cardLabel) => ({
|
|
publicId: generateUID(),
|
|
type: "card.updated.label.added" as const,
|
|
cardId: cardLabel.cardId,
|
|
labelId: cardLabel.labelId,
|
|
createdBy: args.createdBy,
|
|
sourceBoardId: args.sourceBoardId,
|
|
}));
|
|
await tx.insert(cardActivities).values(labelActivities);
|
|
}
|
|
}
|
|
|
|
if (card.checklists?.length) {
|
|
const sortedChecklists = [...card.checklists].sort(
|
|
(a, b) => a.index - b.index,
|
|
);
|
|
for (const checklist of sortedChecklists) {
|
|
const [createdChecklist] = await tx
|
|
.insert(checklists)
|
|
.values({
|
|
publicId: generateUID(),
|
|
name: checklist.name,
|
|
createdBy: args.createdBy,
|
|
cardId: createdCard.id,
|
|
index: checklist.index,
|
|
})
|
|
.returning({ id: checklists.id });
|
|
|
|
if (!createdChecklist) continue;
|
|
|
|
// Create card.updated.checklist.added activity
|
|
await tx.insert(cardActivities).values({
|
|
publicId: generateUID(),
|
|
type: "card.updated.checklist.added",
|
|
cardId: createdCard.id,
|
|
toTitle: checklist.name,
|
|
createdBy: args.createdBy,
|
|
sourceBoardId: args.sourceBoardId,
|
|
});
|
|
|
|
if (checklist.items.length) {
|
|
const itemValues = [...checklist.items]
|
|
.sort((a, b) => a.index - b.index)
|
|
.map((checklistItem) => ({
|
|
publicId: generateUID(),
|
|
title: checklistItem.title,
|
|
createdBy: args.createdBy,
|
|
checklistId: createdChecklist.id,
|
|
index: checklistItem.index,
|
|
completed: !!checklistItem.completed,
|
|
}));
|
|
|
|
if (itemValues.length) {
|
|
await tx.insert(checklistItems).values(itemValues);
|
|
|
|
// Create card.updated.checklist.item.added activities for each item
|
|
const itemActivities = itemValues.map((item) => ({
|
|
publicId: generateUID(),
|
|
type: "card.updated.checklist.item.added" as const,
|
|
cardId: createdCard.id,
|
|
toTitle: item.title,
|
|
createdBy: args.createdBy,
|
|
sourceBoardId: args.sourceBoardId,
|
|
}));
|
|
await tx.insert(cardActivities).values(itemActivities);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return newBoard;
|
|
});
|
|
};
|
|
|
|
export const addUserFavorite = async (
|
|
db: dbClient,
|
|
userId: string,
|
|
boardId: number,
|
|
) => {
|
|
return db
|
|
.insert(userBoardFavorites)
|
|
.values({
|
|
userId,
|
|
boardId,
|
|
})
|
|
.onConflictDoNothing()
|
|
.returning();
|
|
};
|
|
|
|
export const removeUserFavorite = async (
|
|
db: dbClient,
|
|
userId: string,
|
|
boardId: number,
|
|
) => {
|
|
return db
|
|
.delete(userBoardFavorites)
|
|
.where(
|
|
and(
|
|
eq(userBoardFavorites.userId, userId),
|
|
eq(userBoardFavorites.boardId, boardId)
|
|
)
|
|
)
|
|
.returning();
|
|
}; |