added access variable to exams soo we can distinguish private, public and confidential exams and also bugfixes and improvements
This commit is contained in:
@@ -1,287 +1,394 @@
|
||||
import {useMemo, useState} from "react";
|
||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||
import { useMemo, useState } from "react";
|
||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
||||
import useExams from "@/hooks/useExams";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import {Module} from "@/interfaces";
|
||||
import {Exam} from "@/interfaces/exam";
|
||||
import {User} from "@/interfaces/user";
|
||||
import { Module } from "@/interfaces";
|
||||
import { Exam } from "@/interfaces/exam";
|
||||
import { User } from "@/interfaces/user";
|
||||
import useExamStore from "@/stores/exam";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
import {countExercises} from "@/utils/moduleUtils";
|
||||
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||
import { getExamById } from "@/utils/exams";
|
||||
import { countExercises } from "@/utils/moduleUtils";
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import {capitalize, uniq} from "lodash";
|
||||
import {useRouter} from "next/router";
|
||||
import {BsBan, BsCheck, BsCircle, BsPencil, BsTrash, BsUpload, BsX} from "react-icons/bs";
|
||||
import {toast} from "react-toastify";
|
||||
import {useListSearch} from "@/hooks/useListSearch";
|
||||
import { capitalize } from "lodash";
|
||||
import { useRouter } from "next/router";
|
||||
import { BsCheck, BsPencil, BsTrash, BsUpload, BsX } from "react-icons/bs";
|
||||
import { toast } from "react-toastify";
|
||||
import { useListSearch } from "@/hooks/useListSearch";
|
||||
import Modal from "@/components/Modal";
|
||||
import {checkAccess} from "@/utils/permissions";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
||||
import Button from "@/components/Low/Button";
|
||||
import {EntityWithRoles} from "@/interfaces/entity";
|
||||
import {BiEdit} from "react-icons/bi";
|
||||
import {findBy, mapBy} from "@/utils";
|
||||
import {getUserName} from "@/utils/users";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { BiEdit } from "react-icons/bi";
|
||||
import { findBy, mapBy } from "@/utils";
|
||||
|
||||
const searchFields = [["module"], ["id"], ["createdBy"]];
|
||||
|
||||
const CLASSES: {[key in Module]: string} = {
|
||||
reading: "text-ielts-reading",
|
||||
listening: "text-ielts-listening",
|
||||
speaking: "text-ielts-speaking",
|
||||
writing: "text-ielts-writing",
|
||||
level: "text-ielts-level",
|
||||
const CLASSES: { [key in Module]: string } = {
|
||||
reading: "text-ielts-reading",
|
||||
listening: "text-ielts-listening",
|
||||
speaking: "text-ielts-speaking",
|
||||
writing: "text-ielts-writing",
|
||||
level: "text-ielts-level",
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<Exam>();
|
||||
|
||||
export default function ExamList({user, entities}: {user: User; entities: EntityWithRoles[]}) {
|
||||
const [selectedExam, setSelectedExam] = useState<Exam>();
|
||||
export default function ExamList({
|
||||
user,
|
||||
entities,
|
||||
}: {
|
||||
user: User;
|
||||
entities: EntityWithRoles[];
|
||||
}) {
|
||||
const [selectedExam, setSelectedExam] = useState<Exam>();
|
||||
|
||||
const {exams, reload} = useExams();
|
||||
const {users} = useUsers();
|
||||
const canViewConfidentialEntities = useMemo(
|
||||
() =>
|
||||
mapBy(
|
||||
findAllowedEntities(user, entities, "view_confidential_exams"),
|
||||
"id"
|
||||
),
|
||||
[user, entities]
|
||||
);
|
||||
|
||||
const filteredExams = useMemo(
|
||||
() =>
|
||||
exams.filter((e) => {
|
||||
if (!e.private) return true;
|
||||
return (e.entities || []).some((ent) => mapBy(user.entities, "id").includes(ent));
|
||||
}),
|
||||
[exams, user?.entities],
|
||||
);
|
||||
const { exams, reload, isLoading } = useExams();
|
||||
const { users } = useUsers();
|
||||
// Pass this permission filter to the backend later
|
||||
const filteredExams = useMemo(
|
||||
() =>
|
||||
["admin", "developer"].includes(user.type)
|
||||
? exams
|
||||
: exams.filter((item) => {
|
||||
if (
|
||||
item.access === "confidential" &&
|
||||
!canViewConfidentialEntities.find((x) =>
|
||||
(item.entities ?? []).includes(x)
|
||||
)
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
}),
|
||||
[canViewConfidentialEntities, exams, user.type]
|
||||
);
|
||||
|
||||
const parsedExams = useMemo(() => {
|
||||
return filteredExams.map((exam) => {
|
||||
if (exam.createdBy) {
|
||||
const user = users.find((u) => u.id === exam.createdBy);
|
||||
if (!user) return exam;
|
||||
const parsedExams = useMemo(() => {
|
||||
return filteredExams.map((exam) => {
|
||||
if (exam.createdBy) {
|
||||
const user = users.find((u) => u.id === exam.createdBy);
|
||||
if (!user) return exam;
|
||||
|
||||
return {
|
||||
...exam,
|
||||
createdBy: user.type === "developer" ? "system" : user.name,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...exam,
|
||||
createdBy: user.type === "developer" ? "system" : user.name,
|
||||
};
|
||||
}
|
||||
|
||||
return exam;
|
||||
});
|
||||
}, [filteredExams, users]);
|
||||
return exam;
|
||||
});
|
||||
}, [filteredExams, users]);
|
||||
|
||||
const {rows: filteredRows, renderSearch} = useListSearch<Exam>(searchFields, parsedExams);
|
||||
const { rows: filteredRows, renderSearch } = useListSearch<Exam>(
|
||||
searchFields,
|
||||
parsedExams
|
||||
);
|
||||
|
||||
const dispatch = useExamStore((state) => state.dispatch);
|
||||
const dispatch = useExamStore((state) => state.dispatch);
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter();
|
||||
|
||||
const loadExam = async (module: Module, examId: string) => {
|
||||
const exam = await getExamById(module, examId.trim());
|
||||
if (!exam) {
|
||||
toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", {
|
||||
toastId: "invalid-exam-id",
|
||||
});
|
||||
const loadExam = async (module: Module, examId: string) => {
|
||||
const exam = await getExamById(module, examId.trim());
|
||||
if (!exam) {
|
||||
toast.error(
|
||||
"Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID",
|
||||
{
|
||||
toastId: "invalid-exam-id",
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
dispatch({type: "INIT_EXAM", payload: {exams: [exam], modules: [module]}});
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
type: "INIT_EXAM",
|
||||
payload: { exams: [exam], modules: [module] },
|
||||
});
|
||||
|
||||
router.push("/exam");
|
||||
};
|
||||
router.push("/exam");
|
||||
};
|
||||
|
||||
const privatizeExam = async (exam: Exam) => {
|
||||
if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return;
|
||||
/*
|
||||
const privatizeExam = async (exam: Exam) => {
|
||||
if (
|
||||
!confirm(
|
||||
`Are you sure you want to make this ${capitalize(exam.module)} exam ${
|
||||
exam.access
|
||||
}?`
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
axios
|
||||
.patch(`/api/exam/${exam.module}/${exam.id}`, {private: !exam.private})
|
||||
.then(() => toast.success(`Updated the "${exam.id}" exam`))
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Exam not found!");
|
||||
return;
|
||||
}
|
||||
axios
|
||||
.patch(`/api/exam/${exam.module}/${exam.id}`, { private: !exam.private })
|
||||
.then(() => toast.success(`Updated the "${exam.id}" exam`))
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Exam not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to update this exam!");
|
||||
return;
|
||||
}
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to update this exam!");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
*/
|
||||
|
||||
const deleteExam = async (exam: Exam) => {
|
||||
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
|
||||
const deleteExam = async (exam: Exam) => {
|
||||
if (
|
||||
!confirm(
|
||||
`Are you sure you want to delete this ${capitalize(exam.module)} exam?`
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
axios
|
||||
.delete(`/api/exam/${exam.module}/${exam.id}`)
|
||||
.then(() => toast.success(`Deleted the "${exam.id}" exam`))
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Exam not found!");
|
||||
return;
|
||||
}
|
||||
axios
|
||||
.delete(`/api/exam/${exam.module}/${exam.id}`)
|
||||
.then(() => toast.success(`Deleted the "${exam.id}" exam`))
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Exam not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to delete this exam!");
|
||||
return;
|
||||
}
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to delete this exam!");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
|
||||
const getTotalExercises = (exam: Exam) => {
|
||||
if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") {
|
||||
return countExercises(exam.parts.flatMap((x) => x.exercises));
|
||||
}
|
||||
const getTotalExercises = (exam: Exam) => {
|
||||
if (
|
||||
exam.module === "reading" ||
|
||||
exam.module === "listening" ||
|
||||
exam.module === "level"
|
||||
) {
|
||||
return countExercises((exam.parts ?? []).flatMap((x) => x.exercises));
|
||||
}
|
||||
|
||||
return countExercises(exam.exercises);
|
||||
};
|
||||
return countExercises(exam.exercises);
|
||||
};
|
||||
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("id", {
|
||||
header: "ID",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("module", {
|
||||
header: "Module",
|
||||
cell: (info) => <span className={CLASSES[info.getValue()]}>{capitalize(info.getValue())}</span>,
|
||||
}),
|
||||
columnHelper.accessor((x) => getTotalExercises(x), {
|
||||
header: "Exercises",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("minTimer", {
|
||||
header: "Timer",
|
||||
cell: (info) => <>{info.getValue()} minute(s)</>,
|
||||
}),
|
||||
columnHelper.accessor("private", {
|
||||
header: "Private",
|
||||
cell: (info) => <span className="w-full flex items-center justify-center">{!info.getValue() ? <BsX /> : <BsCheck />}</span>,
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
header: "Created At",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
if (value) {
|
||||
return new Date(value).toLocaleDateString();
|
||||
}
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("id", {
|
||||
header: "ID",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("module", {
|
||||
header: "Module",
|
||||
cell: (info) => (
|
||||
<span className={CLASSES[info.getValue()]}>
|
||||
{capitalize(info.getValue())}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor((x) => getTotalExercises(x), {
|
||||
header: "Exercises",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("minTimer", {
|
||||
header: "Timer",
|
||||
cell: (info) => <>{info.getValue()} minute(s)</>,
|
||||
}),
|
||||
columnHelper.accessor("access", {
|
||||
header: "Access",
|
||||
cell: (info) => <span>{capitalize(info.getValue())}</span>,
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
header: "Created At",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
if (value) {
|
||||
return new Date(value).toLocaleDateString();
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("createdBy", {
|
||||
header: "Created By",
|
||||
cell: (info) => (!info.getValue() ? "System" : findBy(users, "id", info.getValue())?.name || "N/A"),
|
||||
}),
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({row}: {row: {original: Exam}}) => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{(row.original.owners?.includes(user.id) || checkAccess(user, ["admin", "developer"])) && (
|
||||
<>
|
||||
<button
|
||||
data-tip={row.original.private ? "Set as public" : "Set as private"}
|
||||
onClick={async () => await privatizeExam(row.original)}
|
||||
className="cursor-pointer tooltip">
|
||||
{row.original.private ? <BsCircle /> : <BsBan />}
|
||||
</button>
|
||||
{checkAccess(user, ["admin", "developer", "mastercorporate"]) && (
|
||||
<button data-tip="Edit exam" onClick={() => setSelectedExam(row.original)} className="cursor-pointer tooltip">
|
||||
<BsPencil />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
data-tip="Load exam"
|
||||
className="cursor-pointer tooltip"
|
||||
onClick={async () => await loadExam(row.original.module, row.original.id)}>
|
||||
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</button>
|
||||
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
||||
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteExam(row.original)}>
|
||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("createdBy", {
|
||||
header: "Created By",
|
||||
cell: (info) =>
|
||||
!info.getValue()
|
||||
? "System"
|
||||
: findBy(users, "id", info.getValue())?.name || "N/A",
|
||||
}),
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({ row }: { row: { original: Exam } }) => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{(row.original.owners?.includes(user.id) ||
|
||||
checkAccess(user, ["admin", "developer"])) && (
|
||||
<>
|
||||
{checkAccess(user, [
|
||||
"admin",
|
||||
"developer",
|
||||
"mastercorporate",
|
||||
]) && (
|
||||
<button
|
||||
data-tip="Edit exam"
|
||||
onClick={() => setSelectedExam(row.original)}
|
||||
className="cursor-pointer tooltip"
|
||||
>
|
||||
<BsPencil />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
data-tip="Load exam"
|
||||
className="cursor-pointer tooltip"
|
||||
onClick={async () =>
|
||||
await loadExam(row.original.module, row.original.id)
|
||||
}
|
||||
>
|
||||
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</button>
|
||||
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
||||
<div
|
||||
data-tip="Delete"
|
||||
className="cursor-pointer tooltip"
|
||||
onClick={() => deleteExam(row.original)}
|
||||
>
|
||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredRows,
|
||||
columns: defaultColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
const table = useReactTable({
|
||||
data: filteredRows,
|
||||
columns: defaultColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleExamEdit = () => {
|
||||
router.push(`/generation?id=${selectedExam!.id}&module=${selectedExam!.module}`);
|
||||
};
|
||||
const handleExamEdit = () => {
|
||||
router.push(
|
||||
`/generation?id=${selectedExam!.id}&module=${selectedExam!.module}`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full h-full">
|
||||
{renderSearch()}
|
||||
<Modal isOpen={!!selectedExam} onClose={() => setSelectedExam(undefined)} maxWidth="max-w-xl">
|
||||
{!!selectedExam ? (
|
||||
<>
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<BiEdit className="w-5 h-5 text-gray-600" />
|
||||
<span className="text-gray-600 font-medium">Ready to Edit</span>
|
||||
</div>
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full h-full">
|
||||
{renderSearch()}
|
||||
<Modal
|
||||
isOpen={!!selectedExam}
|
||||
onClose={() => setSelectedExam(undefined)}
|
||||
maxWidth="max-w-xl"
|
||||
>
|
||||
{!!selectedExam ? (
|
||||
<>
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<BiEdit className="w-5 h-5 text-gray-600" />
|
||||
<span className="text-gray-600 font-medium">
|
||||
Ready to Edit
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-3">
|
||||
<p className="font-medium mb-1">Exam ID: {selectedExam.id}</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-3">
|
||||
<p className="font-medium mb-1">Exam ID: {selectedExam.id}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-500 text-sm">Click 'Next' to proceed to the exam editor.</p>
|
||||
</div>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Click 'Next' to proceed to the exam editor.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-4 mt-8">
|
||||
<Button color="purple" variant="outline" onClick={() => setSelectedExam(undefined)} className="w-32">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="purple" onClick={handleExamEdit} className="w-32 text-white flex items-center justify-center gap-2">
|
||||
Proceed
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/*<ExamOwnerSelector options={filteredCorporates} exam={selectedExam} onSave={(owners) => updateExam(selectedExam, { owners })} />*/}
|
||||
</>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</Modal>
|
||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th className="p-4 text-left" key={header.id}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="px-2">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td className="px-4 py-2" key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
<div className="flex justify-between gap-4 mt-8">
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => setSelectedExam(undefined)}
|
||||
className="w-32"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={handleExamEdit}
|
||||
className="w-32 text-white flex items-center justify-center gap-2"
|
||||
>
|
||||
Proceed
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/*<ExamOwnerSelector options={filteredCorporates} exam={selectedExam} onSave={(owners) => updateExam(selectedExam, { owners })} />*/}
|
||||
</>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</Modal>
|
||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th className="p-4 text-left" key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="px-2">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr
|
||||
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
||||
key={row.id}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td className="px-4 py-2" key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{isLoading ? (
|
||||
<div className="min-h-screen flex justify-center items-start">
|
||||
<span className="loading loading-infinity w-32" />
|
||||
</div>
|
||||
) : (
|
||||
filteredRows.length === 0 && (
|
||||
<div className="w-full flex justify-center items-start">
|
||||
<span className="text-xl text-gray-500">No data found...</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user