Merged in workflow-permissions (pull request #146)
Workflow permissions Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -17,7 +17,7 @@ async function put(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const user = await requestUser(req, res);
|
const user = await requestUser(req, res);
|
||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) {
|
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
|
||||||
return res.status(403).json({ ok: false });
|
return res.status(403).json({ ok: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { ApprovalWorkflow } from "@/interfaces/approval.workflow";
|
|||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { deleteApprovalWorkflow, getApprovalWorkflow, updateApprovalWorkflow } from "@/utils/approval.workflows.be";
|
import { deleteApprovalWorkflow, getApprovalWorkflow, updateApprovalWorkflow } from "@/utils/approval.workflows.be";
|
||||||
|
import { getEntityWithRoles } from "@/utils/entities.be";
|
||||||
|
import { doesEntityAllow } from "@/utils/permissions";
|
||||||
import { withIronSessionApiRoute } from "iron-session/next";
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
@@ -19,20 +21,30 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const user = await requestUser(req, res);
|
const user = await requestUser(req, res);
|
||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) {
|
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
|
||||||
return res.status(403).json({ ok: false });
|
return res.status(403).json({ ok: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = req.query as { id?: string };
|
const { id } = req.query as { id: string };
|
||||||
|
const workflow = await getApprovalWorkflow("active-workflows", id);
|
||||||
|
|
||||||
if (id) return res.status(200).json(await deleteApprovalWorkflow("active-workflows", id));
|
if (!workflow) return res.status(404).json({ ok: false });
|
||||||
|
|
||||||
|
const entity = await getEntityWithRoles(workflow.entityId);
|
||||||
|
if (!entity) return res.status(404).json({ ok: false });
|
||||||
|
|
||||||
|
if (!doesEntityAllow(user, entity, "delete_workflow") && !["admin", "developer"].includes(user.type)) {
|
||||||
|
return res.status(403).json({ ok: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json(await deleteApprovalWorkflow("active-workflows", id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function put(req: NextApiRequest, res: NextApiResponse) {
|
async function put(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const user = await requestUser(req, res);
|
const user = await requestUser(req, res);
|
||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) {
|
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
|
||||||
return res.status(403).json({ ok: false });
|
return res.status(403).json({ ok: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +62,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const user = await requestUser(req, res);
|
const user = await requestUser(req, res);
|
||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) {
|
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
|
||||||
return res.status(403).json({ ok: false });
|
return res.status(403).json({ ok: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const user = await requestUser(req, res);
|
const user = await requestUser(req, res);
|
||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) {
|
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
|
||||||
return res.status(403).json({ ok: false });
|
return res.status(403).json({ ok: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const user = await requestUser(req, res);
|
const user = await requestUser(req, res);
|
||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) {
|
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
|
||||||
return res.status(403).json({ ok: false });
|
return res.status(403).json({ ok: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import Layout from "@/components/High/Layout";
|
|||||||
import { ApprovalWorkflow, EditableApprovalWorkflow, EditableWorkflowStep, getUserTypeLabelShort } from "@/interfaces/approval.workflow";
|
import { ApprovalWorkflow, EditableApprovalWorkflow, EditableWorkflowStep, getUserTypeLabelShort } from "@/interfaces/approval.workflow";
|
||||||
import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser, User } from "@/interfaces/user";
|
import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser, User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { redirect, serialize } from "@/utils";
|
import { findBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { getApprovalWorkflow } from "@/utils/approval.workflows.be";
|
import { getApprovalWorkflow } from "@/utils/approval.workflows.be";
|
||||||
|
import { getEntityWithRoles } from "@/utils/entities.be";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
|
import { doesEntityAllow } from "@/utils/permissions";
|
||||||
import { getEntityUsers } from "@/utils/users.be";
|
import { getEntityUsers } from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { LayoutGroup, motion } from "framer-motion";
|
import { LayoutGroup, motion } from "framer-motion";
|
||||||
@@ -30,9 +32,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
|
|||||||
const { id } = params as { id: string };
|
const { id } = params as { id: string };
|
||||||
|
|
||||||
const workflow: ApprovalWorkflow | null = await getApprovalWorkflow("active-workflows", id);
|
const workflow: ApprovalWorkflow | null = await getApprovalWorkflow("active-workflows", id);
|
||||||
|
if (!workflow) return redirect("/approval-workflows");
|
||||||
|
|
||||||
if (!workflow)
|
const entityWithRole = await getEntityWithRoles(workflow.entityId);
|
||||||
return redirect("/approval-workflows")
|
if (!entityWithRole) return redirect("/approval-workflows");
|
||||||
|
|
||||||
|
if (!doesEntityAllow(user, entityWithRole, "edit_workflow")) return redirect("/approval-workflows");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({
|
props: serialize({
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import useExamStore from "@/stores/exam";
|
|||||||
import { redirect, serialize } from "@/utils";
|
import { redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { getApprovalWorkflow } from "@/utils/approval.workflows.be";
|
import { getApprovalWorkflow } from "@/utils/approval.workflows.be";
|
||||||
|
import { getEntityWithRoles } from "@/utils/entities.be";
|
||||||
import { getExamById } from "@/utils/exams";
|
import { getExamById } from "@/utils/exams";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
|
import { doesEntityAllow } from "@/utils/permissions";
|
||||||
import { getSpecificUsers, getUser } from "@/utils/users.be";
|
import { getSpecificUsers, getUser } from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
|
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
|
||||||
@@ -46,8 +48,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
|
|||||||
|
|
||||||
const workflow: ApprovalWorkflow | null = await getApprovalWorkflow("active-workflows", id);
|
const workflow: ApprovalWorkflow | null = await getApprovalWorkflow("active-workflows", id);
|
||||||
|
|
||||||
if (!workflow)
|
if (!workflow) return redirect("/approval-workflows")
|
||||||
return redirect("/approval-workflows")
|
|
||||||
|
const entityWithRole = await getEntityWithRoles(workflow.entityId);
|
||||||
|
if (!entityWithRole) return redirect("/approval-workflows");
|
||||||
|
|
||||||
|
if (!doesEntityAllow(user, entityWithRole, "view_workflows")) return redirect("/approval-workflows");
|
||||||
|
|
||||||
const allAssigneeIds: string[] = [
|
const allAssigneeIds: string[] = [
|
||||||
...new Set(
|
...new Set(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Tip from "@/components/ApprovalWorkflows/Tip";
|
import Tip from "@/components/ApprovalWorkflows/Tip";
|
||||||
import WorkflowForm from "@/components/ApprovalWorkflows/WorkflowForm";
|
import WorkflowForm from "@/components/ApprovalWorkflows/WorkflowForm";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
@@ -8,11 +7,13 @@ import { ApprovalWorkflow, EditableApprovalWorkflow } from "@/interfaces/approva
|
|||||||
import { Entity } from "@/interfaces/entity";
|
import { Entity } from "@/interfaces/entity";
|
||||||
import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser, User } from "@/interfaces/user";
|
import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser, User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { redirect, serialize } from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { getApprovalWorkflowsByEntities } from "@/utils/approval.workflows.be";
|
import { getApprovalWorkflowsByEntities } from "@/utils/approval.workflows.be";
|
||||||
import { getEntities } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
|
import { findAllowedEntities } from "@/utils/permissions";
|
||||||
|
import { isAdmin } from "@/utils/users";
|
||||||
import { getEntitiesUsers } from "@/utils/users.be";
|
import { getEntitiesUsers } from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
|
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
|
||||||
@@ -31,10 +32,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login")
|
||||||
|
|
||||||
if (shouldRedirectHome(user) || !["admin", "developer", "corporate", "mastercorporate"].includes(user.type))
|
if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type))
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
const userEntitiesWithLabel = await getEntities(user.entities.map(entity => entity.id));
|
const entityIDS = mapBy(user.entities, "id");
|
||||||
|
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS);
|
||||||
|
const userEntitiesWithLabel = findAllowedEntities(user, entities, "configure_workflows");
|
||||||
|
|
||||||
const allConfiguredWorkflows = await getApprovalWorkflowsByEntities("configured-workflows", userEntitiesWithLabel.map(entity => entity.id));
|
const allConfiguredWorkflows = await getApprovalWorkflowsByEntities("configured-workflows", userEntitiesWithLabel.map(entity => entity.id));
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import Button from "@/components/Low/Button";
|
|||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
|
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
|
||||||
|
import { useAllowedEntities, useAllowedEntitiesSomePermissions, useEntityPermission } from "@/hooks/useEntityPermissions";
|
||||||
import { Module, ModuleTypeLabels } from "@/interfaces";
|
import { Module, ModuleTypeLabels } from "@/interfaces";
|
||||||
import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow";
|
import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow";
|
||||||
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { redirect, serialize } from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { getApprovalWorkflows } from "@/utils/approval.workflows.be";
|
import { getApprovalWorkflows } from "@/utils/approval.workflows.be";
|
||||||
import { getEntities } from "@/utils/entities.be";
|
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
|
import { doesEntityAllow, findAllowedEntities } from "@/utils/permissions";
|
||||||
|
import { isAdmin } from "@/utils/users";
|
||||||
import { getSpecificUsers } from "@/utils/users.be";
|
import { getSpecificUsers } from "@/utils/users.be";
|
||||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -21,397 +24,378 @@ import clsx from "clsx";
|
|||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { BsTrash } from "react-icons/bs";
|
import { BsTrash } from "react-icons/bs";
|
||||||
import { FaRegEdit } from "react-icons/fa";
|
import { FaRegEdit } from "react-icons/fa";
|
||||||
import { IoIosAddCircleOutline } from "react-icons/io";
|
import { IoIosAddCircleOutline } from "react-icons/io";
|
||||||
import { toast, ToastContainer } from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<ApprovalWorkflow>();
|
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|
||||||
const user = await requestUser(req, res)
|
|
||||||
if (!user) return redirect("/login")
|
|
||||||
|
|
||||||
if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type))
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
const workflows = await getApprovalWorkflows("active-workflows");
|
|
||||||
|
|
||||||
const allAssigneeIds: string[] = [
|
|
||||||
...new Set(
|
|
||||||
workflows
|
|
||||||
.map(workflow => workflow.steps
|
|
||||||
.map(step => step.assignees)
|
|
||||||
.flat()
|
|
||||||
).flat()
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: serialize({
|
|
||||||
user,
|
|
||||||
initialWorkflows: workflows,
|
|
||||||
workflowsAssignees: await getSpecificUsers(allAssigneeIds),
|
|
||||||
userEntitiesWithLabel: await getEntities(user.entities.map(entity => entity.id)),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}, sessionOptions);
|
|
||||||
|
|
||||||
const StatusClassNames: { [key in ApprovalWorkflowStatus]: string } = {
|
const StatusClassNames: { [key in ApprovalWorkflowStatus]: string } = {
|
||||||
approved: "bg-green-100 text-green-800 border border-green-300 before:content-[''] before:w-2 before:h-2 before:bg-green-500 before:rounded-full before:inline-block before:mr-2",
|
approved:
|
||||||
pending: "bg-orange-100 text-orange-800 border border-orange-300 before:content-[''] before:w-2 before:h-2 before:bg-orange-500 before:rounded-full before:inline-block before:mr-2",
|
"bg-green-100 text-green-800 border border-green-300 before:content-[''] before:w-2 before:h-2 before:bg-green-500 before:rounded-full before:inline-block before:mr-2",
|
||||||
rejected: "bg-red-100 text-red-800 border border-red-300 before:content-[''] before:w-2 before:h-2 before:bg-red-500 before:rounded-full before:inline-block before:mr-2",
|
pending:
|
||||||
|
"bg-orange-100 text-orange-800 border border-orange-300 before:content-[''] before:w-2 before:h-2 before:bg-orange-500 before:rounded-full before:inline-block before:mr-2",
|
||||||
|
rejected:
|
||||||
|
"bg-red-100 text-red-800 border border-red-300 before:content-[''] before:w-2 before:h-2 before:bg-red-500 before:rounded-full before:inline-block before:mr-2",
|
||||||
};
|
};
|
||||||
|
|
||||||
type CustomStatus = ApprovalWorkflowStatus | undefined;
|
type CustomStatus = ApprovalWorkflowStatus | undefined;
|
||||||
type CustomEntity = EntityWithRoles["id"] | undefined;
|
type CustomEntity = EntityWithRoles["id"] | undefined;
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{
|
{
|
||||||
label: "Approved",
|
label: "Approved",
|
||||||
value: "approved",
|
value: "approved",
|
||||||
filter: (x: ApprovalWorkflow) => x.status === "approved",
|
filter: (x: ApprovalWorkflow) => x.status === "approved",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pending",
|
label: "Pending",
|
||||||
value: "pending",
|
value: "pending",
|
||||||
filter: (x: ApprovalWorkflow) => x.status === "pending",
|
filter: (x: ApprovalWorkflow) => x.status === "pending",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Rejected",
|
label: "Rejected",
|
||||||
value: "rejected",
|
value: "rejected",
|
||||||
filter: (x: ApprovalWorkflow) => x.status === "rejected",
|
filter: (x: ApprovalWorkflow) => x.status === "rejected",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<ApprovalWorkflow>();
|
||||||
|
|
||||||
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
|
const user = await requestUser(req, res);
|
||||||
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
|
if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) return redirect("/");
|
||||||
|
|
||||||
|
const workflows = await getApprovalWorkflows("active-workflows");
|
||||||
|
|
||||||
|
const allAssigneeIds: string[] = [
|
||||||
|
...new Set(
|
||||||
|
workflows
|
||||||
|
.map(workflow => workflow.steps
|
||||||
|
.map(step => step.assignees)
|
||||||
|
.flat()
|
||||||
|
).flat()
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const entityIDS = mapBy(user.entities, "id");
|
||||||
|
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS);
|
||||||
|
const allowedEntities = findAllowedEntities(user, entities, "view_workflows");
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: serialize({
|
||||||
|
user,
|
||||||
|
initialWorkflows: workflows,
|
||||||
|
workflowsAssignees: await getSpecificUsers(allAssigneeIds),
|
||||||
|
userEntitiesWithLabel: allowedEntities,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}, sessionOptions);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User,
|
user: User;
|
||||||
initialWorkflows: ApprovalWorkflow[],
|
initialWorkflows: ApprovalWorkflow[];
|
||||||
workflowsAssignees: User[],
|
workflowsAssignees: User[];
|
||||||
userEntitiesWithLabel: Entity[],
|
userEntitiesWithLabel: EntityWithRoles[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ApprovalWorkflows({ user, initialWorkflows, workflowsAssignees, userEntitiesWithLabel }: Props) {
|
export default function ApprovalWorkflows({ user, initialWorkflows, workflowsAssignees, userEntitiesWithLabel }: Props) {
|
||||||
|
const { workflows, reload } = useApprovalWorkflows();
|
||||||
|
const currentWorkflows = workflows || initialWorkflows;
|
||||||
|
|
||||||
const { workflows, reload } = useApprovalWorkflows();
|
const [filteredWorkflows, setFilteredWorkflows] = useState<ApprovalWorkflow[]>([]);
|
||||||
const currentWorkflows = workflows || initialWorkflows;
|
|
||||||
|
|
||||||
const [filteredWorkflows, setFilteredWorkflows] = useState<ApprovalWorkflow[]>([]);
|
const [statusFilter, setStatusFilter] = useState<CustomStatus>(undefined);
|
||||||
|
const [entityFilter, setEntityFilter] = useState<CustomEntity>(undefined);
|
||||||
|
const [nameFilter, setNameFilter] = useState<string>("");
|
||||||
|
|
||||||
const [statusFilter, setStatusFilter] = useState<CustomStatus>(undefined);
|
const router = useRouter();
|
||||||
const [entityFilter, setEntityFilter] = useState<CustomEntity>(undefined);
|
|
||||||
const [nameFilter, setNameFilter] = useState<string>("");
|
|
||||||
|
|
||||||
const ENTITY_OPTIONS = [
|
/* const allowedEntities = useAllowedEntities(user, userEntitiesWithLabel, "view_workflows");
|
||||||
...userEntitiesWithLabel
|
const allowedSomeEntities = useAllowedEntitiesSomePermissions(user, userEntitiesWithLabel, ["view_workflows", "create_workflow"]); */
|
||||||
.map(entity => ({
|
|
||||||
label: entity.label,
|
|
||||||
value: entity.id,
|
|
||||||
filter: (x: ApprovalWorkflow) => x.entityId === entity.id,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
||||||
];
|
|
||||||
|
|
||||||
useEffect(() => {
|
const ENTITY_OPTIONS = [
|
||||||
const filters: Array<(workflow: ApprovalWorkflow) => boolean> = [];
|
...userEntitiesWithLabel
|
||||||
|
.map((entity) => ({
|
||||||
|
label: entity.label,
|
||||||
|
value: entity.id,
|
||||||
|
filter: (x: ApprovalWorkflow) => x.entityId === entity.id,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||||
|
];
|
||||||
|
|
||||||
if (statusFilter && statusFilter !== undefined) {
|
useEffect(() => {
|
||||||
const statusOption = STATUS_OPTIONS.find((x) => x.value === statusFilter);
|
const filters: Array<(workflow: ApprovalWorkflow) => boolean> = [];
|
||||||
if (statusOption && statusOption.filter) {
|
|
||||||
filters.push(statusOption.filter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entityFilter && entityFilter !== undefined) {
|
if (statusFilter && statusFilter !== undefined) {
|
||||||
const entityOption = ENTITY_OPTIONS.find((x) => x.value === entityFilter);
|
const statusOption = STATUS_OPTIONS.find((x) => x.value === statusFilter);
|
||||||
if (entityOption && entityOption.filter) {
|
if (statusOption && statusOption.filter) {
|
||||||
filters.push(entityOption.filter);
|
filters.push(statusOption.filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameFilter.trim() !== "") {
|
if (entityFilter && entityFilter !== undefined) {
|
||||||
const nameFilterFunction = (workflow: ApprovalWorkflow) =>
|
const entityOption = ENTITY_OPTIONS.find((x) => x.value === entityFilter);
|
||||||
workflow.name.toLowerCase().includes(nameFilter.toLowerCase());
|
if (entityOption && entityOption.filter) {
|
||||||
filters.push(nameFilterFunction);
|
filters.push(entityOption.filter);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Apply all filters
|
if (nameFilter.trim() !== "") {
|
||||||
const filtered = currentWorkflows.filter(workflow => filters.every(filterFn => filterFn(workflow)));
|
const nameFilterFunction = (workflow: ApprovalWorkflow) => workflow.name.toLowerCase().includes(nameFilter.toLowerCase());
|
||||||
setFilteredWorkflows(filtered);
|
filters.push(nameFilterFunction);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}
|
||||||
}, [currentWorkflows, statusFilter, entityFilter, nameFilter]);
|
|
||||||
|
|
||||||
|
// Apply all filters
|
||||||
|
const filtered = currentWorkflows.filter((workflow) => filters.every((filterFn) => filterFn(workflow)));
|
||||||
|
setFilteredWorkflows(filtered);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentWorkflows, statusFilter, entityFilter, nameFilter]);
|
||||||
|
|
||||||
const handleNameFilterChange = (name: ApprovalWorkflow["name"]) => {
|
const handleNameFilterChange = (name: ApprovalWorkflow["name"]) => {
|
||||||
setNameFilter(name);
|
setNameFilter(name);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteApprovalWorkflow = (id: string | undefined, name: string) => {
|
const deleteApprovalWorkflow = (id: string | undefined, name: string) => {
|
||||||
if (id === undefined) return;
|
if (id === undefined) return;
|
||||||
if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return;
|
if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete(`/api/approval-workflows/${id}`)
|
.delete(`/api/approval-workflows/${id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`Successfully deleted ${name} Approval Workflow.`);
|
toast.success(`Successfully deleted ${name} Approval Workflow.`);
|
||||||
reload();
|
reload();
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
if (reason.response.status === 404) {
|
if (reason.response.status === 403) {
|
||||||
toast.error("Approval Workflow not found!");
|
toast.error("You do not have permission to delete this Approval Workflow!");
|
||||||
} else if (reason.response.status === 403) {
|
} else {
|
||||||
toast.error("You do not have permission to delete an Approval Workflow!");
|
toast.error("Something went wrong, please try again later.");
|
||||||
} else {
|
}
|
||||||
toast.error("Something went wrong, please try again later.");
|
return;
|
||||||
}
|
});
|
||||||
return;
|
};
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: "EXAM NAME",
|
header: "EXAM NAME",
|
||||||
cell: (info) => (
|
cell: (info) => <span className="font-medium">{info.getValue()}</span>,
|
||||||
<span className="font-medium">
|
}),
|
||||||
{info.getValue()}
|
columnHelper.accessor("modules", {
|
||||||
</span>
|
header: "MODULES",
|
||||||
),
|
cell: (info) => (
|
||||||
}),
|
<div className="flex flex-wrap gap-2">
|
||||||
columnHelper.accessor("modules", {
|
{info.getValue().map((module: Module, index: number) => (
|
||||||
header: "MODULES",
|
<span
|
||||||
cell: (info) => (
|
key={index}
|
||||||
<div className="flex flex-wrap gap-2">
|
className="inline-block rounded-full px-3 py-1 text-sm font-medium bg-indigo-100 border border-indigo-300 text-indigo-900">
|
||||||
{info.getValue().map((module: Module, index: number) => (
|
{ModuleTypeLabels[module]}
|
||||||
<span
|
</span>
|
||||||
key={index}
|
))}
|
||||||
className="inline-block rounded-full px-3 py-1 text-sm font-medium bg-indigo-100 border border-indigo-300 text-indigo-900"
|
</div>
|
||||||
>
|
),
|
||||||
{ModuleTypeLabels[module]}
|
}),
|
||||||
</span>
|
columnHelper.accessor("status", {
|
||||||
))}
|
header: "STATUS",
|
||||||
</div>
|
cell: (info) => (
|
||||||
),
|
<span
|
||||||
}),
|
className={clsx(
|
||||||
columnHelper.accessor("status", {
|
"inline-block rounded-full px-3 py-1 text-sm font-medium text-left w-[110px]",
|
||||||
header: "STATUS",
|
StatusClassNames[info.getValue()],
|
||||||
cell: (info) => (
|
)}>
|
||||||
<span className={clsx("inline-block rounded-full px-3 py-1 text-sm font-medium text-left w-[110px]", StatusClassNames[info.getValue()])}>
|
{ApprovalWorkflowStatusLabel[info.getValue()]}
|
||||||
{ApprovalWorkflowStatusLabel[info.getValue()]}
|
</span>
|
||||||
</span>
|
),
|
||||||
),
|
}),
|
||||||
}),
|
columnHelper.accessor("entityId", {
|
||||||
columnHelper.accessor("entityId", {
|
header: "ENTITY",
|
||||||
header: "ENTITY",
|
cell: (info) => <span className="font-medium">{userEntitiesWithLabel.find((entity) => entity.id === info.getValue())?.label}</span>,
|
||||||
cell: (info) => (
|
}),
|
||||||
<span className="font-medium">
|
columnHelper.accessor("steps", {
|
||||||
{userEntitiesWithLabel.find((entity) => entity.id === info.getValue())?.label}
|
id: "currentAssignees",
|
||||||
</span>
|
header: "CURRENT ASSIGNEES",
|
||||||
),
|
cell: (info) => {
|
||||||
}),
|
const steps = info.row.original.steps;
|
||||||
columnHelper.accessor("steps", {
|
const currentStep = steps.find((step) => !step.completed);
|
||||||
id: "currentAssignees",
|
const rejected = steps.find((step) => step.rejected);
|
||||||
header: "CURRENT ASSIGNEES",
|
|
||||||
cell: (info) => {
|
|
||||||
const steps = info.row.original.steps;
|
|
||||||
const currentStep = steps.find((step) => !step.completed);
|
|
||||||
const rejected = steps.find((step) => step.rejected);
|
|
||||||
|
|
||||||
if (rejected) return "";
|
if (rejected) return "";
|
||||||
|
|
||||||
const assignees = currentStep?.assignees.map((assigneeId) => {
|
const assignees = currentStep?.assignees.map((assigneeId) => {
|
||||||
const assignee = workflowsAssignees.find((user) => user.id === assigneeId);
|
const assignee = workflowsAssignees.find((user) => user.id === assigneeId);
|
||||||
return assignee?.name || "Unknown Assignee";
|
return assignee?.name || "Unknown Assignee";
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignees?.map((assigneeName: string, index: number) => (
|
{assignees?.map((assigneeName: string, index: number) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className="inline-block rounded-full px-3 py-1 text-sm font-medium bg-gray-100 border border-gray-300 text-gray-900"
|
className="inline-block rounded-full px-3 py-1 text-sm font-medium bg-gray-100 border border-gray-300 text-gray-900">
|
||||||
>
|
{assigneeName}
|
||||||
{assigneeName}
|
</span>
|
||||||
</span>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
},
|
||||||
},
|
}),
|
||||||
}),
|
columnHelper.accessor("steps", {
|
||||||
columnHelper.accessor("steps", {
|
id: "currentStep",
|
||||||
id: "currentStep",
|
header: "CURRENT STEP",
|
||||||
header: "CURRENT STEP",
|
cell: (info) => {
|
||||||
cell: (info) => {
|
const steps = info.row.original.steps;
|
||||||
const steps = info.row.original.steps;
|
const currentStep = steps.find((step) => !step.completed);
|
||||||
const currentStep = steps.find((step) => !step.completed);
|
const rejected = steps.find((step) => step.rejected);
|
||||||
const rejected = steps.find((step) => step.rejected);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{currentStep && !rejected
|
{currentStep && !rejected ? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}` : "Completed"}
|
||||||
? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}`
|
</span>
|
||||||
: "Completed"}
|
);
|
||||||
</span>
|
},
|
||||||
);
|
}),
|
||||||
},
|
columnHelper.accessor("steps", {
|
||||||
}),
|
header: "ACTIONS",
|
||||||
columnHelper.accessor("steps", {
|
id: "actions",
|
||||||
header: "ACTIONS",
|
cell: ({ row }) => {
|
||||||
id: "actions",
|
const steps = row.original.steps;
|
||||||
cell: ({ row }) => {
|
const currentStep = steps.find((step) => !step.completed);
|
||||||
const steps = row.original.steps;
|
const rejected = steps.find((step) => step.rejected);
|
||||||
const currentStep = steps.find((step) => !step.completed);
|
|
||||||
const rejected = steps.find((step) => step.rejected);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<button
|
||||||
data-tip="Delete"
|
data-tip="Delete"
|
||||||
className="cursor-pointer tooltip"
|
className="cursor-pointer tooltip"
|
||||||
onClick={(e) => {
|
disabled={!doesEntityAllow(user, userEntitiesWithLabel.find(entity => entity.id === row.original.entityId)!, "delete_workflow")}
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
deleteApprovalWorkflow(row.original._id?.toString(), row.original.name);
|
e.stopPropagation();
|
||||||
}}
|
deleteApprovalWorkflow(row.original._id?.toString(), row.original.name);
|
||||||
>
|
}}>
|
||||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{currentStep && !rejected && (
|
{currentStep && !rejected && (
|
||||||
<Link
|
<button
|
||||||
onClick={(e) => e.stopPropagation()}
|
data-tip="Edit"
|
||||||
data-tip="Edit"
|
className="cursor-pointer tooltip"
|
||||||
href={`/approval-workflows/${row.original._id?.toString()}/edit`}
|
disabled={!doesEntityAllow(user, userEntitiesWithLabel.find(entity => entity.id === row.original.entityId)!, "edit_workflow")}
|
||||||
className="cursor-pointer tooltip"
|
onClick={(e) => {
|
||||||
>
|
e.stopPropagation();
|
||||||
<FaRegEdit className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
router.push(`/approval-workflows/${row.original._id?.toString()}/edit`);
|
||||||
</Link>
|
}}>
|
||||||
)}
|
<FaRegEdit className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</button>
|
||||||
);
|
)}
|
||||||
},
|
</div>
|
||||||
})
|
);
|
||||||
];
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredWorkflows,
|
data: filteredWorkflows,
|
||||||
columns: columns,
|
columns: columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Approval Workflows Panel | EnCoach</title>
|
<title>Approval Workflows Panel | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<h1 className="text-2xl font-semibold">Approval Workflows</h1>
|
<h1 className="text-2xl font-semibold">Approval Workflows</h1>
|
||||||
|
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<Link href={"/approval-workflows/create"}>
|
<Link href={"/approval-workflows/create"}>
|
||||||
<Button
|
<Button color="purple" variant="solid" className="min-w-fit text-lg font-medium flex items-center gap-2 text-left">
|
||||||
color="purple"
|
<IoIosAddCircleOutline className="size-6" />
|
||||||
variant="solid"
|
Configure Workflows
|
||||||
className="min-w-fit text-lg font-medium flex items-center gap-2 text-left"
|
</Button>
|
||||||
>
|
</Link>
|
||||||
<IoIosAddCircleOutline className="size-6" />
|
</div>
|
||||||
Configure Workflows
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full items-center gap-4">
|
<div className="flex w-full items-center gap-4">
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">Name</label>
|
<label className="text-mti-gray-dim text-base font-normal">Name</label>
|
||||||
<Input
|
<Input name="nameFilter" type="text" value={nameFilter} onChange={handleNameFilterChange} placeholder="Filter by name..." />
|
||||||
name="nameFilter"
|
</div>
|
||||||
type="text"
|
<div className="flex w-full flex-col gap-3">
|
||||||
value={nameFilter}
|
<label className="text-mti-gray-dim text-base font-normal">Status</label>
|
||||||
onChange={handleNameFilterChange}
|
<Select
|
||||||
placeholder="Filter by name..."
|
options={STATUS_OPTIONS}
|
||||||
/>
|
value={STATUS_OPTIONS.find((x) => x.value === statusFilter)}
|
||||||
</div>
|
onChange={(value) => setStatusFilter((value?.value as ApprovalWorkflowStatus) ?? undefined)}
|
||||||
<div className="flex w-full flex-col gap-3">
|
isClearable
|
||||||
<label className="text-mti-gray-dim text-base font-normal">Status</label>
|
placeholder="Filter by status..."
|
||||||
<Select
|
/>
|
||||||
options={STATUS_OPTIONS}
|
</div>
|
||||||
value={STATUS_OPTIONS.find((x) => x.value === statusFilter)}
|
<div className="flex w-full flex-col gap-3">
|
||||||
onChange={(value) => setStatusFilter((value?.value as ApprovalWorkflowStatus) ?? undefined)}
|
<label className="text-mti-gray-dim text-base font-normal">Entity</label>
|
||||||
isClearable
|
<Select
|
||||||
placeholder="Filter by status..."
|
options={ENTITY_OPTIONS}
|
||||||
/>
|
value={ENTITY_OPTIONS.find((x) => x.value === entityFilter)}
|
||||||
</div>
|
onChange={(value) => setEntityFilter((value?.value as CustomEntity) ?? undefined)}
|
||||||
<div className="flex w-full flex-col gap-3">
|
isClearable
|
||||||
<label className="text-mti-gray-dim text-base font-normal">Entity</label>
|
placeholder="Filter by entity..."
|
||||||
<Select
|
/>
|
||||||
options={ENTITY_OPTIONS}
|
</div>
|
||||||
value={ENTITY_OPTIONS.find((x) => x.value === entityFilter)}
|
</div>
|
||||||
onChange={(value) => setEntityFilter((value?.value as CustomEntity) ?? undefined)}
|
|
||||||
isClearable
|
|
||||||
placeholder="Filter by entity..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tip text="An exam submission will instantiate the approval workflow configured for the exam author. The exam will be valid only when all the steps of the workflow have been approved."></Tip>
|
<Tip text="An exam submission will instantiate the approval workflow configured for the exam author. The exam will be valid only when all the steps of the workflow have been approved."></Tip>
|
||||||
|
|
||||||
<div className="px-6 pb-4 bg-mti-purple-ultralight rounded-2xl border-2 border-mti-purple-light border-opacity-40">
|
<div className="px-6 pb-4 bg-mti-purple-ultralight rounded-2xl border-2 border-mti-purple-light border-opacity-40">
|
||||||
<table
|
<table className="w-full table-auto border-separate border-spacing-y-2" style={{ tableLayout: "auto" }}>
|
||||||
className="w-full table-auto border-separate border-spacing-y-2"
|
<thead>
|
||||||
style={{ tableLayout: "auto" }}
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
>
|
<tr key={headerGroup.id}>
|
||||||
<thead>
|
{headerGroup.headers.map((header) => (
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<th key={header.id} className="px-3 py-2 text-left text-mti-purple-ultradark">
|
||||||
<tr key={headerGroup.id}>
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
{headerGroup.headers.map((header) => (
|
</th>
|
||||||
<th key={header.id} className="px-3 py-2 text-left text-mti-purple-ultradark">
|
))}
|
||||||
{header.isPlaceholder
|
</tr>
|
||||||
? null
|
))}
|
||||||
: flexRender(
|
</thead>
|
||||||
header.column.columnDef.header,
|
<tbody>
|
||||||
header.getContext()
|
{table.getRowModel().rows.map((row) => (
|
||||||
)}
|
<tr
|
||||||
</th>
|
key={row.id}
|
||||||
))}
|
onClick={() => (window.location.href = `/approval-workflows/${row.original._id?.toString()}`)}
|
||||||
</tr>
|
style={{ cursor: "pointer" }}
|
||||||
))}
|
className="bg-purple-50">
|
||||||
</thead>
|
{row.getVisibleCells().map((cell, cellIndex) => {
|
||||||
<tbody>
|
const lastCellIndex = row.getVisibleCells().length - 1;
|
||||||
{table.getRowModel().rows.map((row) => (
|
|
||||||
<tr
|
|
||||||
key={row.id}
|
|
||||||
onClick={() => window.location.href = `/approval-workflows/${row.original._id?.toString()}`}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
className="bg-purple-50"
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell, cellIndex) => {
|
|
||||||
const lastCellIndex = row.getVisibleCells().length - 1;
|
|
||||||
|
|
||||||
let cellClasses = "pl-3 pr-4 py-2 border-y-2 border-mti-purple-light border-opacity-60";
|
let cellClasses = "pl-3 pr-4 py-2 border-y-2 border-mti-purple-light border-opacity-60";
|
||||||
if (cellIndex === 0) {
|
if (cellIndex === 0) {
|
||||||
cellClasses += " border-l-2 rounded-l-2xl";
|
cellClasses += " border-l-2 rounded-l-2xl";
|
||||||
}
|
}
|
||||||
if (cellIndex === lastCellIndex) {
|
if (cellIndex === lastCellIndex) {
|
||||||
cellClasses += " border-r-2 rounded-r-2xl";
|
cellClasses += " border-r-2 rounded-r-2xl";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={cellIndex} className={cellClasses}>
|
<td key={cellIndex} className={cellClasses}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
</table>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,13 @@ const ASSIGNMENT_MANAGEMENT: PermissionLayout[] = [
|
|||||||
{label: "Archive Assignments", key: "archive_assignment"},
|
{label: "Archive Assignments", key: "archive_assignment"},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const WORKFLOW_MANAGEMENT: PermissionLayout[] = [
|
||||||
|
{label: "View Workflows", key: "view_workflows"},
|
||||||
|
{label: "Configure Workflows", key: "configure_workflows"},
|
||||||
|
{label: "Edit Workflow", key: "edit_workflow"},
|
||||||
|
{label: "Delete Workflow", key: "delete_workflow"},
|
||||||
|
];
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => {
|
||||||
const user = await requestUser(req, res);
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login");
|
if (!user) return redirect("/login");
|
||||||
@@ -399,6 +406,30 @@ export default function EntityRole({user, entity, role, userCount, disableEdit}:
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="w-full flex items-center justify-between">
|
||||||
|
<b>Workflow Management</b>
|
||||||
|
<Checkbox
|
||||||
|
disabled={!canEditPermissions || disableEdit}
|
||||||
|
isChecked={mapBy(WORKFLOW_MANAGEMENT, "key").every((k) => permissions.includes(k))}
|
||||||
|
onChange={() => toggleMultiplePermissions(mapBy(WORKFLOW_MANAGEMENT, "key").filter(enableCheckbox))}>
|
||||||
|
Select all
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{WORKFLOW_MANAGEMENT.map(({label, key}) => (
|
||||||
|
<Checkbox
|
||||||
|
disabled={!enableCheckbox(key)}
|
||||||
|
key={key}
|
||||||
|
isChecked={permissions.includes(key)}
|
||||||
|
onChange={() => togglePermissions(key)}>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ export type RolePermission =
|
|||||||
| "pay_entity"
|
| "pay_entity"
|
||||||
| "view_payment_record"
|
| "view_payment_record"
|
||||||
| "view_approval_workflows"
|
| "view_approval_workflows"
|
||||||
| "update_exam_privacy";
|
| "update_exam_privacy"
|
||||||
|
| "view_workflows"
|
||||||
|
| "configure_workflows"
|
||||||
|
| "edit_workflow"
|
||||||
|
| "delete_workflow";
|
||||||
|
|
||||||
export const DEFAULT_PERMISSIONS: RolePermission[] = [
|
export const DEFAULT_PERMISSIONS: RolePermission[] = [
|
||||||
"view_students",
|
"view_students",
|
||||||
@@ -77,7 +81,6 @@ export const DEFAULT_PERMISSIONS: RolePermission[] = [
|
|||||||
"view_entity_roles",
|
"view_entity_roles",
|
||||||
"view_statistics",
|
"view_statistics",
|
||||||
"download_statistics_report",
|
"download_statistics_report",
|
||||||
"view_approval_workflows",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ADMIN_PERMISSIONS: RolePermission[] = [
|
export const ADMIN_PERMISSIONS: RolePermission[] = [
|
||||||
@@ -149,4 +152,8 @@ export const ADMIN_PERMISSIONS: RolePermission[] = [
|
|||||||
"pay_entity",
|
"pay_entity",
|
||||||
"view_payment_record",
|
"view_payment_record",
|
||||||
"update_exam_privacy",
|
"update_exam_privacy",
|
||||||
|
"configure_workflows",
|
||||||
|
"view_workflows",
|
||||||
|
"edit_workflow",
|
||||||
|
"delete_workflow",
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user