feat: add board favourites with animations (#318)

* feat: add board favorites with animations

* feat: add board favorites functionality with admin-only access

* refactor: make board favorites user-specific via junction table

* fix: assertUserInWorkspace

* Update pnpm lockfile to match package.json

* chore: clean up migrations

---------

Co-authored-by: Henry <henry_ball@hotmail.co.uk>
This commit is contained in:
Eliott Herbert-Byrnes
2026-02-01 22:29:51 +00:00
committed by GitHub
parent ec37f5480a
commit ef5bf87fdf
16 changed files with 6729 additions and 47 deletions

View File

@@ -26,6 +26,7 @@ import {
comments,
labels,
lists,
userBoardFavorites,
workspaceMembers,
} from "@kan/db/schema";
import { generateUID } from "@kan/shared/utils";
@@ -39,17 +40,24 @@ export const getCount = async (db: dbClient) => {
return result[0]?.count ?? 0;
};
export const getAllByWorkspaceId = (
export const getAllByWorkspaceId = async (
db: dbClient,
workspaceId: number,
userId: string,
opts?: { type?: "regular" | "template" },
) => {
return db.query.boards.findMany({
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,
@@ -72,6 +80,21 @@ export const getAllByWorkspaceId = (
opts?.type ? eq(boards.type, opts.type) : 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) => {
@@ -122,6 +145,7 @@ const buildDueDateWhere = (filters: DueDateFilter[]) => {
export const getByPublicId = async (
db: dbClient,
boardPublicId: string,
userId: string,
filters: {
members: string[];
labels: string[];
@@ -173,6 +197,12 @@ export const getByPublicId = async (
visibility: true,
},
with: {
userFavorites: {
where: eq(userBoardFavorites.userId, userId),
columns: {
userId: true,
},
},
workspace: {
columns: {
publicId: true,
@@ -325,6 +355,8 @@ export const getByPublicId = async (
const formattedResult = {
...board,
favorite: board.userFavorites.length > 0,
userFavorites: undefined,
lists: board.lists.map((list) => ({
...list,
cards: list.cards.map((card) => ({
@@ -924,3 +956,34 @@ export const createFromSnapshot = async (
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();
};

View File

@@ -6,6 +6,7 @@ import {
ilike,
inArray,
isNull,
asc,
or,
sql,
} from "drizzle-orm";
@@ -250,6 +251,7 @@ export const getBySlugWithBoards = (db: dbClient, workspaceSlug: string) => {
name: true,
},
where: and(isNull(boards.deletedAt), eq(boards.visibility, "public")),
orderBy: [asc(boards.name)]
},
},
where: and(