major change on how workflow builder works. It now fetches in edit mode all the currently configured workflows

This commit is contained in:
Joao Correia
2025-02-01 22:36:42 +00:00
parent a0229cd971
commit ac539332e6
12 changed files with 159 additions and 56 deletions

View File

@@ -33,6 +33,7 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityApprove
key: stepCounter, key: stepCounter,
stepType: "approval-by", stepType: "approval-by",
stepNumber: workflow.steps.length, stepNumber: workflow.steps.length,
completed: false,
assignees: [null], assignees: [null],
firstStep: false, firstStep: false,
finalStep: false, finalStep: false,

View File

@@ -43,6 +43,10 @@ export interface EditableWorkflowStep {
key: number, key: number,
stepType: StepType, stepType: StepType,
stepNumber: number, stepNumber: number,
completed: boolean,
rejected?: boolean,
completedBy?: User["id"],
completedDate?: number,
assignees: (User["id"] | null | undefined)[]; // bit of an hack, but allowing null or undefined values allows us to match one to one the select input components with the assignees array. And since select inputs allow undefined or null values, it is allowed here too, but must validate required input before form submission assignees: (User["id"] | null | undefined)[]; // bit of an hack, but allowing null or undefined values allows us to match one to one the select input components with the assignees array. And since select inputs allow undefined or null values, it is allowed here too, but must validate required input before form submission
firstStep: boolean, firstStep: boolean,
finalStep?: boolean, finalStep?: boolean,

View File

@@ -2,7 +2,7 @@
import { ApprovalWorkflow } from "@/interfaces/approval.workflow"; 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 { createApprovalWorkflow } from "@/utils/approval.workflows.be"; import { createConfiguredWorkflow } from "@/utils/approval.workflows.be";
import { withIronSessionApiRoute } from "iron-session/next"; import { withIronSessionApiRoute } from "iron-session/next";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
@@ -23,5 +23,5 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const approvalWorkflow: ApprovalWorkflow = req.body; const approvalWorkflow: ApprovalWorkflow = req.body;
if (approvalWorkflow) if (approvalWorkflow)
return res.status(201).json(await createApprovalWorkflow(approvalWorkflow)); return res.status(201).json(await createConfiguredWorkflow(approvalWorkflow));
} }

View File

@@ -2,7 +2,7 @@
import { ApprovalWorkflow } from "@/interfaces/approval.workflow"; 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 { updateApprovalWorkflow } from "@/utils/approval.workflows.be"; import { updateConfiguredWorkflow } from "@/utils/approval.workflows.be";
import { withIronSessionApiRoute } from "iron-session/next"; import { withIronSessionApiRoute } from "iron-session/next";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
@@ -24,7 +24,7 @@ async function put(req: NextApiRequest, res: NextApiResponse) {
const approvalWorkflow: ApprovalWorkflow = req.body; const approvalWorkflow: ApprovalWorkflow = req.body;
if (id && approvalWorkflow) { if (id && approvalWorkflow) {
await updateApprovalWorkflow(id, approvalWorkflow); await updateConfiguredWorkflow(approvalWorkflow);
return res.status(204).end(); return res.status(204).end();
} }
} }

View File

