use custom hook to rerender workflow instead of reloading full page.

This commit is contained in:
Joao Correia
2025-02-03 12:31:21 +00:00
parent d59b654ac2
commit 19f2193414
3 changed files with 65 additions and 71 deletions

View File

@@ -2,16 +2,16 @@ import { ApprovalWorkflow } from "@/interfaces/approval.workflow";
import axios from "axios"; import axios from "axios";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
export default function useApprovalWorkflows() { export default function useApprovalWorkflow(id: string) {
const [workflows, setWorkflows] = useState<ApprovalWorkflow[]>([]); const [workflow, setWorkflow] = useState<ApprovalWorkflow>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
const getData = useCallback(() => { const getData = useCallback(() => {
setIsLoading(true); setIsLoading(true);
axios axios
.get<ApprovalWorkflow[]>(`/api/approval-workflows`) .get<ApprovalWorkflow>(`/api/approval-workflows/${id}`)
.then((response) => setWorkflows(response.data)) .then((response) => setWorkflow(response.data))
.catch((error) => { .catch((error) => {
setIsError(true); setIsError(true);
}) })
@@ -20,5 +20,5 @@ export default function useApprovalWorkflows() {
useEffect(getData, [getData]); useEffect(getData, [getData]);
return { workflows, isLoading, isError, reload: getData }; return { workflow, isLoading, isError, reload: getData };
} }

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 { deleteApprovalWorkflow, getApprovalWorkflow, updateApprovalWorkflow } from "@/utils/approval.workflows.be";
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";
@@ -12,6 +12,7 @@ export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "DELETE") return await del(req, res); if (req.method === "DELETE") return await del(req, res);
if (req.method === "PUT") return await put(req, res); if (req.method === "PUT") return await put(req, res);
if (req.method === "GET") return await get(req, res);
} }
async function del(req: NextApiRequest, res: NextApiResponse) { async function del(req: NextApiRequest, res: NextApiResponse) {
@@ -44,3 +45,18 @@ async function put(req: NextApiRequest, res: NextApiResponse) {
return res.status(204).end(); return res.status(204).end();
} }
} }
async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res);
if (!user) return res.status(401).json({ ok: false });
if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) {
return res.status(403).json({ ok: false });
}
const { id } = req.query as { id?: string };
if (id) {
return res.status(200).json(await getApprovalWorkflow("configured-workflows", id));
}
}

View File

