Added password input
This commit is contained in:
parent
4fbcd5581d
commit
dc4379b482
|
|
@ -1,88 +0,0 @@
|
|||
"use client";
|
||||
import { UserFormData } from "@/features/dashboard/users/formSchemas/userFormDataSchema";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Modal,
|
||||
ScrollArea,
|
||||
Text,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import deleteUser from "@/features/dashboard/users/actions/deleteUser";
|
||||
import { showNotification } from "@/utils/notifications";
|
||||
|
||||
interface Props {
|
||||
data: UserFormData;
|
||||
}
|
||||
|
||||
export default function DeleteModal(props: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
|
||||
/**
|
||||
* Closes the modal. It won't close if a submission is in progress.
|
||||
*/
|
||||
const closeModal = () => {
|
||||
if (isSubmitting) return;
|
||||
router.replace("?");
|
||||
};
|
||||
|
||||
const confirmAction = () => {
|
||||
setSubmitting(true)
|
||||
deleteUser(props.data.id)
|
||||
.then((response) => {
|
||||
if (response.success){
|
||||
showNotification(response.message);
|
||||
router.replace("?")
|
||||
return;
|
||||
} else {
|
||||
showNotification(response.message, "error")
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
//TODO: Handle Error
|
||||
})
|
||||
.finally(() => {
|
||||
setSubmitting(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal opened onClose={closeModal} title={`Delete confirmation`}>
|
||||
<Text size="sm">
|
||||
Are you sure you want to delete user{" "}
|
||||
<Text span fw={700}>
|
||||
{props.data.name}
|
||||
</Text>
|
||||
? This action is irreversible.
|
||||
</Text>
|
||||
{/* Buttons */}
|
||||
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={closeModal}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
// leftSection={<TbDeviceFloppy size={20} />}
|
||||
type="submit"
|
||||
color="red"
|
||||
loading={isSubmitting}
|
||||
onClick={confirmAction}
|
||||
>
|
||||
Delete User
|
||||
</Button>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import DeleteModal from "./DeleteModal";
|
||||
|
||||
export default DeleteModal;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react'
|
||||
import { FormModal } from '..'
|
||||
import { UserFormData } from '@/features/dashboard/users/formSchemas/userFormDataSchema'
|
||||
|
||||
interface Props {
|
||||
data: UserFormData
|
||||
}
|
||||
|
||||
export default function DetailModal(props: Props) {
|
||||
return <FormModal readonly modalTitle='Detail User' data={props.data} />
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import DetailModal from "./DetailModal";
|
||||
|
||||
export default DetailModal;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react'
|
||||
import { FormModal } from '..'
|
||||
import { UserFormData } from '@/features/dashboard/users/formSchemas/userFormDataSchema'
|
||||
|
||||
interface Props {
|
||||
data: UserFormData
|
||||
}
|
||||
|
||||
export default function EditModal(props: Props) {
|
||||
return <FormModal modalTitle='Edit User' data={props.data} />
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import EditModal from "./EditModal";
|
||||
|
||||
export default EditModal;
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Modal,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { TbDeviceFloppy } from "react-icons/tb";
|
||||
import editUser from "@/features/dashboard/users/actions/editUser";
|
||||
import userFormDataSchema, {
|
||||
UserFormData,
|
||||
} from "@/features/dashboard/users/formSchemas/userFormDataSchema";
|
||||
import { showNotification } from "@/utils/notifications";
|
||||
import stringToColorHex from "@/utils/stringToColorHex";
|
||||
import { zodResolver } from "@mantine/form";
|
||||
|
||||
interface Props {
|
||||
readonly?: boolean;
|
||||
modalTitle: string;
|
||||
data: UserFormData;
|
||||
}
|
||||
|
||||
export default function FormModal(props: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
|
||||
const form = useForm<UserFormData>({
|
||||
initialValues: props.data,
|
||||
validate: zodResolver(userFormDataSchema),
|
||||
});
|
||||
|
||||
/**
|
||||
* Closes the modal. It won't close if a submission is in progress.
|
||||
*/
|
||||
const closeModal = () => {
|
||||
if (isSubmitting) return;
|
||||
router
|
||||
.replace("?")
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the form submission.
|
||||
*
|
||||
* @param data The data from the form.
|
||||
*/
|
||||
const handleSubmit = (data: UserFormData) => {
|
||||
setSubmitting(true);
|
||||
editUser(data)
|
||||
.then((response) => {
|
||||
if (response.success) {
|
||||
showNotification(response.message);
|
||||
router.replace("?")
|
||||
return;
|
||||
} else {
|
||||
if (response.errors) {
|
||||
form.setErrors(response.errors);
|
||||
return;
|
||||
}
|
||||
showNotification(response.message, "error");
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
//TODO: Handle Error
|
||||
})
|
||||
.finally(() => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened
|
||||
onClose={closeModal}
|
||||
title={<Title order={3}>{props.modalTitle}</Title>}
|
||||
scrollAreaComponent={ScrollArea.Autosize}
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack mt="sm" gap="lg" px="lg">
|
||||
{/* Avatar */}
|
||||
<Center>
|
||||
<Avatar
|
||||
color={stringToColorHex(props.data.id)}
|
||||
src={props.data.photoProfileUrl}
|
||||
size={120}
|
||||
>
|
||||
{props.data.name?.[0].toUpperCase()}
|
||||
</Avatar>
|
||||
</Center>
|
||||
|
||||
{/* ID */}
|
||||
<TextInput
|
||||
label="ID"
|
||||
readOnly
|
||||
variant="filled"
|
||||
{...form.getInputProps("id")}
|
||||
/>
|
||||
|
||||
{/* Name */}
|
||||
<TextInput
|
||||
data-autofocus
|
||||
label="Name"
|
||||
readOnly={props.readonly}
|
||||
disabled={isSubmitting}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
|
||||
{/* Email */}
|
||||
<TextInput
|
||||
label="Email"
|
||||
readOnly={props.readonly}
|
||||
disabled={isSubmitting}
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
|
||||
{/* Buttons */}
|
||||
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={closeModal}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
{!props.readonly && (
|
||||
<Button
|
||||
variant="filled"
|
||||
leftSection={<TbDeviceFloppy size={20} />}
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import FormModal from "./FormModal";
|
||||
|
||||
export default FormModal;
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export { default as DeleteModal } from "./DeleteModal"
|
||||
export { default as DetailModal } from "./DetailModal"
|
||||
export { default as EditModal } from "./EditModal"
|
||||
export { default as FormModal } from "./FormModal"
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import columns, { UserRow } from "./columns";
|
||||
import { Table, Text } from "@mantine/core";
|
||||
|
||||
interface Props {
|
||||
users: UserRow[];
|
||||
}
|
||||
|
||||
export default function UsersTable({ users }: Props) {
|
||||
const table = useReactTable({
|
||||
data: users,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
defaultColumn: {
|
||||
cell: (props) => <Text>{props.getValue() as React.ReactNode}</Text>,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Table verticalSpacing="xs" horizontalSpacing="xs">
|
||||
{/* Thead */}
|
||||
<Table.Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<Table.Tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<Table.Th
|
||||
key={header.id}
|
||||
style={{
|
||||
maxWidth: `${header.column.columnDef.maxSize}px`,
|
||||
width: `${header.getSize()}`,
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Thead>
|
||||
|
||||
{/* TBody */}
|
||||
<Table.Tbody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Tr key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Td
|
||||
key={cell.id}
|
||||
style={{
|
||||
maxWidth: `${cell.column.columnDef.maxSize}px`,
|
||||
}}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Td>
|
||||
))}
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import Link from "next/link";
|
||||
import { ActionIcon, Anchor, Avatar, Badge, Flex, Group, Text, Tooltip } from "@mantine/core";
|
||||
import { TbEye, TbPencil, TbTrash } from "react-icons/tb";
|
||||
import stringToColorHex from "@/utils/stringToColorHex";
|
||||
|
||||
export interface UserRow {
|
||||
id: string,
|
||||
name: string | null,
|
||||
email: string | null,
|
||||
photoUrl: string | null,
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<UserRow>()
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: "sequence",
|
||||
header: "#",
|
||||
cell: props => props.row.index + 1,
|
||||
size: 1
|
||||
}),
|
||||
|
||||
columnHelper.accessor('name', {
|
||||
header: "Name",
|
||||
cell: (props) => <Group>
|
||||
<Avatar color={stringToColorHex(props.row.original.id)} src={props.row.original.photoUrl} size={26}>{props.getValue()?.[0].toUpperCase()}</Avatar>
|
||||
<Text size="sm" fw={500}>{props.getValue()}</Text>
|
||||
</Group>
|
||||
}),
|
||||
|
||||
columnHelper.accessor('email', {
|
||||
header: "Email",
|
||||
cell: (props) => <Anchor href={`mailto:${props.getValue()}`} size="sm" component={Link}>{props.getValue()}</Anchor>
|
||||
}),
|
||||
|
||||
columnHelper.display({
|
||||
id: "status",
|
||||
header: "Status",
|
||||
cell: (props) => <Badge color="green">Active</Badge>
|
||||
}),
|
||||
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
header: "Actions",
|
||||
size: 10,
|
||||
meta: {
|
||||
className: "w-fit"
|
||||
},
|
||||
cell: (props) => <Flex gap="xs">
|
||||
|
||||
{/* Detail */}
|
||||
<Tooltip label="Detail">
|
||||
<ActionIcon variant="light" color="green" component={Link} href={`?detail=${props.row.original.id}`}>
|
||||
<TbEye />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Edit */}
|
||||
<Tooltip label="Edit">
|
||||
<ActionIcon variant="light" color="yellow" component={Link} href={`?edit=${props.row.original.id}`}>
|
||||
<TbPencil />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Delete */}
|
||||
<Tooltip label="Delete">
|
||||
<ActionIcon variant="light" color="red" component={Link} href={`?delete=${props.row.original.id}`}>
|
||||
<TbTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
})
|
||||
];
|
||||
|
||||
export default columns;
|
||||
|
|
@ -3,12 +3,15 @@
|
|||
import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
|
||||
import prisma from "@/db";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import userFormDataSchema, { UserFormData } from "../formSchemas/userFormSchema";
|
||||
import userFormDataSchema, {
|
||||
UserFormData,
|
||||
} from "../formSchemas/userFormSchema";
|
||||
import checkPermission from "@/modules/dashboard/services/checkPermission";
|
||||
import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
||||
import DashboardError from "@/modules/dashboard/errors/DashboardError";
|
||||
import handleCatch from "@/modules/dashboard/utils/handleCatch";
|
||||
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
|
||||
import hashPassword from "@/modules/auth/utils/hashPassword";
|
||||
|
||||
/**
|
||||
* Upserts a user based on the provided UserFormData.
|
||||
|
|
@ -34,32 +37,37 @@ export default async function upsertUser(
|
|||
const validatedFields = userFormDataSchema.safeParse(data);
|
||||
if (!validatedFields.success) {
|
||||
throw new DashboardError({
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors)
|
||||
})
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: mapObjectToFirstValue(
|
||||
validatedFields.error.flatten().fieldErrors
|
||||
),
|
||||
});
|
||||
}
|
||||
const userData = {
|
||||
id: validatedFields.data.id ? validatedFields.data.id : undefined,
|
||||
id: validatedFields.data.id ? validatedFields.data.id : undefined,
|
||||
name: validatedFields.data.name,
|
||||
photoProfile: validatedFields.data.photoProfileUrl ?? "",
|
||||
email: validatedFields.data.email
|
||||
photoProfile: validatedFields.data.photoProfileUrl ?? "",
|
||||
email: validatedFields.data.email,
|
||||
};
|
||||
const passwordHash = await hashPassword(validatedFields.data.password!);
|
||||
|
||||
// Database operation
|
||||
if (isInsert) {
|
||||
if (await prisma.user.findFirst({
|
||||
where: {
|
||||
email: userData.email
|
||||
}
|
||||
})){
|
||||
throw new DashboardError({
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: {
|
||||
email: "The user is already exists"
|
||||
}
|
||||
})
|
||||
}
|
||||
await prisma.user.create({ data: userData });
|
||||
if (
|
||||
await prisma.user.findFirst({
|
||||
where: {
|
||||
email: userData.email,
|
||||
},
|
||||
})
|
||||
) {
|
||||
throw new DashboardError({
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: {
|
||||
email: "The user is already exists",
|
||||
},
|
||||
});
|
||||
}
|
||||
await prisma.user.create({ data: { ...userData, passwordHash } });
|
||||
} else {
|
||||
await prisma.user.update({
|
||||
where: { id: validatedFields.data.id! },
|
||||
|
|
@ -78,6 +86,6 @@ export default async function upsertUser(
|
|||
}.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleCatch(error)
|
||||
return handleCatch(error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export interface UserFormData {
|
||||
id: string;
|
||||
id: string | undefined;
|
||||
name: string;
|
||||
photoProfileUrl: string;
|
||||
email: string;
|
||||
password: string | undefined
|
||||
}
|
||||
|
||||
const userFormDataSchema = z.object({
|
||||
id: z.string().nullable(),
|
||||
id: z.string().optional(),
|
||||
name: z.string(),
|
||||
photoProfileUrl: z.union([z.string(), z.null()]),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8).optional(),
|
||||
});
|
||||
|
||||
export default userFormDataSchema;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
Alert,
|
||||
Center,
|
||||
Avatar,
|
||||
PasswordInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
|
@ -55,6 +56,7 @@ export default function UserFormModal(props: ModalProps) {
|
|||
email: "",
|
||||
name: "",
|
||||
photoProfileUrl: "",
|
||||
password: "",
|
||||
},
|
||||
validate: zodResolver(userFormDataSchema),
|
||||
validateInputOnChange: false,
|
||||
|
|
@ -128,7 +130,7 @@ export default function UserFormModal(props: ModalProps) {
|
|||
onClose={closeModal}
|
||||
title={props.title}
|
||||
scrollAreaComponent={ScrollArea.Autosize}
|
||||
size="xl"
|
||||
size="md"
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack mt="sm" gap="lg" px="lg">
|
||||
|
|
@ -137,7 +139,7 @@ export default function UserFormModal(props: ModalProps) {
|
|||
<Skeleton visible={isFetching}>
|
||||
<Center>
|
||||
<Avatar
|
||||
color={stringToColorHex(form.values.id)}
|
||||
color={stringToColorHex(form.values.id ?? "")}
|
||||
src={form.values.photoProfileUrl}
|
||||
size={120}
|
||||
>
|
||||
|
|
@ -178,6 +180,16 @@ export default function UserFormModal(props: ModalProps) {
|
|||
/>
|
||||
</Skeleton>
|
||||
|
||||
{/* Password */}
|
||||
{!form.values.id && !isFetching && (
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
readOnly={props.readonly}
|
||||
disabled={isSubmitting}
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
|
||||
<Button
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user