@@ -2,7 +2,7 @@
import { ApprovalWorkflow } from "@/interfaces/approval.workflow"; 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, updateApprovalWorkflow } from "@/utils/approval.workflows.be"; import { deleteConfiguredWorkflow, updateConfiguredWorkflow } from "@/utils/approval.workflows.be";
import { withIronSessionApiRoute } from "iron-session/next"; import { withIronSessionApiRoute } from "iron-session/next";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
@@ -23,7 +23,7 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query as { id?: string }; const { id } = req.query as { id?: string };
if (id) return res.status(200).json(await deleteApprovalWorkflow(id)); if (id) return res.status(200).json(await deleteConfiguredWorkflow(id));
} }
async function put(req: NextApiRequest, res: NextApiResponse) { async function put(req: NextApiRequest, res: NextApiResponse) {
@@ -38,7 +38,7 @@ async function put(req: NextApiRequest, res: NextApiResponse) {
const workflow = req.body; const workflow = req.body;
if (id && workflow) { if (id && workflow) {
await updateApprovalWorkflow(id, workflow); await updateConfiguredWorkflow(workflow);
return res.status(204).end(); return res.status(204).end();
} }
} }

View File

@@ -1,13 +1,19 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { ApprovalWorkflow } from "@/interfaces/approval.workflow"; import { ApprovalWorkflow } from "@/interfaces/approval.workflow";
import { Entity } from "@/interfaces/entity";
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import { createApprovalWorkflows } from "@/utils/approval.workflows.be"; import { replaceConfiguredWorkflowsByEntities } from "@/utils/approval.workflows.be";
import { withIronSessionApiRoute } from "iron-session/next"; import { withIronSessionApiRoute } from "iron-session/next";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
export default withIronSessionApiRoute(handler, sessionOptions); export default withIronSessionApiRoute(handler, sessionOptions);
interface ReplaceApprovalWorkflowsRequest {
filteredWorkflows: ApprovalWorkflow[];
userEntitiesWithLabel: Entity[];
}
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return await post(req, res); if (req.method === "POST") return await post(req, res);
} }
@@ -20,9 +26,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
return res.status(403).json({ ok: false }); return res.status(403).json({ ok: false });
} }
const approvalWorkflows: ApprovalWorkflow[] = req.body; const { filteredWorkflows, userEntitiesWithLabel } = req.body as ReplaceApprovalWorkflowsRequest;
await createApprovalWorkflows(approvalWorkflows); const configuredWorkflows: ApprovalWorkflow[] = filteredWorkflows;
const entitiesIds: string[] = userEntitiesWithLabel.map((e) => e.id);
return res.status(201).json(approvalWorkflows); await replaceConfiguredWorkflowsByEntities(configuredWorkflows, entitiesIds);
return res.status(201).json({ ok: true });
} }

View File

