feat: add user setting for home board (#956)

This commit is contained in:
Meier Lukas
2024-08-09 19:24:50 +02:00
committed by GitHub
parent 13e09968d9
commit f327837d82
6 changed files with 147 additions and 14 deletions

View File

@@ -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 (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Select w="100%" data={boardsData} {...form.getInputProps("homeBoardId")} />
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof validation.user.changeHomeBoard>;

View File

@@ -1,12 +0,0 @@
import { Stack, Title } from "@mantine/core";
import { LanguageCombobox } from "~/components/language/language-combobox";
export const ProfileLanguageChange = () => {
return (
<Stack mb="lg">
<Title order={2}>Language & Region</Title>
<LanguageCombobox />
</Stack>
);
};

View File

@@ -6,14 +6,15 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next"; import { auth } from "@homarr/auth/next";
import { getI18n, getScopedI18n } from "@homarr/translation/server"; import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { LanguageCombobox } from "~/components/language/language-combobox";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone"; import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { catchTrpcNotFound } from "~/errors/trpc-not-found"; import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { createMetaTitle } from "~/metadata"; import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access"; import { canAccessUserEditPage } from "../access";
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
import { DeleteUserButton } from "./_components/_delete-user-button"; import { DeleteUserButton } from "./_components/_delete-user-button";
import { UserProfileAvatarForm } from "./_components/_profile-avatar-form"; import { UserProfileAvatarForm } from "./_components/_profile-avatar-form";
import { UserProfileForm } from "./_components/_profile-form"; import { UserProfileForm } from "./_components/_profile-form";
import { ProfileLanguageChange } from "./_components/_profile-language-change";
interface Props { interface Props {
params: { params: {
@@ -54,6 +55,8 @@ export default async function EditUserPage({ params }: Props) {
notFound(); notFound();
} }
const boards = await api.board.getAllBoards();
const isCredentialsUser = user.provider === "credentials"; const isCredentialsUser = user.provider === "credentials";
return ( return (
@@ -74,7 +77,21 @@ export default async function EditUserPage({ params }: Props) {
</Box> </Box>
</Group> </Group>
<ProfileLanguageChange /> <Stack mb="lg">
<Title order={2}>{tGeneral("item.language")}</Title>
<LanguageCombobox />
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.board")}</Title>
<ChangeHomeBoardForm
user={user}
boardsData={boards.map((board) => ({
value: board.id,
label: board.name,
}))}
/>
</Stack>
{isCredentialsUser && ( {isCredentialsUser && (
<DangerZoneRoot> <DangerZoneRoot>

View File

@@ -156,6 +156,7 @@ export const userRouter = createTRPCRouter({
emailVerified: true, emailVerified: true,
image: true, image: true,
provider: true, provider: true,
homeBoardId: true,
}, },
where: eq(users.id, input.userId), where: eq(users.id, input.userId),
}); });
@@ -266,6 +267,39 @@ export const userRouter = createTRPCRouter({
}) })
.where(eq(users.id, input.userId)); .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<typeof validation.user.create>) => { const createUserAsync = async (db: Database, input: z.infer<typeof validation.user.create>) => {

View File

@@ -37,6 +37,9 @@ export default {
previousPassword: { previousPassword: {
label: "Previous password", label: "Previous password",
}, },
homeBoard: {
label: "Home board",
},
}, },
error: { error: {
usernameTaken: "Username already taken", 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: { manageAvatar: {
changeImage: { changeImage: {
label: "Change image", label: "Change image",
@@ -1404,10 +1417,17 @@ export default {
setting: { setting: {
general: { general: {
title: "General", title: "General",
item: {
language: "Language & Region",
board: "Home board",
},
}, },
security: { security: {
title: "Security", title: "Security",
}, },
board: {
title: "Boards",
},
}, },
list: { list: {
metaTitle: "Manage users", metaTitle: "Manage users",
@@ -1736,6 +1756,7 @@ export default {
}, },
general: "General", general: "General",
security: "Security", security: "Security",
board: "Boards",
groups: { groups: {
label: "Groups", label: "Groups",
}, },

View File

@@ -68,6 +68,10 @@ const changePasswordSchema = z
const changePasswordApiSchema = changePasswordSchema.and(z.object({ userId: z.string() })); const changePasswordApiSchema = changePasswordSchema.and(z.object({ userId: z.string() }));
const changeHomeBoardSchema = z.object({
homeBoardId: z.string().min(1),
});
export const userSchemas = { export const userSchemas = {
signIn: signInSchema, signIn: signInSchema,
registration: registrationSchema, registration: registrationSchema,
@@ -77,5 +81,6 @@ export const userSchemas = {
password: passwordSchema, password: passwordSchema,
editProfile: editProfileSchema, editProfile: editProfileSchema,
changePassword: changePasswordSchema, changePassword: changePasswordSchema,
changeHomeBoard: changeHomeBoardSchema,
changePasswordApi: changePasswordApiSchema, changePasswordApi: changePasswordApiSchema,
}; };