feat: add user setting for home board (#956)
This commit is contained in:
@@ -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>;
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>) => {
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user