@@ -22,9 +22,9 @@ import { useEffect, useState } from "react";
import { BsChevronLeft } from "react-icons/bs"; import { BsChevronLeft } from "react-icons/bs";
import { GrClearOption } from "react-icons/gr"; import { GrClearOption } from "react-icons/gr";
import { toast, ToastContainer } from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import { getApprovalWorkflow } from "@/utils/approval.workflows.be";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import axios from "axios"; import axios from "axios";
import { getConfiguredWorkflow } from "@/utils/approval.workflows.be";
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);
@@ -35,7 +35,7 @@ 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(id); const workflow: ApprovalWorkflow | null = await getConfiguredWorkflow(id);
if (!workflow) if (!workflow)
return redirect("/approval-workflows") return redirect("/approval-workflows")
@@ -91,6 +91,7 @@ export default function Home({ user, workflow, userEntitiesWithLabel, userEntiti
key: step.stepNumber + 999, // just making sure they are unique because new steps that users add will have key=3 key=4 etc key: step.stepNumber + 999, // just making sure they are unique because new steps that users add will have key=3 key=4 etc
stepType: step.stepType, stepType: step.stepType,
stepNumber: step.stepNumber, stepNumber: step.stepNumber,
completed: false,
assignees: step.assignees.map(id => id), assignees: step.assignees.map(id => id),
firstStep: step.firstStep || false, firstStep: step.firstStep || false,
finalStep: step.finalStep || false, finalStep: step.finalStep || false,
@@ -179,8 +180,8 @@ export default function Home({ user, workflow, userEntitiesWithLabel, userEntiti
startDate: Date.now(), startDate: Date.now(),
status: "pending", status: "pending",
steps: [ steps: [
{ key: 9998, stepType: "form-intake", stepNumber: 1, firstStep: true, finalStep: false, assignees: [null] }, { key: 9998, stepType: "form-intake", stepNumber: 1, completed: false, firstStep: true, finalStep: false, assignees: [null] },
{ key: 9999, stepType: "approval-by", stepNumber: 2, firstStep: false, finalStep: true, assignees: [null] }, { key: 9999, stepType: "approval-by", stepNumber: 2, completed: false, firstStep: false, finalStep: true, assignees: [null] },
], ],
}; };
setCloneWorkflow(newWorkflow); setCloneWorkflow(newWorkflow);

View File

@@ -8,18 +8,18 @@ import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser, User }
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import { redirect, serialize } from "@/utils"; import { redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import { getConfiguredWorkflow } from "@/utils/approval.workflows.be";
import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { getEntityUsers } from "@/utils/users.be"; import { getEntityUsers } from "@/utils/users.be";
import axios from "axios";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
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 { BsChevronLeft } from "react-icons/bs"; import { BsChevronLeft } from "react-icons/bs";
import { toast, ToastContainer } from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import axios from "axios";
import { getApprovalWorkflow } from "@/utils/approval.workflows.be";
import { useRouter } from "next/router";
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);
@@ -30,7 +30,7 @@ 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(id); const workflow: ApprovalWorkflow | null = await getConfiguredWorkflow(id);
if (!workflow) if (!workflow)
return redirect("/approval-workflows") return redirect("/approval-workflows")
@@ -62,6 +62,9 @@ export default function Home({ user, workflow, workflowEntityApprovers }: Props)
key: step.stepNumber + 999, // just making sure they are unique because new steps that users add will have key=3 key=4 etc key: step.stepNumber + 999, // just making sure they are unique because new steps that users add will have key=3 key=4 etc
stepType: step.stepType, stepType: step.stepType,
stepNumber: step.stepNumber, stepNumber: step.stepNumber,
completed: step.completed,
completedBy: step.completedBy || undefined,
completedDate: step.completedDate || undefined,
assignees: step.assignees.map(id => id), assignees: step.assignees.map(id => id),
firstStep: step.firstStep || false, firstStep: step.firstStep || false,
finalStep: step.finalStep || false, finalStep: step.finalStep || false,

View File

@@ -11,7 +11,7 @@ import { User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
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 { getConfiguredWorkflow } from "@/utils/approval.workflows.be";
import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { getSpecificUsers, getUser } from "@/utils/users.be"; import { getSpecificUsers, getUser } from "@/utils/users.be";
import axios from "axios"; import axios from "axios";
@@ -23,12 +23,14 @@ import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { BsChevronLeft } from "react-icons/bs"; import { BsChevronLeft } from "react-icons/bs";
import { FaSpinner, FaWpforms } from "react-icons/fa6"; import { FaSpinner, FaWpforms } from "react-icons/fa6";
import { FiSave } from "react-icons/fi";
import { IoMdCheckmarkCircleOutline } from "react-icons/io";
import { IoDocumentTextOutline } from "react-icons/io5";
import { MdOutlineDoubleArrow } from "react-icons/md"; import { MdOutlineDoubleArrow } from "react-icons/md";
import { RiThumbUpLine } from "react-icons/ri"; import { RiThumbUpLine } from "react-icons/ri";
import { toast, ToastContainer } from "react-toastify";
import { IoMdCheckmarkCircleOutline } from "react-icons/io";
import { FiSave } from "react-icons/fi";
import { RxCrossCircled } from "react-icons/rx"; import { RxCrossCircled } from "react-icons/rx";
import { TiEdit } from "react-icons/ti";
import { toast, ToastContainer } from "react-toastify";
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);
@@ -39,7 +41,7 @@ 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(id); const workflow: ApprovalWorkflow | null = await getConfiguredWorkflow(id);
if (!workflow) if (!workflow)
return redirect("/approval-workflows") return redirect("/approval-workflows")
@@ -132,6 +134,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
const updatedWorkflow: ApprovalWorkflow = { const updatedWorkflow: ApprovalWorkflow = {
...workflow, ...workflow,
status: selectedStepIndex === workflow.steps.length - 1 ? "approved" : "pending",
steps: workflow.steps.map((step, index) => steps: workflow.steps.map((step, index) =>
index === selectedStepIndex ? index === selectedStepIndex ?
{ {
@@ -206,6 +209,14 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
}) })
}; };
const handleViewExam = () => {
}
const handleEditExam = () => {
}
return ( return (
<> <>
<Head> <Head>
@@ -244,6 +255,29 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
status={workflow.status} status={workflow.status}
/> />
</div> </div>
<div className="flex flex-row gap-3">
<Button
color="purple"
variant="solid"
onClick={handleViewExam}
padding="px-6 py-2"
className="w-[240px] text-lg flex items-center justify-center gap-2 text-left"
>
<IoDocumentTextOutline />
View Exam
</Button>
<Button
color="purple"
variant="solid"
onClick={handleEditExam}
padding="px-6 py-2"
className="w-[240px] text-lg flex items-center justify-center gap-2 text-left"
>
<TiEdit size={20} />
Edit Exam
</Button>
</div>
{steps.find((step) => !step.completed) === undefined && {steps.find((step) => !step.completed) === undefined &&
<Tip text="All steps in this instance have been completed." /> <Tip text="All steps in this instance have been completed." />
} }
@@ -271,7 +305,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
{/* Side panel */} {/* Side panel */}
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<LayoutGroup key="sidePanel"> <LayoutGroup key="sidePanel">
<section className={`absolute inset-y-0 right-0 h-full bg-mti-purple-ultralight bg-opacity-50 shadow-xl shadow-mti-purple transition-all duration-300 overflow-hidden ${isPanelOpen ? 'w-2/5' : 'w-0'}`}> <section className={`absolute inset-y-0 right-0 h-full bg-mti-purple-ultralight bg-opacity-50 shadow-xl shadow-mti-purple transition-all duration-300 overflow-hidden ${isPanelOpen ? 'w-[500px]' : 'w-0'}`}>
{isPanelOpen && selectedStep && ( {isPanelOpen && selectedStep && (
<motion.div <motion.div
className="p-6" className="p-6"
@@ -412,7 +446,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
) : ( ) : (
<> <>
<RxCrossCircled size={20} /> <RxCrossCircled size={20} />
Reject Step Reject
</> </>
)} )}
</Button> </Button>

View File

@@ -10,6 +10,7 @@ import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser, User }
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import { redirect, serialize } from "@/utils"; import { redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import { getConfiguredWorkflowsByEntities } from "@/utils/approval.workflows.be";
import { getEntities } from "@/utils/entities.be"; import { getEntities } from "@/utils/entities.be";
import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { getEntitiesUsers } from "@/utils/users.be"; import { getEntitiesUsers } from "@/utils/users.be";
@@ -33,10 +34,13 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
return redirect("/") return redirect("/")
const userEntitiesWithLabel = await getEntities(user.entities.map(entity => entity.id)); const userEntitiesWithLabel = await getEntities(user.entities.map(entity => entity.id));
const allConfiguredWorkflows = await getConfiguredWorkflowsByEntities(userEntitiesWithLabel.map(entity => entity.id));
return { return {
props: serialize({ props: serialize({
user, user,
allConfiguredWorkflows,
userEntitiesWithLabel, userEntitiesWithLabel,
userEntitiesApprovers: await getEntitiesUsers(userEntitiesWithLabel.map(entity => entity.id), { type: { $in: ["teacher", "corporate", "mastercorporate", "developer"] } }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[], userEntitiesApprovers: await getEntitiesUsers(userEntitiesWithLabel.map(entity => entity.id), { type: { $in: ["teacher", "corporate", "mastercorporate", "developer"] } }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
}), }),
@@ -45,12 +49,13 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
interface Props { interface Props {
user: User, user: User,
allConfiguredWorkflows: EditableApprovalWorkflow[],
userEntitiesWithLabel: Entity[], userEntitiesWithLabel: Entity[],
userEntitiesApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[], userEntitiesApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
} }
export default function Home({ user, userEntitiesWithLabel, userEntitiesApprovers }: Props) { export default function Home({ user, allConfiguredWorkflows, userEntitiesWithLabel, userEntitiesApprovers }: Props) {
const [workflows, setWorkflows] = useState<EditableApprovalWorkflow[]>([]); const [workflows, setWorkflows] = useState<EditableApprovalWorkflow[]>(allConfiguredWorkflows);
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | undefined>(undefined); const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | undefined>(undefined);
const [entityId, setEntityId] = useState<string | null | undefined>(null); const [entityId, setEntityId] = useState<string | null | undefined>(null);
const [entityApprovers, setEntityApprovers] = useState<(TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]>([]); const [entityApprovers, setEntityApprovers] = useState<(TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]>([]);
@@ -93,13 +98,14 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesApprover
...workflow, ...workflow,
steps: workflow.steps.map(step => ({ steps: workflow.steps.map(step => ({
...step, ...step,
completed: false,
assignees: step.assignees.filter((assignee): assignee is string => assignee !== null && assignee !== undefined) assignees: step.assignees.filter((assignee): assignee is string => assignee !== null && assignee !== undefined)
})) }))
})); }));
const requestData = {filteredWorkflows, userEntitiesWithLabel};
axios axios
.post(`/api/approval-workflows/create`, filteredWorkflows) .post(`/api/approval-workflows/create`, requestData)
.then(() => { .then(() => {
toast.success("Approval Workflows created successfully."); toast.success("Approval Workflows created successfully.");
setIsRedirecting(true); setIsRedirecting(true);
@@ -135,8 +141,8 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesApprover
startDate: Date.now(), startDate: Date.now(),
status: "pending", status: "pending",
steps: [ steps: [
{ key: 9998, stepType: "form-intake", stepNumber: 1, firstStep: true, finalStep: false, assignees: [null] }, { key: 9998, stepType: "form-intake", stepNumber: 1, completed: false, firstStep: true, finalStep: false, assignees: [null] },
{ key: 9999, stepType: "approval-by", stepNumber: 2, firstStep: false, finalStep: true, assignees: [null] }, { key: 9999, stepType: "approval-by", stepNumber: 2, completed: false, firstStep: false, finalStep: true, assignees: [null] },
], ],
}; };
setWorkflows((prev) => [...prev, newWorkflow]); setWorkflows((prev) => [...prev, newWorkflow]);

View File

@@ -1,5 +1,6 @@
import Layout from "@/components/High/Layout"; 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 Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
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";
@@ -8,6 +9,7 @@ import { User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import { redirect, serialize } from "@/utils"; import { redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import { getConfiguredWorkflows } from "@/utils/approval.workflows.be";
import { getEntities } from "@/utils/entities.be"; import { getEntities } from "@/utils/entities.be";
import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { getSpecificUsers } from "@/utils/users.be"; import { getSpecificUsers } from "@/utils/users.be";
@@ -17,18 +19,14 @@ 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 { FaRegClone } from "react-icons/fa6";
import { IoIosAddCircleOutline } from "react-icons/io"; import { IoIosAddCircleOutline } from "react-icons/io";
import { toast, ToastContainer } from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import Input from "@/components/Low/Input";
import { FaRegClone } from "react-icons/fa6";
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
import { getApprovalWorkflows } from "@/utils/approval.workflows.be";
import { useRouter } from "next/router";
const columnHelper = createColumnHelper<ApprovalWorkflow>(); const columnHelper = createColumnHelper<ApprovalWorkflow>();
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
@@ -38,7 +36,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type))
return redirect("/") return redirect("/")
const workflows = await getApprovalWorkflows(); const workflows = await getConfiguredWorkflows();
const allAssigneeIds: string[] = [ const allAssigneeIds: string[] = [
...new Set( ...new Set(
@@ -331,7 +329,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
className="min-w-fit text-lg font-medium flex items-center gap-2 text-left" className="min-w-fit text-lg font-medium flex items-center gap-2 text-left"
> >
<IoIosAddCircleOutline className="size-6" /> <IoIosAddCircleOutline className="size-6" />
Configure New Workflows Configure Workflows
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@@ -1,36 +1,83 @@
import { ObjectId } from "mongodb";
import { ApprovalWorkflow } from "@/interfaces/approval.workflow"; import { ApprovalWorkflow } from "@/interfaces/approval.workflow";
import client from "@/lib/mongodb"; import client from "@/lib/mongodb";
import { ObjectId } from "mongodb";
const db = client.db(process.env.MONGODB_DB); const db = client.db(process.env.MONGODB_DB);
export const getApprovalWorkflows = async (ids?: string[]) => { export const getConfiguredWorkflows = async (ids?: string[]) => {
return await db return await db
.collection<ApprovalWorkflow>("approval-workflows") .collection<ApprovalWorkflow>("configured-workflows")
.find(ids ? { _id: { $in: ids.map((id) => new ObjectId(id)) } } : {}) .find(ids ? { _id: { $in: ids.map((id) => new ObjectId(id)) } } : {})
.toArray(); .toArray();
}; };
export const getApprovalWorkflow = async (id: string) => { export const getConfiguredWorkflow = async (id: string) => {
return await db.collection<ApprovalWorkflow>("approval-workflows").findOne({ _id: new ObjectId(id) }); return await db.collection<ApprovalWorkflow>("configured-workflows").findOne({ _id: new ObjectId(id) });
}; };
export const createApprovalWorkflow = async (workflow: ApprovalWorkflow) => { export const getConfiguredWorkflowsByEntities = async (ids: string[]) => {
const { _id, ...workflowWithoutId } = workflow as ApprovalWorkflow; return await db
return await db.collection("approval-workflows").insertOne(workflowWithoutId); .collection<ApprovalWorkflow>("configured-workflows")
.find({ entityId: { $in: ids } })
.toArray();
}; };
export const createApprovalWorkflows = async (workflows: ApprovalWorkflow[]) => { export const createConfiguredWorkflow = async (workflow: ApprovalWorkflow) => {
const { _id, ...workflowWithoutId } = workflow as ApprovalWorkflow;
return await db.collection("configured-workflows").insertOne(workflowWithoutId);
};
export const createConfiguredWorkflows = async (workflows: ApprovalWorkflow[]) => {
if (workflows.length === 0) return; if (workflows.length === 0) return;
const workflowsWithoutIds: ApprovalWorkflow[] = workflows.map(({_id, ...wfs}) => wfs) const workflowsWithoutIds: ApprovalWorkflow[] = workflows.map(({ _id, ...wfs }) => wfs);
return await db.collection("approval-workflows").insertMany(workflowsWithoutIds); return await db.collection("configured-workflows").insertMany(workflowsWithoutIds);
}; };
export const updateApprovalWorkflow = async (id: string, workflow: ApprovalWorkflow) => { export const updateConfiguredWorkflow = async (workflow: ApprovalWorkflow) => {
const { _id, ...workflowWithoutId } = workflow as ApprovalWorkflow; const { _id, ...workflowWithoutId } = workflow as ApprovalWorkflow;
return await db.collection("approval-workflows").replaceOne({ _id: new ObjectId(id) }, workflowWithoutId); return await db.collection("configured-workflows").replaceOne({ _id: new ObjectId(_id) }, workflowWithoutId);
}; };
export const deleteApprovalWorkflow = async (id: string) => { export const updateConfiguredWorkflows = async (workflows: ApprovalWorkflow[]) => {
return await db.collection("approval-workflows").deleteOne({ _id: new ObjectId(id) }); const bulkOperations = workflows.map((workflow) => {
const { _id, ...workflowWithoutId } = workflow;
return {
replaceOne: {
filter: { _id: new ObjectId(_id) },
replacement: workflowWithoutId,
},
};
});
return await db.collection("configured-workflows").bulkWrite(bulkOperations);
};
export const deleteConfiguredWorkflow = async (id: string) => {
return await db.collection("configured-workflows").deleteOne({ _id: new ObjectId(id) });
};
export const replaceConfiguredWorkflowsByEntities = async (workflows: ApprovalWorkflow[], entityIds: string[]) => {
// 1. Keep track of the _id values of all workflows we want to end up with
const finalIds = new Set<string>();
// 2. Process incoming workflows
for (const workflow of workflows) {
if (workflow._id) {
// Replace the existing ones
await updateConfiguredWorkflow(workflow);
finalIds.add(workflow._id.toString());
} else {
// Insert if no _id
const insertResult = await createConfiguredWorkflow(workflow);
finalIds.add(insertResult.insertedId.toString());
}
}
// 3. Delete any existing workflow (within these entityIds) that wasn't in the final list
await db.collection("configured-workflows").deleteMany({
_id: {
$nin: Array.from(finalIds).map((id) => new ObjectId(id)),
},
entityId: { $in: entityIds },
});
}; };