feat: add home board for users (#505)

* feat: add home board for users

* fix: format issues

* fix: deepsource issue

* chore: address pull request feedback

* fix: typecheck issue
This commit is contained in:
Meier Lukas
2024-05-18 16:57:00 +02:00
committed by GitHub
parent dfed804f65
commit 7e339c09c8
36 changed files with 2509 additions and 62 deletions

View File

@@ -12,6 +12,7 @@ import {
integrationItems,
items,
sections,
users,
} from "@homarr/db/schema/sqlite";
import type { WidgetKind } from "@homarr/definitions";
import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
@@ -33,14 +34,15 @@ import { throwIfActionForbiddenAsync } from "./board/board-access";
export const boardRouter = createTRPCRouter({
getAllBoards: publicProcedure.query(async ({ ctx }) => {
const userId = ctx.session?.user.id;
const permissionsOfCurrentUserWhenPresent =
await ctx.db.query.boardUserPermissions.findMany({
where: eq(boardUserPermissions.userId, ctx.session?.user.id ?? ""),
where: eq(boardUserPermissions.userId, userId ?? ""),
});
const permissionsOfCurrentUserGroupsWhenPresent =
await ctx.db.query.groupMembers.findMany({
where: eq(groupMembers.userId, ctx.session?.user.id ?? ""),
where: eq(groupMembers.userId, userId ?? ""),
with: {
group: {
with: {
@@ -60,6 +62,11 @@ export const boardRouter = createTRPCRouter({
)
.flat(),
);
const currentUserWhenPresent = await ctx.db.query.users.findFirst({
where: eq(users.id, userId ?? ""),
});
const dbBoards = await ctx.db.query.boards.findMany({
columns: {
id: true,
@@ -98,7 +105,10 @@ export const boardRouter = createTRPCRouter({
boardIds.length > 0 ? inArray(boards.id, boardIds) : undefined,
),
});
return dbBoards;
return dbBoards.map((board) => ({
...board,
isHome: currentUserWhenPresent?.homeBoardId === board.id,
}));
}),
createBoard: permissionRequiredProcedure
.requiresPermission("board-create")
@@ -160,8 +170,31 @@ export const boardRouter = createTRPCRouter({
await ctx.db.delete(boards).where(eq(boards.id, input.id));
}),
getDefaultBoard: publicProcedure.query(async ({ ctx }) => {
const boardWhere = eq(boards.name, "default");
setHomeBoard: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(
ctx,
eq(boards.id, input.id),
"board-view",
);
await ctx.db
.update(users)
.set({ homeBoardId: input.id })
.where(eq(users.id, ctx.session.user.id));
}),
getHomeBoard: publicProcedure.query(async ({ ctx }) => {
const userId = ctx.session?.user.id;
const user = userId
? await ctx.db.query.users.findFirst({
where: eq(users.id, userId),
})
: null;
const boardWhere = user?.homeBoardId
? eq(boards.id, user.homeBoardId)
: eq(boards.name, "home");
await throwIfActionForbiddenAsync(ctx, boardWhere, "board-view");
return await getFullBoardWithWhereAsync(

View File

@@ -41,6 +41,7 @@ const createRandomUserAsync = async (db: Database) => {
const userId = createId();
await db.insert(users).values({
id: userId,
homeBoardId: null,
});
return userId;
};
@@ -493,21 +494,21 @@ describe("deleteBoard should delete board", () => {
});
});
describe("getDefaultBoard should return default board", () => {
it("should return default board", async () => {
describe("getHomeBoard should return home board", () => {
it("should return home board", async () => {
// Arrange
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
const db = createDb();
const caller = boardRouter.createCaller({ db, session: defaultSession });
const fullBoardProps = await createFullBoardAsync(db, "default");
const fullBoardProps = await createFullBoardAsync(db, "home");
// Act
const result = await caller.getDefaultBoard();
const result = await caller.getHomeBoard();
// Assert
expectInputToBeFullBoardWithName(result, {
name: "default",
name: "home",
...fullBoardProps,
});
expect(spy).toHaveBeenCalledWith(
@@ -1339,7 +1340,7 @@ describe("saveGroupBoardPermissions should save group board permissions", () =>
});
const expectInputToBeFullBoardWithName = (
input: RouterOutputs["board"]["getDefaultBoard"],
input: RouterOutputs["board"]["getHomeBoard"],
props: { name: string } & Awaited<ReturnType<typeof createFullBoardAsync>>,
) => {
expect(input.id).toBe(props.boardId);

View File

@@ -232,6 +232,7 @@ describe("editProfile shoud update user", () => {
salt: null,
password: null,
image: null,
homeBoardId: null,
});
});
@@ -274,6 +275,7 @@ describe("editProfile shoud update user", () => {
salt: null,
password: null,
image: null,
homeBoardId: null,
});
});
});
@@ -297,6 +299,7 @@ describe("delete should delete user", () => {
image: null,
password: null,
salt: null,
homeBoardId: null,
},
{
id: userToDelete,
@@ -306,6 +309,7 @@ describe("delete should delete user", () => {
image: null,
password: null,
salt: null,
homeBoardId: null,
},
{
id: createId(),
@@ -315,6 +319,7 @@ describe("delete should delete user", () => {
image: null,
password: null,
salt: null,
homeBoardId: null,
},
];

View File

@@ -0,0 +1,2 @@
ALTER TABLE `user` ADD `homeBoardId` varchar(64);--> statement-breakpoint
ALTER TABLE `user` ADD CONSTRAINT `user_homeBoardId_board_id_fk` FOREIGN KEY (`homeBoardId`) REFERENCES `board`(`id`) ON DELETE set null ON UPDATE no action;

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,13 @@
"when": 1715334452118,
"tag": "0000_harsh_photon",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1715885855801,
"tag": "0001_wild_alex_wilder",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,33 @@
COMMIT TRANSACTION;
--> statement-breakpoint
PRAGMA foreign_keys = OFF;
--> statement-breakpoint
BEGIN TRANSACTION;
--> statement-breakpoint
ALTER TABLE `user` RENAME TO `__user_old`;
--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`name` text,
`email` text,
`emailVerified` integer,
`image` text,
`password` text,
`salt` text,
`homeBoardId` text,
FOREIGN KEY (`homeBoardId`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
INSERT INTO `user` SELECT `id`, `name`, `email`, `emailVerified`, `image`, `password`, `salt`, null FROM `__user_old`;
--> statement-breakpoint
DROP TABLE `__user_old`;
--> statement-breakpoint
ALTER TABLE `user` RENAME TO `__user_old`;
--> statement-breakpoint
ALTER TABLE `__user_old` RENAME TO `user`;
--> statement-breakpoint
COMMIT TRANSACTION;
--> statement-breakpoint
PRAGMA foreign_keys = ON;
--> statement-breakpoint
BEGIN TRANSACTION;

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,13 @@
"when": 1715334238443,
"tag": "0000_talented_ben_parker",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1715871797713,
"tag": "0001_mixed_titanium_man",
"breakpoints": true
}
]
}

View File

@@ -1,5 +1,6 @@
import type { AdapterAccount } from "@auth/core/adapters";
import { relations } from "drizzle-orm";
import type { AnyMySqlColumn } from "drizzle-orm/mysql-core";
import {
boolean,
index,
@@ -36,6 +37,12 @@ export const users = mysqlTable("user", {
image: text("image"),
password: text("password"),
salt: text("salt"),
homeBoardId: varchar("homeBoardId", { length: 64 }).references(
(): AnyMySqlColumn => boards.id,
{
onDelete: "set null",
},
),
});
export const accounts = mysqlTable(

View File

@@ -1,6 +1,7 @@
import type { AdapterAccount } from "@auth/core/adapters";
import type { InferSelectModel } from "drizzle-orm";
import { relations } from "drizzle-orm";
import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core";
import {
index,
int,
@@ -35,6 +36,12 @@ export const users = sqliteTable("user", {
image: text("image"),
password: text("password"),
salt: text("salt"),
homeBoardId: text("homeBoardId").references(
(): AnySQLiteColumn => boards.id,
{
onDelete: "set null",
},
),
});
export const accounts = sqliteTable(

View File

@@ -4,11 +4,16 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import { schema } from "..";
export const createDb = () => {
export const createDb = (debug?: boolean) => {
const sqlite = new Database(":memory:");
const db = drizzle(sqlite, { schema });
const db = drizzle(sqlite, { schema, logger: debug });
migrate(db, {
migrationsFolder: "./packages/db/migrations/sqlite",
});
if (debug) {
console.log("Database created");
}
return db;
};

View File

@@ -502,7 +502,7 @@ export default {
preferences: "Your preferences",
logout: "Logout",
login: "Login",
navigateDefaultBoard: "Navigate to default board",
homeBoard: "Your home board",
loggedOut: "Logged out",
},
},
@@ -970,6 +970,9 @@ export default {
label: "Name",
},
},
content: {
metaTitle: "{boardName} board",
},
setting: {
title: "Settings for {boardName} board",
section: {
@@ -1152,6 +1155,13 @@ export default {
settings: {
label: "Settings",
},
setHomeBoard: {
label: "Set as your home board",
badge: {
label: "Home",
tooltip: "This board will show as your home board",
},
},
delete: {
label: "Delete permanently",
confirm: {

View File

@@ -7,7 +7,7 @@ import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "..";
import { ClientServerDataInitalizer } from "./client";
import { GlobalItemServerDataProvider } from "./provider";
type Board = RouterOutputs["board"]["getDefaultBoard"];
type Board = RouterOutputs["board"]["getHomeBoard"];
type Props = PropsWithChildren<{
shouldRun: boolean;