Added permission
This commit is contained in:
parent
bae8a2aa3e
commit
dc3a707bb0
42
src/app/dashboard/(auth)/permissions/page.tsx
Normal file
42
src/app/dashboard/(auth)/permissions/page.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { Card, Stack, Title } from "@mantine/core";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
// import RolesTable from "./_tables/RolesTable/RolesTable";
|
||||
// import getRoles from "@/features/dashboard/roles/data/getRoles";
|
||||
import checkMultiplePermissions from "@/features/auth/tools/checkMultiplePermissions";
|
||||
import { PermissionTable } from "@/features/dashboard/permissions/tables";
|
||||
import getPermissions from "@/features/dashboard/permissions/data/getPermissions";
|
||||
|
||||
interface Props {
|
||||
searchParams: {
|
||||
detail?: string;
|
||||
edit?: string;
|
||||
delete?: string;
|
||||
create?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Permissions - Dashboard",
|
||||
};
|
||||
|
||||
export default async function RolesPage({ searchParams }: Props) {
|
||||
const permissions = await checkMultiplePermissions({
|
||||
create: "permission.create",
|
||||
readAll: "permission.readAll",
|
||||
read: "permission.read",
|
||||
update: "permission.update",
|
||||
delete: "permission.delete",
|
||||
});
|
||||
|
||||
const permissionData = await getPermissions()
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={1}>Permissions</Title>
|
||||
<Card>
|
||||
<PermissionTable permissions={permissions} permissionData={permissionData} />
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import { RoleFormData } from "@/features/dashboard/roles/formSchemas/RoleFormDat
|
|||
import { string } from "zod";
|
||||
import { DeleteModal } from "../../_modals";
|
||||
import { DeleteModalProps } from "../../_modals/DeleteModal/DeleteModal";
|
||||
import { DashboardTable } from "@/features/dashboard/components";
|
||||
|
||||
interface Props {
|
||||
permissions: Partial<CrudPermissions>;
|
||||
|
|
@ -114,60 +115,8 @@ export default function RolesTable(props: Props) {
|
|||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<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.length > 0 ? (
|
||||
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.Tr>
|
||||
<Table.Td colSpan={table.getAllColumns().length}>
|
||||
<Center>- No Data -</Center>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
<DashboardTable table={table} />
|
||||
|
||||
<FormModal {...modalProps} onClose={closeModal} />
|
||||
<DeleteModal
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React from "react";
|
|||
import RolesTable from "./_tables/RolesTable/RolesTable";
|
||||
import getRoles from "@/features/dashboard/roles/data/getRoles";
|
||||
import checkMultiplePermissions from "@/features/auth/tools/checkMultiplePermissions";
|
||||
import { unauthorized } from "@/features/dashboard/errors/DashboardError";
|
||||
|
||||
interface Props {
|
||||
searchParams: {
|
||||
|
|
@ -27,6 +28,8 @@ export default async function RolesPage({ searchParams }: Props) {
|
|||
delete: "role.delete",
|
||||
});
|
||||
|
||||
if (!permissions.readAll) unauthorized()
|
||||
|
||||
const roles = await getRoles();
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const allMenu: MenuItem[] = [
|
|||
children: [
|
||||
{ label: "Users", link: "/users" },
|
||||
{ label: "Roles", link: "/roles" },
|
||||
{ label: "Permissions", link: "#" },
|
||||
{ label: "Permissions", link: "/permissions" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
|
||||
import { Table, Center } from "@mantine/core"
|
||||
import { Table as ReactTable, flexRender } from "@tanstack/react-table";
|
||||
|
||||
interface Props<TData> {
|
||||
table: ReactTable<TData>
|
||||
}
|
||||
|
||||
export default function DashboardTable<T>({table}: Props<T>) {
|
||||
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.length > 0 ? (
|
||||
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.Tr>
|
||||
<Table.Td colSpan={table.getAllColumns().length}>
|
||||
<Center>- No Data -</Center>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./DashboardTable";
|
||||
3
src/features/dashboard/components/index.ts
Normal file
3
src/features/dashboard/components/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import DashboardTable from "./DashboardTable";
|
||||
|
||||
export { DashboardTable };
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
"use server";
|
||||
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import { handleCatch, unauthorized } from "../../errors/DashboardError";
|
||||
import ServerResponse from "@/types/Action";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export default async function deletePermission(id: string): Promise<ServerResponse> {
|
||||
try {
|
||||
if (!(await checkPermission("permission.delete"))) return unauthorized();
|
||||
const permission = await prisma.permission.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
|
||||
revalidatePath(".")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "The permission has been deleted successfully",
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
return handleCatch(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
"use server";
|
||||
|
||||
import { unauthorized } from "@/BaseError";
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
|
||||
export default async function getPermissionById(id: string) {
|
||||
if (!(await checkPermission("permission.read"))) unauthorized();
|
||||
|
||||
const permission = await prisma.permission.findFirst({
|
||||
where: { id },
|
||||
select: {
|
||||
code: true,
|
||||
description: true,
|
||||
id: true,
|
||||
isActive: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!permission) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Permission not found",
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Permission fetched successfully",
|
||||
data: permission,
|
||||
} as const;
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
"use server";
|
||||
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import permissionFormDataSchema, { PermissionFormData } from "../formSchemas/PermissionFormData";
|
||||
import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
|
||||
import prisma from "@/db";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import ServerResponse from "@/types/Action";
|
||||
import DashboardError, { handleCatch, unauthorized } from "../../errors/DashboardError";
|
||||
|
||||
/**
|
||||
* Upserts a permission based on the provided PermissionFormData.
|
||||
* If the permission already exists (determined by `id`), it updates the permission; otherwise, it creates a new permission.
|
||||
* Authorization checks are performed based on whether it's a create or update operation.
|
||||
*
|
||||
* @param data - The data for creating or updating the permission.
|
||||
* @returns An object containing the success status, message, and any errors.
|
||||
*/
|
||||
export default async function upsertPermission(
|
||||
data: PermissionFormData
|
||||
): Promise<ServerResponse> {
|
||||
try {
|
||||
const isInsert = !data.id;
|
||||
|
||||
// Authorization check
|
||||
const permissionType = isInsert ? "permission.create" : "permission.update";
|
||||
if (!(await checkPermission(permissionType))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
// Validate form data
|
||||
const validatedFields = permissionFormDataSchema.safeParse(data);
|
||||
if (!validatedFields.success) {
|
||||
throw new DashboardError({
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors)
|
||||
})
|
||||
}
|
||||
const permissionData = {
|
||||
code: validatedFields.data.code,
|
||||
description: validatedFields.data.description,
|
||||
name: validatedFields.data.name,
|
||||
isActive: validatedFields.data.isActive,
|
||||
};
|
||||
|
||||
// Database operation
|
||||
if (isInsert) {
|
||||
if (await prisma.permission.findFirst({
|
||||
where: {
|
||||
code: permissionData.code
|
||||
}
|
||||
})){
|
||||
throw new DashboardError({
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: {
|
||||
code: "The code is already exists"
|
||||
}
|
||||
})
|
||||
}
|
||||
await prisma.permission.create({ data: permissionData });
|
||||
} else {
|
||||
await prisma.permission.update({
|
||||
where: { id: validatedFields.data.id! },
|
||||
data: permissionData,
|
||||
});
|
||||
}
|
||||
|
||||
// Revalidate the cache
|
||||
revalidatePath(".");
|
||||
|
||||
// Return success message
|
||||
return {
|
||||
success: true,
|
||||
message: `Permission ${validatedFields.data.name} has been successfully ${
|
||||
isInsert ? "created" : "updated"
|
||||
}.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleCatch(error)
|
||||
}
|
||||
}
|
||||
48
src/features/dashboard/permissions/data/getPermissions.ts
Normal file
48
src/features/dashboard/permissions/data/getPermissions.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { unauthorized } from "@/BaseError";
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import "server-only";
|
||||
|
||||
/**
|
||||
* Retrieves all permissions along with the count of associated permissions and users.
|
||||
* Authorization check is performed for the operation.
|
||||
*
|
||||
* @returns An array of permission objects each including details and counts of related permissions and users.
|
||||
*/
|
||||
export default async function getPermissions() {
|
||||
// Authorization check
|
||||
if (!(await checkPermission("permissions.getAll"))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch permissions from the database
|
||||
const permissions = await prisma.permission.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
roles: true,
|
||||
directUsers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Transform the data into the desired format
|
||||
return permissions.map(
|
||||
({ id, code, name, description, isActive, _count }) => ({
|
||||
id,
|
||||
code,
|
||||
name,
|
||||
description,
|
||||
isActive,
|
||||
roleCount: _count.roles,
|
||||
//User count counts only direct user
|
||||
userCount: _count.directUsers,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error retrieving permissions", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { z } from "zod"
|
||||
|
||||
export interface PermissionFormData {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const permissionFormDataSchema = z.object({
|
||||
id: z.string().nullable(),
|
||||
name: z.string().min(1),
|
||||
code: z.string().min(1),
|
||||
description: z.string(),
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
|
||||
export default permissionFormDataSchema;
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
"use client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Modal,
|
||||
ScrollArea,
|
||||
Text,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title,
|
||||
Alert,
|
||||
} from "@mantine/core";
|
||||
import { showNotification } from "@/utils/notifications";
|
||||
import deletePermission from "@/features/dashboard/permissions/actions/deletePermission";
|
||||
import withErrorHandling from "@/features/dashboard/utils/withServerAction";
|
||||
import { error } from "console";
|
||||
import DashboardError from "@/features/dashboard/errors/DashboardError";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export interface DeleteModalProps {
|
||||
data?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function DeleteModal(props: DeleteModalProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
/**
|
||||
* Closes the modal. It won't close if a submission is in progress.
|
||||
*/
|
||||
const closeModal = () => {
|
||||
if (isSubmitting) return;
|
||||
setErrorMessage("")
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const confirmAction = () => {
|
||||
if (!props.data?.id) return;
|
||||
setSubmitting(true);
|
||||
|
||||
withErrorHandling(() => deletePermission(props.data!.id))
|
||||
.then((response) => {
|
||||
showNotification(
|
||||
response.message ?? "Permission deleted successfully"
|
||||
);
|
||||
setSubmitting(false);
|
||||
props.onClose()
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e instanceof DashboardError){
|
||||
setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`)
|
||||
}
|
||||
else if (e instanceof Error) {
|
||||
setErrorMessage(`ERROR: ${e.message}`)
|
||||
} else {
|
||||
setErrorMessage(`Unkown error is occured. Please contact administrator`)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setSubmitting(false)
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={!!props.data}
|
||||
onClose={closeModal}
|
||||
title={`Delete confirmation`}
|
||||
>
|
||||
<Text size="sm">
|
||||
Are you sure you want to delete permission{" "}
|
||||
<Text span fw={700}>
|
||||
{props.data?.name}
|
||||
</Text>
|
||||
? This action is irreversible.
|
||||
</Text>
|
||||
|
||||
{errorMessage && <Alert color="red">{errorMessage}</Alert>}
|
||||
{/* 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 Permission
|
||||
</Button>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./DeleteModal";
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import DashboardError from "@/features/dashboard/errors/DashboardError";
|
||||
import getPermissionById from "@/features/dashboard/permissions/actions/getPermissionById";
|
||||
import upsertPermission from "@/features/dashboard/permissions/actions/upsertPermission";
|
||||
import permissionFormDataSchema, {
|
||||
PermissionFormData,
|
||||
} from "@/features/dashboard/permissions/formSchemas/PermissionFormData";
|
||||
import withErrorHandling from "@/features/dashboard/utils/withServerAction";
|
||||
import { showNotification } from "@/utils/notifications";
|
||||
import {
|
||||
Flex,
|
||||
Modal,
|
||||
Stack,
|
||||
Switch,
|
||||
TextInput,
|
||||
Textarea,
|
||||
Button,
|
||||
ScrollArea,
|
||||
Checkbox,
|
||||
Skeleton,
|
||||
Fieldset,
|
||||
Alert,
|
||||
} from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { TbDeviceFloppy } from "react-icons/tb";
|
||||
|
||||
export interface ModalProps {
|
||||
title: string;
|
||||
readonly?: boolean;
|
||||
id?: string;
|
||||
opened: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component for rendering a modal with a form to create or edit a permission.
|
||||
*
|
||||
* @param props - The props for the component.
|
||||
* @returns The rendered element.
|
||||
*/
|
||||
export default function FormModal(props: ModalProps) {
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [isFetching, setFetching] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const form = useForm<PermissionFormData>({
|
||||
initialValues: {
|
||||
code: "",
|
||||
description: "",
|
||||
id: "",
|
||||
isActive: false,
|
||||
name: "",
|
||||
},
|
||||
validate: zodResolver(permissionFormDataSchema),
|
||||
validateInputOnChange: false,
|
||||
onValuesChange: (values) => {
|
||||
console.log(values);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetches permission data by ID and populates the form if the modal is opened and an ID is provided.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!props.opened || !props.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFetching(true);
|
||||
getPermissionById(props.id)
|
||||
.then((response) => {
|
||||
if (response.success) {
|
||||
const data = response.data;
|
||||
form.setValues({
|
||||
code: data.code,
|
||||
description: data.description,
|
||||
id: data.id,
|
||||
isActive: data.isActive,
|
||||
name: data.name,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
//TODO: Handle error
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
setFetching(false);
|
||||
});
|
||||
}, [props.opened, props.id]);
|
||||
|
||||
const closeModal = () => {
|
||||
props.onClose ? props.onClose() : router.replace("?");
|
||||
};
|
||||
|
||||
const handleSubmit = (values: PermissionFormData) => {
|
||||
setSubmitting(true);
|
||||
withErrorHandling(() => upsertPermission(values))
|
||||
.then((response) => {
|
||||
showNotification(response.message!, "success");
|
||||
closeModal();
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e instanceof DashboardError) {
|
||||
if (e.errorCode === "INVALID_FORM_DATA") {
|
||||
form.setErrors(e.formErrors ?? {});
|
||||
} else {
|
||||
setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`);
|
||||
}
|
||||
} else if (e instanceof Error) {
|
||||
setErrorMessage(`ERROR: ${e.message}`);
|
||||
} else {
|
||||
setErrorMessage(
|
||||
`Unkown error is occured. Please contact administrator`
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={props.opened}
|
||||
onClose={closeModal}
|
||||
title={props.title}
|
||||
scrollAreaComponent={ScrollArea.Autosize}
|
||||
size="xl"
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack mt="sm" gap="lg" px="lg">
|
||||
{errorMessage && <Alert color="red">{errorMessage}</Alert>}
|
||||
{/* ID */}
|
||||
{form.values.id ? (
|
||||
<TextInput
|
||||
label="ID"
|
||||
readOnly
|
||||
variant="filled"
|
||||
{...form.getInputProps("id")}
|
||||
/>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
{/* Code */}
|
||||
<Skeleton visible={isFetching}>
|
||||
<TextInput
|
||||
data-autofocus
|
||||
label="Code"
|
||||
readOnly={props.readonly}
|
||||
disabled={isSubmitting}
|
||||
{...form.getInputProps("code")}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
{/* Name */}
|
||||
<Skeleton visible={isFetching}>
|
||||
<TextInput
|
||||
label="Name"
|
||||
readOnly={props.readonly}
|
||||
disabled={isSubmitting}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
{/* Description */}
|
||||
<Skeleton visible={isFetching}>
|
||||
<Textarea
|
||||
label="Description"
|
||||
readOnly={props.readonly}
|
||||
disabled={isSubmitting}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
<Skeleton visible={isFetching}>
|
||||
<Checkbox
|
||||
label="Active"
|
||||
labelPosition="right"
|
||||
{...form.getInputProps("isActive", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./FormModal";
|
||||
4
src/features/dashboard/permissions/modals/index.ts
Normal file
4
src/features/dashboard/permissions/modals/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import DeleteModal from "./DeleteModal";
|
||||
import FormModal from "./FormModal";
|
||||
|
||||
export { FormModal, DeleteModal };
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
"use client";
|
||||
import { Table, Text, Flex, Button, Center } from "@mantine/core";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import React, { useState } from "react";
|
||||
import CrudPermissions from "@/features/auth/types/CrudPermissions";
|
||||
import { TbPlus } from "react-icons/tb";
|
||||
import { PermissionFormData } from "@/features/dashboard/permissions/formSchemas/PermissionFormData";
|
||||
import { string } from "zod";
|
||||
import { DashboardTable } from "@/features/dashboard/components";
|
||||
import getPermissions from "../../data/getPermissions";
|
||||
import FormModal, { ModalProps } from "../../modals/FormModal/FormModal";
|
||||
import DeleteModal, { DeleteModalProps } from "../../modals/DeleteModal/DeleteModal";
|
||||
import createColumns from "./_columns";
|
||||
|
||||
interface Props {
|
||||
permissions: Partial<CrudPermissions>;
|
||||
permissionData: Awaited<ReturnType<typeof getPermissions>>;
|
||||
}
|
||||
|
||||
export default function PermissionsTable(props: Props) {
|
||||
const [modalProps, setModalProps] = useState<ModalProps>({
|
||||
opened: false,
|
||||
title: "",
|
||||
});
|
||||
|
||||
const [deleteModalProps, setDeleteModalProps] = useState<
|
||||
Omit<DeleteModalProps, "onClose">
|
||||
>({
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: props.permissionData,
|
||||
columns: createColumns({
|
||||
permissions: props.permissions,
|
||||
actions: {
|
||||
detail: (id: string) => openFormModal("detail", id),
|
||||
edit: (id: string) => openFormModal("edit", id),
|
||||
delete: (id: string, name: string) => openDeleteModal(id, name),
|
||||
},
|
||||
}),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
defaultColumn: {
|
||||
cell: (props) => <Text>{props.getValue() as React.ReactNode}</Text>,
|
||||
},
|
||||
});
|
||||
|
||||
const openFormModal = (type: "create" | "edit" | "detail", id?: string) => {
|
||||
const openCreateModal = () => {
|
||||
setModalProps({
|
||||
id,
|
||||
opened: true,
|
||||
title: "Create new permission",
|
||||
});
|
||||
};
|
||||
|
||||
const openDetailModal = () => {
|
||||
setModalProps({
|
||||
id,
|
||||
opened: true,
|
||||
title: "Permission detail",
|
||||
readonly: true,
|
||||
});
|
||||
};
|
||||
|
||||
const openEditModal = () => {
|
||||
setModalProps({
|
||||
id,
|
||||
opened: true,
|
||||
title: "Edit permission",
|
||||
});
|
||||
};
|
||||
|
||||
type === "create"
|
||||
? openCreateModal()
|
||||
: type === "detail"
|
||||
? openDetailModal()
|
||||
: openEditModal();
|
||||
};
|
||||
|
||||
const openDeleteModal = (id: string, name: string) => {
|
||||
setDeleteModalProps({
|
||||
data: {
|
||||
id,
|
||||
name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalProps({
|
||||
id: "",
|
||||
opened: false,
|
||||
title: "",
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: Add view when data is empty
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justify="flex-end">
|
||||
{props.permissions.create && (
|
||||
<Button
|
||||
leftSection={<TbPlus />}
|
||||
onClick={() => openFormModal("create")}
|
||||
>
|
||||
New Permission
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<DashboardTable table={table} />
|
||||
|
||||
<FormModal {...modalProps} onClose={closeModal} />
|
||||
<DeleteModal
|
||||
{...deleteModalProps}
|
||||
onClose={() => setDeleteModalProps({})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { Badge, Flex } from "@mantine/core";
|
||||
import createActionButtons from "@/features/dashboard/utils/createActionButtons";
|
||||
import CrudPermissions from "@/features/auth/types/CrudPermissions";
|
||||
import { TbEye, TbPencil, TbTrash } from "react-icons/tb";
|
||||
|
||||
export interface PermissionRow {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
roleCount: number;
|
||||
userCount: number;
|
||||
}
|
||||
|
||||
interface ColumnOptions {
|
||||
permissions: Partial<CrudPermissions>;
|
||||
actions: {
|
||||
detail: (id: string) => void;
|
||||
edit: (id: string) => void;
|
||||
delete: (id: string, name: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const createColumns = (options: ColumnOptions) => {
|
||||
const columnHelper = createColumnHelper<PermissionRow>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor("id", {
|
||||
id: "sequence",
|
||||
header: "#",
|
||||
cell: (props) => props.row.index + 1,
|
||||
}),
|
||||
|
||||
columnHelper.accessor("code", {
|
||||
header: "Code",
|
||||
}),
|
||||
|
||||
columnHelper.accessor("name", {
|
||||
header: "Name",
|
||||
}),
|
||||
|
||||
columnHelper.accessor("isActive", {
|
||||
header: "Status",
|
||||
cell: (props) => {
|
||||
props.getValue() ? (
|
||||
<Badge color="green">Enabled</Badge>
|
||||
) : (
|
||||
<Badge color="orange">Disabled</Badge>
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
columnHelper.accessor("roleCount", {
|
||||
header: "Roles",
|
||||
}),
|
||||
|
||||
columnHelper.accessor("userCount", {
|
||||
header: "Users",
|
||||
}),
|
||||
|
||||
columnHelper.display({
|
||||
id: "Actions",
|
||||
header: "Actions",
|
||||
cell: (props) => (
|
||||
<Flex gap="xs">
|
||||
{createActionButtons([
|
||||
{
|
||||
label: "Detail",
|
||||
permission: options.permissions.read,
|
||||
action: () =>
|
||||
options.actions.detail(props.row.original.id),
|
||||
color: "green",
|
||||
icon: <TbEye />,
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
permission: options.permissions.update,
|
||||
action: () =>
|
||||
options.actions.edit(props.row.original.id),
|
||||
color: "yellow",
|
||||
icon: <TbPencil />,
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
permission: options.permissions.delete,
|
||||
action: () =>
|
||||
options.actions.delete(
|
||||
props.row.original.id,
|
||||
props.row.original.name
|
||||
),
|
||||
color: "red",
|
||||
icon: <TbTrash />,
|
||||
},
|
||||
])}
|
||||
</Flex>
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
export default createColumns;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./PermissionTable"
|
||||
3
src/features/dashboard/permissions/tables/index.ts
Normal file
3
src/features/dashboard/permissions/tables/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import PermissionTable from "./PermissionTable";
|
||||
|
||||
export { PermissionTable };
|
||||
Loading…
Reference in New Issue
Block a user