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:
committed by
GitHub
parent
ec37f5480a
commit
ef5bf87fdf
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS "user_board_favorites" (
|
||||
"userId" uuid NOT NULL,
|
||||
"boardId" bigint NOT NULL,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "user_board_favorites_userId_boardId_pk" PRIMARY KEY("userId","boardId")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "user_board_favorites" ADD CONSTRAINT "user_board_favorites_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "user_board_favorites" ADD CONSTRAINT "user_board_favorites_boardId_board_id_fk" FOREIGN KEY ("boardId") REFERENCES "public"."board"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "user_board_favorite_user_idx" ON "user_board_favorites" USING btree ("userId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "user_board_favorite_board_idx" ON "user_board_favorites" USING btree ("boardId");
|
||||
2982
packages/db/migrations/meta/20260119164257_snapshot.json
Normal file
2982
packages/db/migrations/meta/20260119164257_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "c24a8e89-c100-4483-9f0b-391201327ac5",
|
||||
"prevId": "4e1ebb8b-bd52-48c5-87d6-8e6e5eb8bbde",
|
||||
"prevId": "3b54938c-c8d2-41c1-b4ba-a377719891bc",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
|
||||
3411
packages/db/migrations/meta/20260201215958_snapshot.json
Normal file
3411
packages/db/migrations/meta/20260201215958_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -169,6 +169,13 @@
|
||||
"when": 1769435949476,
|
||||
"tag": "20260126135909_AddWorkspaceRoles",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 24,
|
||||
"version": "7",
|
||||
"when": 1769983198190,
|
||||
"tag": "20260201215958_AddUserBoardFavouritesTable",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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(
|
||||
|
||||
@@ -2,9 +2,11 @@ import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
bigserial,
|
||||
boolean,
|
||||
index,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
@@ -67,6 +69,7 @@ export const boards = pgTable(
|
||||
).enableRLS();
|
||||
|
||||
export const boardsRelations = relations(boards, ({ one, many }) => ({
|
||||
userFavorites: many(userBoardFavorites),
|
||||
createdBy: one(users, {
|
||||
fields: [boards.createdBy],
|
||||
references: [users.id],
|
||||
@@ -91,3 +94,21 @@ export const boardsRelations = relations(boards, ({ one, many }) => ({
|
||||
relationName: "boardWorkspace",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const userBoardFavorites = pgTable(
|
||||
"user_board_favorites",
|
||||
{
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
boardId: bigint("boardId", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => boards.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
pk: primaryKey({ columns: [table.userId, table.boardId] }),
|
||||
userIdx: index("user_board_favorite_user_idx").on(table.userId),
|
||||
boardIdx: index("user_board_favorite_board_idx").on(table.boardId),
|
||||
}),
|
||||
);
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
import { apikey } from "./auth";
|
||||
import { boards } from "./boards";
|
||||
import { boards, userBoardFavorites } from "./boards";
|
||||
import { cards } from "./cards";
|
||||
import { imports } from "./imports";
|
||||
import { lists } from "./lists";
|
||||
@@ -84,3 +84,14 @@ export const usersToWorkspacesRelations = relations(
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const userBoardFavoritesRelations = relations(userBoardFavorites, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [userBoardFavorites.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
board: one(boards, {
|
||||
fields: [userBoardFavorites.boardId],
|
||||
references: [boards.id],
|
||||
}),
|
||||
}));
|
||||
Reference in New Issue
Block a user