@@ -6,6 +6,7 @@ import UserWithProfilePic from "@/components/ApprovalWorkflows/UserWithProfilePi
import WorkflowStepComponent from "@/components/ApprovalWorkflows/WorkflowStepComponent"; import WorkflowStepComponent from "@/components/ApprovalWorkflows/WorkflowStepComponent";
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 useApprovalWorkflow from "@/hooks/useApprovalWorkflow";
import { ApprovalWorkflow, getUserTypeLabelShort, WorkflowStep } from "@/interfaces/approval.workflow"; import { ApprovalWorkflow, getUserTypeLabelShort, WorkflowStep } from "@/interfaces/approval.workflow";
import { User } from "@/interfaces/user"; import { User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
@@ -19,7 +20,6 @@ import { AnimatePresence, LayoutGroup, 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 { 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";
@@ -57,7 +57,8 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
return { return {
props: serialize({ props: serialize({
user, user,
workflow, initialWorkflow: workflow,
id,
workflowAssignees: await getSpecificUsers(allAssigneeIds), workflowAssignees: await getSpecificUsers(allAssigneeIds),
workflowRequester: await getUser(workflow.requester), workflowRequester: await getUser(workflow.requester),
}), }),
@@ -66,25 +67,26 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
interface Props { interface Props {
user: User, user: User,
workflow: ApprovalWorkflow, initialWorkflow: ApprovalWorkflow,
id: string,
workflowAssignees: User[], workflowAssignees: User[],
workflowRequester: User, workflowRequester: User,
} }
export default function Home({ user, workflow, workflowAssignees, workflowRequester }: Props) { export default function Home({ user, initialWorkflow, id, workflowAssignees, workflowRequester }: Props) {
const steps = workflow.steps;
let currentStep = steps.findIndex(step => !step.completed || step.rejected); const { workflow, reload, isLoading } = useApprovalWorkflow(id);
const currentWorkflow = workflow || initialWorkflow;
let currentStep = currentWorkflow.steps.findIndex(step => !step.completed || step.rejected);
if (currentStep === -1) if (currentStep === -1)
currentStep = steps.length - 1; currentStep = currentWorkflow.steps.length - 1;
const [selectedStepIndex, setSelectedStepIndex] = useState<number>(currentStep); const [selectedStepIndex, setSelectedStepIndex] = useState<number>(currentStep);
const [selectedStep, setSelectedStep] = useState<WorkflowStep>(steps[selectedStepIndex]); const [selectedStep, setSelectedStep] = useState<WorkflowStep>(currentWorkflow.steps[selectedStepIndex]);
const [isPanelOpen, setIsPanelOpen] = useState(true); const [isPanelOpen, setIsPanelOpen] = useState(true);
const [comments, setComments] = useState<string>(selectedStep.comments || ""); const [comments, setComments] = useState<string>(selectedStep.comments || "");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isRedirecting, setIsRedirecting] = useState<boolean>(false);
const router = useRouter();
const handleStepClick = (index: number, stepInfo: WorkflowStep) => { const handleStepClick = (index: number, stepInfo: WorkflowStep) => {
setSelectedStep(stepInfo); setSelectedStep(stepInfo);
@@ -94,11 +96,9 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
}; };
const handleSaveComments = () => { const handleSaveComments = () => {
setIsLoading(true);
const updatedWorkflow: ApprovalWorkflow = { const updatedWorkflow: ApprovalWorkflow = {
...workflow, ...currentWorkflow,
steps: workflow.steps.map((step, index) => steps: currentWorkflow.steps.map((step, index) =>
index === selectedStepIndex ? index === selectedStepIndex ?
{ {
...step, ...step,
@@ -109,11 +109,10 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
}; };
axios axios
.put(`/api/approval-workflows/${workflow._id}`, updatedWorkflow) .put(`/api/approval-workflows/${id}`, updatedWorkflow)
.then(() => { .then(() => {
toast.success("Comments saved successfully."); toast.success("Comments saved successfully.");
setIsRedirecting(true); reload();
router.reload();
}) })
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 401) { if (reason.response.status === 401) {
@@ -123,19 +122,16 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
} else { } else {
toast.error("Something went wrong, please try again later."); toast.error("Something went wrong, please try again later.");
} }
setIsLoading(false);
console.log("Submitted Values:", updatedWorkflow); console.log("Submitted Values:", updatedWorkflow);
return; return;
}) })
}; };
const handleApproveStep = () => { const handleApproveStep = () => {
setIsLoading(true);
const updatedWorkflow: ApprovalWorkflow = { const updatedWorkflow: ApprovalWorkflow = {
...workflow, ...currentWorkflow,
status: selectedStepIndex === workflow.steps.length - 1 ? "approved" : "pending", status: selectedStepIndex === currentWorkflow.steps.length - 1 ? "approved" : "pending",
steps: workflow.steps.map((step, index) => steps: currentWorkflow.steps.map((step, index) =>
index === selectedStepIndex ? index === selectedStepIndex ?
{ {
...step, ...step,
@@ -148,11 +144,10 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
}; };
axios axios
.put(`/api/approval-workflows/${workflow._id}`, updatedWorkflow) .put(`/api/approval-workflows/${id}`, updatedWorkflow)
.then(() => { .then(() => {
toast.success("Step approved successfully."); toast.success("Step approved successfully.");
setIsRedirecting(true); reload();
router.reload();
}) })
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 401) { if (reason.response.status === 401) {
@@ -162,20 +157,20 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
} else { } else {
toast.error("Something went wrong, please try again later."); toast.error("Something went wrong, please try again later.");
} }
setIsLoading(false);
console.log("Submitted Values:", updatedWorkflow); console.log("Submitted Values:", updatedWorkflow);
return; return;
}) })
handleStepClick(selectedStepIndex + 1, currentWorkflow.steps[selectedStepIndex + 1]);
}; };
const handleRejectStep = () => { const handleRejectStep = () => {
if (!confirm(`Are you sure you want to reject this step?`)) return; if (!confirm(`Are you sure you want to reject this step?`)) return;
setIsLoading(true);
const updatedWorkflow: ApprovalWorkflow = { const updatedWorkflow: ApprovalWorkflow = {
...workflow, ...currentWorkflow,
status: "rejected", status: "rejected",
steps: workflow.steps.map((step, index) => steps: currentWorkflow.steps.map((step, index) =>
index === selectedStepIndex ? index === selectedStepIndex ?
{ {
...step, ...step,
@@ -189,11 +184,10 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
}; };
axios axios
.put(`/api/approval-workflows/${workflow._id}`, updatedWorkflow) .put(`/api/approval-workflows/${id}`, updatedWorkflow)
.then(() => { .then(() => {
toast.success("Step rejected successfully."); toast.success("Step rejected successfully.");
setIsRedirecting(true); reload();
router.reload();
}) })
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 401) { if (reason.response.status === 401) {
@@ -203,7 +197,6 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
} else { } else {
toast.error("Something went wrong, please try again later."); toast.error("Something went wrong, please try again later.");
} }
setIsLoading(false);
console.log("Submitted Values:", updatedWorkflow); console.log("Submitted Values:", updatedWorkflow);
return; return;
}) })
@@ -220,7 +213,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
return ( return (
<> <>
<Head> <Head>
<title> {workflow.name} | EnCoach</title> <title> Workflow | 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."
@@ -238,7 +231,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"> className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
<BsChevronLeft /> <BsChevronLeft />
</Link> </Link>
<h1 className="text-2xl font-semibold">{workflow.name}</h1> <h1 className="text-2xl font-semibold">{currentWorkflow.name}</h1>
</section> </section>
<section className="flex flex-col gap-6"> <section className="flex flex-col gap-6">
@@ -249,10 +242,10 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
profileImage={workflowRequester.profilePicture} profileImage={workflowRequester.profilePicture}
/> />
<StartedOn <StartedOn
date={workflow.startDate} date={currentWorkflow.startDate}
/> />
<Status <Status
status={workflow.status} status={currentWorkflow.status}
/> />
</div> </div>
<div className="flex flex-row gap-3"> <div className="flex flex-row gap-3">
@@ -278,13 +271,13 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
</Button> </Button>
</div> </div>
{steps.find((step) => !step.completed) === undefined && {currentWorkflow.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." />
} }
</section> </section>
<section className="flex flex-col gap-0"> <section className="flex flex-col gap-0">
{steps.map((step, index) => ( {currentWorkflow.steps.map((step, index) => (
<WorkflowStepComponent <WorkflowStepComponent
workflowAssignees={workflowAssignees} workflowAssignees={workflowAssignees}
key={index} key={index}
@@ -294,7 +287,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
stepNumber={step.stepNumber} stepNumber={step.stepNumber}
stepType={step.stepType} stepType={step.stepType}
assignees={step.assignees} assignees={step.assignees}
finalStep={index === steps.length - 1} finalStep={index === currentWorkflow.steps.length - 1}
currentStep={index === currentStep} currentStep={index === currentStep}
selected={index === selectedStepIndex} selected={index === selectedStepIndex}
onClick={() => handleStepClick(index, step)} onClick={() => handleStepClick(index, step)}
@@ -407,12 +400,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
padding="px-6 py-2" padding="px-6 py-2"
className="mb-3 w-full text-lg flex items-center justify-center gap-2 text-left" className="mb-3 w-full text-lg flex items-center justify-center gap-2 text-left"
> >
{isRedirecting ? ( {isLoading ? (
<>
<FaSpinner className="animate-spin size-5" />
Reloading...
</>
) : isLoading ? (
<> <>
<FaSpinner className="animate-spin size-5" /> <FaSpinner className="animate-spin size-5" />
Loading... Loading...
@@ -433,12 +421,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
padding="px-6 py-2" padding="px-6 py-2"
className="mb-3 w-1/2 text-lg flex items-center justify-center gap-2 text-left" className="mb-3 w-1/2 text-lg flex items-center justify-center gap-2 text-left"
> >
{isRedirecting ? ( {isLoading ? (
<>
<FaSpinner className="animate-spin size-5" />
Reloading...
</>
) : isLoading ? (
<> <>
<FaSpinner className="animate-spin size-5" /> <FaSpinner className="animate-spin size-5" />
Loading... Loading...
@@ -471,12 +454,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
padding="px-6 py-2" padding="px-6 py-2"
className="mt-6 mb-3 w-full text-lg flex items-center justify-center gap-2 text-left" className="mt-6 mb-3 w-full text-lg flex items-center justify-center gap-2 text-left"
> >
{isRedirecting ? ( {isLoading ? (
<>
<FaSpinner className="animate-spin size-5" />
Reloading...
</>
) : isLoading ? (
<> <>
<FaSpinner className="animate-spin size-5" /> <FaSpinner className="animate-spin size-5" />
Loading... Loading...