From 36afde8aa49f4d7f985c4e9fe1d0838757a3fda7 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 6 Feb 2025 18:48:31 +0000 Subject: [PATCH] Created the new permissions as an example --- src/pages/approval-workflows/index.tsx | 687 +++++++++++------------ src/pages/entities/[id]/roles/[role].tsx | 31 + src/resources/entityPermissions.ts | 10 +- 3 files changed, 368 insertions(+), 360 deletions(-) diff --git a/src/pages/approval-workflows/index.tsx b/src/pages/approval-workflows/index.tsx index fbd84ed6..84b65c1c 100644 --- a/src/pages/approval-workflows/index.tsx +++ b/src/pages/approval-workflows/index.tsx @@ -4,414 +4,383 @@ import Button from "@/components/Low/Button"; import Input from "@/components/Low/Input"; import Select from "@/components/Low/Select"; import useApprovalWorkflows from "@/hooks/useApprovalWorkflows"; -import { Module, ModuleTypeLabels } from "@/interfaces"; -import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow"; -import { Entity, EntityWithRoles } from "@/interfaces/entity"; -import { User } from "@/interfaces/user"; -import { sessionOptions } from "@/lib/session"; -import { redirect, serialize } from "@/utils"; -import { requestUser } from "@/utils/api"; -import { getApprovalWorkflows } from "@/utils/approval.workflows.be"; -import { getEntities } from "@/utils/entities.be"; -import { shouldRedirectHome } from "@/utils/navigation.disabled"; -import { getSpecificUsers } from "@/utils/users.be"; -import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import {useAllowedEntities, useAllowedEntitiesSomePermissions, useEntityPermission} from "@/hooks/useEntityPermissions"; +import {Module, ModuleTypeLabels} from "@/interfaces"; +import {ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel} from "@/interfaces/approval.workflow"; +import {Entity, EntityWithRoles} from "@/interfaces/entity"; +import {User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {redirect, serialize} from "@/utils"; +import {requestUser} from "@/utils/api"; +import {getApprovalWorkflows} from "@/utils/approval.workflows.be"; +import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {findAllowedEntities} from "@/utils/permissions"; +import {getSpecificUsers} from "@/utils/users.be"; +import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; import axios from "axios"; import clsx from "clsx"; -import { withIronSessionSsr } from "iron-session/next"; +import {withIronSessionSsr} from "iron-session/next"; import Head from "next/head"; import Link from "next/link"; -import { useEffect, useState } from "react"; -import { BsTrash } from "react-icons/bs"; -import { FaRegEdit } from "react-icons/fa"; -import { IoIosAddCircleOutline } from "react-icons/io"; -import { toast, ToastContainer } from "react-toastify"; +import {useEffect, useState} from "react"; +import {BsTrash} from "react-icons/bs"; +import {FaRegEdit} from "react-icons/fa"; +import {IoIosAddCircleOutline} from "react-icons/io"; +import {toast, ToastContainer} from "react-toastify"; const columnHelper = createColumnHelper(); -export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - if (!user) return redirect("/login") +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("/") + if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) return redirect("/"); - const workflows = await getApprovalWorkflows("active-workflows"); + const workflows = await getApprovalWorkflows("active-workflows"); - const allAssigneeIds: string[] = [ - ...new Set( - workflows - .map(workflow => workflow.steps - .map(step => step.assignees) - .flat() - ).flat() - ) - ]; + 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)), - }), - }; + const entities = await getEntitiesWithRoles(user.entities.map((entity) => entity.id)); + const allowedEntities = findAllowedEntities(user, entities, "view_workflows"); + + return { + props: serialize({ + user, + initialWorkflows: workflows, + workflowsAssignees: await getSpecificUsers(allAssigneeIds), + userEntitiesWithLabel: entities, + }), + }; }, sessionOptions); -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", - 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", +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", + 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 CustomEntity = EntityWithRoles["id"] | undefined; const STATUS_OPTIONS = [ - { - label: "Approved", - value: "approved", - filter: (x: ApprovalWorkflow) => x.status === "approved", - }, - { - label: "Pending", - value: "pending", - filter: (x: ApprovalWorkflow) => x.status === "pending", - }, - { - label: "Rejected", - value: "rejected", - filter: (x: ApprovalWorkflow) => x.status === "rejected", - }, + { + label: "Approved", + value: "approved", + filter: (x: ApprovalWorkflow) => x.status === "approved", + }, + { + label: "Pending", + value: "pending", + filter: (x: ApprovalWorkflow) => x.status === "pending", + }, + { + label: "Rejected", + value: "rejected", + filter: (x: ApprovalWorkflow) => x.status === "rejected", + }, ]; interface Props { - user: User, - initialWorkflows: ApprovalWorkflow[], - workflowsAssignees: User[], - userEntitiesWithLabel: Entity[], + user: User; + initialWorkflows: ApprovalWorkflow[]; + workflowsAssignees: User[]; + 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 currentWorkflows = workflows || initialWorkflows; + const [filteredWorkflows, setFilteredWorkflows] = useState([]); - const [filteredWorkflows, setFilteredWorkflows] = useState([]); + const [statusFilter, setStatusFilter] = useState(undefined); + const [entityFilter, setEntityFilter] = useState(undefined); + const [nameFilter, setNameFilter] = useState(""); - const [statusFilter, setStatusFilter] = useState(undefined); - const [entityFilter, setEntityFilter] = useState(undefined); - const [nameFilter, setNameFilter] = useState(""); + const allowedEntities = useAllowedEntities(user, userEntitiesWithLabel, "view_workflows"); + const allowedSomeEntities = useAllowedEntitiesSomePermissions(user, userEntitiesWithLabel, ["view_workflows", "create_workflow"]); - const ENTITY_OPTIONS = [ - ...userEntitiesWithLabel - .map(entity => ({ - label: entity.label, - value: entity.id, - filter: (x: ApprovalWorkflow) => x.entityId === entity.id, - })) - .sort((a, b) => a.label.localeCompare(b.label)), - ]; + const ENTITY_OPTIONS = [ + ...userEntitiesWithLabel + .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 filters: Array<(workflow: ApprovalWorkflow) => boolean> = []; + useEffect(() => { + const filters: Array<(workflow: ApprovalWorkflow) => boolean> = []; - if (statusFilter && statusFilter !== undefined) { - const statusOption = STATUS_OPTIONS.find((x) => x.value === statusFilter); - if (statusOption && statusOption.filter) { - filters.push(statusOption.filter); - } - } + if (statusFilter && statusFilter !== undefined) { + const statusOption = STATUS_OPTIONS.find((x) => x.value === statusFilter); + if (statusOption && statusOption.filter) { + filters.push(statusOption.filter); + } + } - if (entityFilter && entityFilter !== undefined) { - const entityOption = ENTITY_OPTIONS.find((x) => x.value === entityFilter); - if (entityOption && entityOption.filter) { - filters.push(entityOption.filter); - } - } + if (entityFilter && entityFilter !== undefined) { + const entityOption = ENTITY_OPTIONS.find((x) => x.value === entityFilter); + if (entityOption && entityOption.filter) { + filters.push(entityOption.filter); + } + } - if (nameFilter.trim() !== "") { - const nameFilterFunction = (workflow: ApprovalWorkflow) => - workflow.name.toLowerCase().includes(nameFilter.toLowerCase()); - filters.push(nameFilterFunction); - } + if (nameFilter.trim() !== "") { + const nameFilterFunction = (workflow: ApprovalWorkflow) => workflow.name.toLowerCase().includes(nameFilter.toLowerCase()); + filters.push(nameFilterFunction); + } - // 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]); + // 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"]) => { + setNameFilter(name); + }; - const handleNameFilterChange = (name: ApprovalWorkflow["name"]) => { - setNameFilter(name); - }; + const deleteApprovalWorkflow = (id: string | undefined, name: string) => { + if (id === undefined) return; + if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return; - const deleteApprovalWorkflow = (id: string | undefined, name: string) => { - if (id === undefined) return; - if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return; + axios + .delete(`/api/approval-workflows/${id}`) + .then(() => { + toast.success(`Successfully deleted ${name} Approval Workflow.`); + reload(); + }) + .catch((reason) => { + if (reason.response.status === 404) { + toast.error("Approval Workflow not found!"); + } else if (reason.response.status === 403) { + toast.error("You do not have permission to delete an Approval Workflow!"); + } else { + toast.error("Something went wrong, please try again later."); + } + return; + }); + }; - axios - .delete(`/api/approval-workflows/${id}`) - .then(() => { - toast.success(`Successfully deleted ${name} Approval Workflow.`); - reload(); - }) - .catch((reason) => { - if (reason.response.status === 404) { - toast.error("Approval Workflow not found!"); - } else if (reason.response.status === 403) { - toast.error("You do not have permission to delete an Approval Workflow!"); - } else { - toast.error("Something went wrong, please try again later."); - } - return; - }) - }; + const columns = [ + columnHelper.accessor("name", { + header: "EXAM NAME", + cell: (info) => {info.getValue()}, + }), + columnHelper.accessor("modules", { + header: "MODULES", + cell: (info) => ( +
+ {info.getValue().map((module: Module, index: number) => ( + + {ModuleTypeLabels[module]} + + ))} +
+ ), + }), + columnHelper.accessor("status", { + header: "STATUS", + cell: (info) => ( + + {ApprovalWorkflowStatusLabel[info.getValue()]} + + ), + }), + columnHelper.accessor("entityId", { + header: "ENTITY", + cell: (info) => {userEntitiesWithLabel.find((entity) => entity.id === info.getValue())?.label}, + }), + columnHelper.accessor("steps", { + id: "currentAssignees", + 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); - const columns = [ - columnHelper.accessor("name", { - header: "EXAM NAME", - cell: (info) => ( - - {info.getValue()} - - ), - }), - columnHelper.accessor("modules", { - header: "MODULES", - cell: (info) => ( -
- {info.getValue().map((module: Module, index: number) => ( - - {ModuleTypeLabels[module]} - - ))} -
- ), - }), - columnHelper.accessor("status", { - header: "STATUS", - cell: (info) => ( - - {ApprovalWorkflowStatusLabel[info.getValue()]} - - ), - }), - columnHelper.accessor("entityId", { - header: "ENTITY", - cell: (info) => ( - - {userEntitiesWithLabel.find((entity) => entity.id === info.getValue())?.label} - - ), - }), - columnHelper.accessor("steps", { - id: "currentAssignees", - 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 assignee = workflowsAssignees.find((user) => user.id === assigneeId); + return assignee?.name || "Unknown Assignee"; + }); - const assignees = currentStep?.assignees.map((assigneeId) => { - const assignee = workflowsAssignees.find((user) => user.id === assigneeId); - return assignee?.name || "Unknown Assignee"; - }); + return ( +
+ {assignees?.map((assigneeName: string, index: number) => ( + + {assigneeName} + + ))} +
+ ); + }, + }), + columnHelper.accessor("steps", { + id: "currentStep", + header: "CURRENT STEP", + cell: (info) => { + const steps = info.row.original.steps; + const currentStep = steps.find((step) => !step.completed); + const rejected = steps.find((step) => step.rejected); - return ( -
- {assignees?.map((assigneeName: string, index: number) => ( - - {assigneeName} - - ))} -
- ); - }, - }), - columnHelper.accessor("steps", { - id: "currentStep", - header: "CURRENT STEP", - cell: (info) => { - const steps = info.row.original.steps; - const currentStep = steps.find((step) => !step.completed); - const rejected = steps.find((step) => step.rejected); + return ( + + {currentStep && !rejected ? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}` : "Completed"} + + ); + }, + }), + columnHelper.accessor("steps", { + header: "ACTIONS", + id: "actions", + cell: ({row}) => { + const steps = row.original.steps; + const currentStep = steps.find((step) => !step.completed); + const rejected = steps.find((step) => step.rejected); - return ( - - {currentStep && !rejected - ? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}` - : "Completed"} - - ); - }, - }), - columnHelper.accessor("steps", { - header: "ACTIONS", - id: "actions", - cell: ({ row }) => { - const steps = row.original.steps; - const currentStep = steps.find((step) => !step.completed); - const rejected = steps.find((step) => step.rejected); + return ( +
+ - return ( -
- + {currentStep && !rejected && ( + e.stopPropagation()} + data-tip="Edit" + href={`/approval-workflows/${row.original._id?.toString()}/edit`} + className="cursor-pointer tooltip"> + + + )} +
+ ); + }, + }), + ]; - {currentStep && !rejected && ( - e.stopPropagation()} - data-tip="Edit" - href={`/approval-workflows/${row.original._id?.toString()}/edit`} - className="cursor-pointer tooltip" - > - - - )} -
- ); - }, - }) - ]; + const table = useReactTable({ + data: filteredWorkflows, + columns: columns, + getCoreRowModel: getCoreRowModel(), + }); - const table = useReactTable({ - data: filteredWorkflows, - columns: columns, - getCoreRowModel: getCoreRowModel(), - }); + return ( + <> + + Approval Workflows Panel | EnCoach + + + + + +

Approval Workflows

- return ( - <> - - Approval Workflows Panel | EnCoach - - - - - -

Approval Workflows

+
+ + + +
-
- - - -
+
+
+ + +
+
+ + x.value === entityFilter)} + onChange={(value) => setEntityFilter((value?.value as CustomEntity) ?? undefined)} + isClearable + placeholder="Filter by entity..." + /> +
+
-
-
- - -
-
- - x.value === entityFilter)} - onChange={(value) => setEntityFilter((value?.value as CustomEntity) ?? undefined)} - isClearable - placeholder="Filter by entity..." - /> -
-
+ - +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + (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; -
-
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
- - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - 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"; + if (cellIndex === 0) { + cellClasses += " border-l-2 rounded-l-2xl"; + } + if (cellIndex === lastCellIndex) { + cellClasses += " border-r-2 rounded-r-2xl"; + } - let cellClasses = "pl-3 pr-4 py-2 border-y-2 border-mti-purple-light border-opacity-60"; - if (cellIndex === 0) { - cellClasses += " border-l-2 rounded-l-2xl"; - } - if (cellIndex === lastCellIndex) { - cellClasses += " border-r-2 rounded-r-2xl"; - } - - return ( - - ); - })} - - ))} - - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
- - ); + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + + ))} + + + + + ); } diff --git a/src/pages/entities/[id]/roles/[role].tsx b/src/pages/entities/[id]/roles/[role].tsx index 693b4370..2c90f9b9 100644 --- a/src/pages/entities/[id]/roles/[role].tsx +++ b/src/pages/entities/[id]/roles/[role].tsx @@ -107,6 +107,13 @@ const ASSIGNMENT_MANAGEMENT: PermissionLayout[] = [ {label: "Archive Assignments", key: "archive_assignment"}, ]; +const WORKFLOW_MANAGEMENT: PermissionLayout[] = [ + {label: "View Workflows", key: "view_workflows"}, + {label: "Create Workflow", key: "create_workflow"}, + {label: "Edit Workflow", key: "edit_workflow"}, + {label: "Delete Workflow", key: "delete_workflow"}, +]; + export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => { const user = await requestUser(req, res); if (!user) return redirect("/login"); @@ -399,6 +406,30 @@ export default function EntityRole({user, entity, role, userCount, disableEdit}: ))} + +
+
+ Workflow Management + permissions.includes(k))} + onChange={() => toggleMultiplePermissions(mapBy(WORKFLOW_MANAGEMENT, "key").filter(enableCheckbox))}> + Select all + +
+ +
+ {WORKFLOW_MANAGEMENT.map(({label, key}) => ( + togglePermissions(key)}> + {label} + + ))} +
+
diff --git a/src/resources/entityPermissions.ts b/src/resources/entityPermissions.ts index cb4212cc..b46ed0d0 100644 --- a/src/resources/entityPermissions.ts +++ b/src/resources/entityPermissions.ts @@ -67,7 +67,11 @@ export type RolePermission = | "pay_entity" | "view_payment_record" | "view_approval_workflows" - | "update_exam_privacy"; + | "update_exam_privacy" + | "view_workflows" + | "create_workflow" + | "edit_workflow" + | "delete_workflow"; export const DEFAULT_PERMISSIONS: RolePermission[] = [ "view_students", @@ -149,4 +153,8 @@ export const ADMIN_PERMISSIONS: RolePermission[] = [ "pay_entity", "view_payment_record", "update_exam_privacy", + "create_workflow", + "view_workflows", + "edit_workflow", + "delete_workflow", ];