diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx
new file mode 100644
index 000000000..bbc8f308c
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import { Button, Group, Select, Stack } from "@mantine/core";
+
+import type { RouterOutputs } from "@homarr/api";
+import { clientApi } from "@homarr/api/client";
+import { useZodForm } from "@homarr/form";
+import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
+import { useI18n } from "@homarr/translation/client";
+import type { z } from "@homarr/validation";
+import { validation } from "@homarr/validation";
+
+import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
+
+interface ChangeHomeBoardFormProps {
+ user: RouterOutputs["user"]["getById"];
+ boardsData: { value: string; label: string }[];
+}
+
+export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormProps) => {
+ const t = useI18n();
+ const { mutate, isPending } = clientApi.user.changeHomeBoardId.useMutation({
+ async onSettled() {
+ await revalidatePathActionAsync(`/manage/users/${user.id}`);
+ },
+ onSuccess(_, variables) {
+ form.setInitialValues({
+ homeBoardId: variables.homeBoardId,
+ });
+ showSuccessNotification({
+ message: t("user.action.changeHomeBoard.notification.success.message"),
+ });
+ },
+ onError() {
+ showErrorNotification({
+ message: t("user.action.changeHomeBoard.notification.error.message"),
+ });
+ },
+ });
+ const form = useZodForm(validation.user.changeHomeBoard, {
+ initialValues: {
+ homeBoardId: user.homeBoardId ?? "",
+ },
+ });
+
+ const handleSubmit = (values: FormType) => {
+ mutate({
+ userId: user.id,
+ ...values,
+ });
+ };
+
+ return (
+
+ );
+};
+
+type FormType = z.infer;
diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-language-change.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-language-change.tsx
deleted file mode 100644
index e83b8b170..000000000
--- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-language-change.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Stack, Title } from "@mantine/core";
-
-import { LanguageCombobox } from "~/components/language/language-combobox";
-
-export const ProfileLanguageChange = () => {
- return (
-
- Language & Region
-
-
- );
-};
diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx
index 756554366..7d1ee0302 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx
@@ -6,14 +6,15 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
+import { LanguageCombobox } from "~/components/language/language-combobox";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access";
+import { ChangeHomeBoardForm } from "./_components/_change-home-board";
import { DeleteUserButton } from "./_components/_delete-user-button";
import { UserProfileAvatarForm } from "./_components/_profile-avatar-form";
import { UserProfileForm } from "./_components/_profile-form";
-import { ProfileLanguageChange } from "./_components/_profile-language-change";
interface Props {
params: {
@@ -54,6 +55,8 @@ export default async function EditUserPage({ params }: Props) {
notFound();
}
+ const boards = await api.board.getAllBoards();
+
const isCredentialsUser = user.provider === "credentials";
return (
@@ -74,7 +77,21 @@ export default async function EditUserPage({ params }: Props) {
-
+
+ {tGeneral("item.language")}
+
+
+
+
+ {tGeneral("item.board")}
+ ({
+ value: board.id,
+ label: board.name,
+ }))}
+ />
+
{isCredentialsUser && (
diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts
index af838445f..c0476027f 100644
--- a/packages/api/src/router/user.ts
+++ b/packages/api/src/router/user.ts
@@ -156,6 +156,7 @@ export const userRouter = createTRPCRouter({
emailVerified: true,
image: true,
provider: true,
+ homeBoardId: true,
},
where: eq(users.id, input.userId),
});
@@ -266,6 +267,39 @@ export const userRouter = createTRPCRouter({
})
.where(eq(users.id, input.userId));
}),
+ changeHomeBoardId: protectedProcedure
+ .input(validation.user.changeHomeBoard.and(z.object({ userId: z.string() })))
+ .mutation(async ({ input, ctx }) => {
+ const user = ctx.session.user;
+ // Only admins can change other users' passwords
+ if (!user.permissions.includes("admin") && user.id !== input.userId) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "User not found",
+ });
+ }
+
+ const dbUser = await ctx.db.query.users.findFirst({
+ columns: {
+ id: true,
+ },
+ where: eq(users.id, input.userId),
+ });
+
+ if (!dbUser) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "User not found",
+ });
+ }
+
+ await ctx.db
+ .update(users)
+ .set({
+ homeBoardId: input.homeBoardId,
+ })
+ .where(eq(users.id, input.userId));
+ }),
});
const createUserAsync = async (db: Database, input: z.infer) => {
diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts
index aa851a7c6..f3decae3b 100644
--- a/packages/translation/src/lang/en.ts
+++ b/packages/translation/src/lang/en.ts
@@ -37,6 +37,9 @@ export default {
previousPassword: {
label: "Previous password",
},
+ homeBoard: {
+ label: "Home board",
+ },
},
error: {
usernameTaken: "Username already taken",
@@ -81,6 +84,16 @@ export default {
},
},
},
+ changeHomeBoard: {
+ notification: {
+ success: {
+ message: "Home board changed successfully",
+ },
+ error: {
+ message: "Unable to change home board",
+ },
+ },
+ },
manageAvatar: {
changeImage: {
label: "Change image",
@@ -1404,10 +1417,17 @@ export default {
setting: {
general: {
title: "General",
+ item: {
+ language: "Language & Region",
+ board: "Home board",
+ },
},
security: {
title: "Security",
},
+ board: {
+ title: "Boards",
+ },
},
list: {
metaTitle: "Manage users",
@@ -1736,6 +1756,7 @@ export default {
},
general: "General",
security: "Security",
+ board: "Boards",
groups: {
label: "Groups",
},
diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts
index fa618eadf..4b1b4c8e0 100644
--- a/packages/validation/src/user.ts
+++ b/packages/validation/src/user.ts
@@ -68,6 +68,10 @@ const changePasswordSchema = z
const changePasswordApiSchema = changePasswordSchema.and(z.object({ userId: z.string() }));
+const changeHomeBoardSchema = z.object({
+ homeBoardId: z.string().min(1),
+});
+
export const userSchemas = {
signIn: signInSchema,
registration: registrationSchema,
@@ -77,5 +81,6 @@ export const userSchemas = {
password: passwordSchema,
editProfile: editProfileSchema,
changePassword: changePasswordSchema,
+ changeHomeBoard: changeHomeBoardSchema,
changePasswordApi: changePasswordApiSchema,
};