Added the permission to update the privacy of an exam
This commit is contained in:
@@ -1,33 +1,32 @@
|
||||
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 { Type, 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 clsx from "clsx";
|
||||
import { capitalize, uniq } from "lodash";
|
||||
import { useRouter } from "next/router";
|
||||
import { BsBan, BsBanFill, BsCheck, BsCircle, BsPencil, BsStop, BsTrash, BsUpload, BsX } from "react-icons/bs";
|
||||
import { toast } from "react-toastify";
|
||||
import { useListSearch } from "@/hooks/useListSearch";
|
||||
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 Modal from "@/components/Modal";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import {checkAccess} from "@/utils/permissions";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import Button from "@/components/Low/Button";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { FiEdit, FiArrowRight } from 'react-icons/fi';
|
||||
import { HiArrowRight } from "react-icons/hi";
|
||||
import { BiEdit } from "react-icons/bi";
|
||||
import {EntityWithRoles} from "@/interfaces/entity";
|
||||
import {BiEdit} from "react-icons/bi";
|
||||
import {findBy, mapBy} from "@/utils";
|
||||
import {getUserName} from "@/utils/users";
|
||||
|
||||
const searchFields = [["module"], ["id"], ["createdBy"]];
|
||||
|
||||
const CLASSES: { [key in Module]: string } = {
|
||||
const CLASSES: {[key in Module]: string} = {
|
||||
reading: "text-ielts-reading",
|
||||
listening: "text-ielts-listening",
|
||||
speaking: "text-ielts-speaking",
|
||||
@@ -37,45 +36,20 @@ const CLASSES: { [key in Module]: string } = {
|
||||
|
||||
const columnHelper = createColumnHelper<Exam>();
|
||||
|
||||
const ExamOwnerSelector = ({ options, exam, onSave }: { options: User[]; exam: Exam; onSave: (owners: string[]) => void }) => {
|
||||
const [owners, setOwners] = useState(exam.owners || []);
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<div className="grid grid-cols-4 mt-4">
|
||||
{options.map((c) => (
|
||||
<Button
|
||||
variant={owners.includes(c.id) ? "solid" : "outline"}
|
||||
onClick={() => setOwners((prev) => (prev.includes(c.id) ? prev.filter((x) => x !== c.id) : [...prev, c.id]))}
|
||||
className="max-w-[200px] w-full"
|
||||
key={c.id}>
|
||||
{c.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={() => onSave(owners)} className="w-full max-w-[200px] self-end">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function ExamList({ user, entities }: { user: User; entities: EntityWithRoles[]; }) {
|
||||
export default function ExamList({user, entities}: {user: User; entities: EntityWithRoles[]}) {
|
||||
const [selectedExam, setSelectedExam] = useState<Exam>();
|
||||
|
||||
const { exams, reload } = useExams();
|
||||
const { users } = useUsers();
|
||||
const { groups } = useGroups({ admin: user?.id, userType: user?.type });
|
||||
const {exams, reload} = useExams();
|
||||
const {users} = useUsers();
|
||||
|
||||
const filteredExams = useMemo(() => exams.filter((e) => {
|
||||
if (!e.private) return true
|
||||
return (e.owners || []).includes(user?.id || "")
|
||||
}), [exams, user?.id])
|
||||
|
||||
const filteredCorporates = useMemo(() => {
|
||||
const participantsAndAdmins = uniq(groups.flatMap((x) => [...x.participants, x.admin])).filter((x) => x !== user?.id);
|
||||
return users.filter((x) => participantsAndAdmins.includes(x.id) && x.type === "corporate");
|
||||
}, [users, groups, user]);
|
||||
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 parsedExams = useMemo(() => {
|
||||
return filteredExams.map((exam) => {
|
||||
@@ -93,7 +67,7 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
||||
});
|
||||
}, [filteredExams, users]);
|
||||
|
||||
const { rows: filteredRows, renderSearch } = useListSearch<Exam>(searchFields, parsedExams);
|
||||
const {rows: filteredRows, renderSearch} = useListSearch<Exam>(searchFields, parsedExams);
|
||||
|
||||
const dispatch = useExamStore((state) => state.dispatch);
|
||||
|
||||
@@ -108,7 +82,7 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
||||
|
||||
return;
|
||||
}
|
||||
dispatch({ type: "INIT_EXAM", payload: { exams: [exam], modules: [module] } })
|
||||
dispatch({type: "INIT_EXAM", payload: {exams: [exam], modules: [module]}});
|
||||
|
||||
router.push("/exam");
|
||||
};
|
||||
@@ -117,7 +91,7 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
||||
if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return;
|
||||
|
||||
axios
|
||||
.patch(`/api/exam/${exam.module}/${exam.id}`, { private: !exam.private })
|
||||
.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) {
|
||||
@@ -135,29 +109,6 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
||||
.finally(reload);
|
||||
};
|
||||
|
||||
const updateExam = async (exam: Exam, body: object) => {
|
||||
if (!confirm(`Are you sure you want to update this ${capitalize(exam.module)} exam?`)) return;
|
||||
|
||||
axios
|
||||
.patch(`/api/exam/${exam.module}/${exam.id}`, body)
|
||||
.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;
|
||||
}
|
||||
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload)
|
||||
.finally(() => setSelectedExam(undefined));
|
||||
};
|
||||
|
||||
const deleteExam = async (exam: Exam) => {
|
||||
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
|
||||
|
||||
@@ -222,12 +173,12 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
||||
}),
|
||||
columnHelper.accessor("createdBy", {
|
||||
header: "Created By",
|
||||
cell: (info) => info.getValue(),
|
||||
cell: (info) => (!info.getValue() ? "System" : findBy(users, "id", info.getValue())?.name || "N/A"),
|
||||
}),
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({ row }: { row: { original: Exam } }) => {
|
||||
cell: ({row}: {row: {original: Exam}}) => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{(row.original.owners?.includes(user.id) || checkAccess(user, ["admin", "developer"])) && (
|
||||
@@ -270,7 +221,7 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
||||
|
||||
const handleExamEdit = () => {
|
||||
router.push(`/generation?id=${selectedExam!.id}&module=${selectedExam!.module}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full h-full">
|
||||
@@ -286,30 +237,17 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-3">
|
||||
<p className="font-medium mb-1">
|
||||
Exam ID: {selectedExam.id}
|
||||
</p>
|
||||
<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>
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<Button color="purple" onClick={handleExamEdit} className="w-32 text-white flex items-center justify-center gap-2">
|
||||
Proceed
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user