implement edit active workflow and do not allow editing on already completed steps

This commit is contained in:
Joao Correia
2025-02-05 00:43:49 +00:00
parent b215885dc6
commit f4c7961caa
5 changed files with 122 additions and 93 deletions

View File

@@ -12,6 +12,7 @@ import WorkflowStepSelects from "./WorkflowStepSelects";
interface Props extends Pick<EditableWorkflowStep, 'stepNumber' | 'assignees' | 'finalStep' | 'onDelete'> { interface Props extends Pick<EditableWorkflowStep, 'stepNumber' | 'assignees' | 'finalStep' | 'onDelete'> {
entityApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]; entityApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[];
onSelectChange: (numberOfSelects: number, index: number, value: Option | null) => void; onSelectChange: (numberOfSelects: number, index: number, value: Option | null) => void;
isCompleted: boolean,
} }
export default function WorkflowEditableStepComponent({ export default function WorkflowEditableStepComponent({
@@ -21,6 +22,7 @@ export default function WorkflowEditableStepComponent({
onDelete, onDelete,
onSelectChange, onSelectChange,
entityApprovers, entityApprovers,
isCompleted,
}: Props) { }: Props) {
const [selects, setSelects] = useState<(Option | null | undefined)[]>([null]); const [selects, setSelects] = useState<(Option | null | undefined)[]>([null]);
@@ -95,7 +97,7 @@ export default function WorkflowEditableStepComponent({
)} )}
</div> </div>
{stepNumber !== 1 && !finalStep {stepNumber !== 1 && !finalStep && !isCompleted
? <LuGripHorizontal className="ml-3 mt-2 cursor-grab active:cursor-grabbing min-w-[25px] min-h-[25px]" /> ? <LuGripHorizontal className="ml-3 mt-2 cursor-grab active:cursor-grabbing min-w-[25px] min-h-[25px]" />
: <div className="ml-3 mt-2" style={{ width: 25, height: 25 }}></div> : <div className="ml-3 mt-2" style={{ width: 25, height: 25 }}></div>
} }
@@ -106,6 +108,7 @@ export default function WorkflowEditableStepComponent({
selects={selects} selects={selects}
placeholder={stepNumber === 1 ? "Form Intake By:" : "Approval By:"} placeholder={stepNumber === 1 ? "Form Intake By:" : "Approval By:"}
onSelectChange={handleSelectChangeAt} onSelectChange={handleSelectChangeAt}
isCompleted={isCompleted}
/> />
</div> </div>

View File

@@ -1,8 +1,7 @@
import { EditableApprovalWorkflow, EditableWorkflowStep } from "@/interfaces/approval.workflow"; import { EditableApprovalWorkflow, EditableWorkflowStep } from "@/interfaces/approval.workflow";
import Option from "@/interfaces/option"; import Option from "@/interfaces/option";
import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser } from "@/interfaces/user"; import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser } from "@/interfaces/user";
import { AnimatePresence, Reorder } from "framer-motion"; import { AnimatePresence, Reorder, motion } from "framer-motion";
import { useState } from "react";
import { FaRegCheckCircle, FaSpinner } from "react-icons/fa"; import { FaRegCheckCircle, FaSpinner } from "react-icons/fa";
import { IoIosAddCircleOutline } from "react-icons/io"; import { IoIosAddCircleOutline } from "react-icons/io";
import Button from "../Low/Button"; import Button from "../Low/Button";
@@ -13,9 +12,9 @@ interface Props {
workflow: EditableApprovalWorkflow; workflow: EditableApprovalWorkflow;
onWorkflowChange: (workflow: EditableApprovalWorkflow) => void; onWorkflowChange: (workflow: EditableApprovalWorkflow) => void;
entityApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]; entityApprovers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[];
entityAvailableFormIntakers: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[]; entityAvailableFormIntakers?: (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[];
isLoading: boolean; isLoading: boolean;
isRedirecting: boolean; isRedirecting?: boolean;
} }
export default function WorkflowForm({ workflow, onWorkflowChange, entityApprovers, entityAvailableFormIntakers, isLoading, isRedirecting }: Props) { export default function WorkflowForm({ workflow, onWorkflowChange, entityApprovers, entityAvailableFormIntakers, isLoading, isRedirecting }: Props) {
@@ -75,19 +74,15 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityApprove
}; };
const handleReorder = (newOrder: EditableWorkflowStep[]) => { const handleReorder = (newOrder: EditableWorkflowStep[]) => {
const firstIndex = newOrder.findIndex((s) => s.firstStep); let draggableIndex = 0;
if (firstIndex !== -1 && firstIndex !== 0) { const updatedSteps = workflow.steps.map((step) => {
const [first] = newOrder.splice(firstIndex, 1); if (!step.firstStep && !step.finalStep && !step.completed) {
newOrder.unshift(first); return newOrder[draggableIndex++];
} }
// Keep static steps as-is
const finalIndex = newOrder.findIndex((s) => s.finalStep); return step;
if (finalIndex !== -1 && finalIndex !== newOrder.length - 1) { });
const [final] = newOrder.splice(finalIndex, 1); onWorkflowChange({ ...workflow, steps: renumberSteps(updatedSteps) });
newOrder.push(final);
}
onWorkflowChange({ ...workflow, steps: renumberSteps(newOrder) });
}; };
@@ -118,55 +113,87 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityApprove
className="flex flex-col gap-0" className="flex flex-col gap-0"
> >
<AnimatePresence> <AnimatePresence>
{workflow.steps.map((step, index) => ( {workflow.steps.map((step, index) =>
<Reorder.Item step.completed || step.firstStep || step.finalStep ? (
key={step.key} <motion.div
value={step} key={step.key}
initial={{ opacity: 0, y: -30 }} layout
animate={{ opacity: 1, y: 0 }} initial={{ opacity: 0, y: -30 }}
exit={{ opacity: 0, x: 30 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.20 }} exit={{ opacity: 0, x: 30 }}
layout transition={{ duration: 0.20 }}
drag={!(step.firstStep || step.finalStep)} >
> <WorkflowEditableStepComponent
stepNumber={index + 1}
<WorkflowEditableStepComponent assignees={step.assignees}
stepNumber={index + 1} finalStep={step.finalStep}
assignees={step.assignees} onDelete={() => handleDelete(step.key)}
finalStep={step.finalStep} onSelectChange={(numberOfSelects, idx, option) =>
onDelete={() => handleDelete(step.key)} handleSelectChange(step.key, numberOfSelects, idx, option)
onSelectChange={(numberOfSelects, idx, option) => handleSelectChange(step.key, numberOfSelects, idx, option)} }
entityApprovers={step.stepNumber === 1 ? entityAvailableFormIntakers : entityApprovers} entityApprovers={
/> step.stepNumber === 1 && entityAvailableFormIntakers
? entityAvailableFormIntakers
{step.finalStep && : entityApprovers
<Button }
type="submit" isCompleted={step.completed}
color="purple" />
variant="solid" </motion.div>
disabled={isLoading} ) : (
className="max-w-fit text-lg font-medium flex items-center gap-2 text-left -mt-4" // Render non-completed steps as draggable items
> <Reorder.Item
{isRedirecting ? ( key={step.key}
<> value={step}
<FaSpinner className="animate-spin size-5" /> initial={{ opacity: 0, y: -30 }}
Redirecting... animate={{ opacity: 1, y: 0 }}
</> exit={{ opacity: 0, x: 30 }}
) : isLoading ? ( transition={{ duration: 0.20 }}
<> layout
<FaSpinner className="animate-spin size-5" /> drag={!step.firstStep && !step.finalStep}
Loading... dragListener={!step.firstStep && !step.finalStep}
</> >
) : ( <WorkflowEditableStepComponent
<> stepNumber={index + 1}
<FaRegCheckCircle className="size-5" /> assignees={step.assignees}
Confirm Exam Workflow Pipeline finalStep={step.finalStep}
</> onDelete={() => handleDelete(step.key)}
)} onSelectChange={(numberOfSelects, idx, option) =>
</Button> handleSelectChange(step.key, numberOfSelects, idx, option)
} }
</Reorder.Item> entityApprovers={
))} step.stepNumber === 1 && entityAvailableFormIntakers
? entityAvailableFormIntakers
: entityApprovers
}
isCompleted={step.completed}
/>
</Reorder.Item>
)
)}
<Button
type="submit"
color="purple"
variant="solid"
disabled={isLoading}
className="max-w-fit text-lg font-medium flex items-center gap-2 text-left -mt-4"
>
{isRedirecting ? (
<>
<FaSpinner className="animate-spin size-5" />
Redirecting...
</>
) : isLoading ? (
<>
<FaSpinner className="animate-spin size-5" />
Loading...
</>
) : (
<>
<FaRegCheckCircle className="size-5" />
Confirm Exam Workflow Pipeline
</>
)}
</Button>
</AnimatePresence> </AnimatePresence>
</Reorder.Group> </Reorder.Group>
</div> </div>

View File

@@ -6,6 +6,7 @@ interface Props {
selects: (Option | null | undefined)[]; selects: (Option | null | undefined)[];
placeholder: string; placeholder: string;
onSelectChange: (numberOfSelects: number, index: number, value: Option | null) => void; onSelectChange: (numberOfSelects: number, index: number, value: Option | null) => void;
isCompleted: boolean;
} }
export default function WorkflowStepSelects({ export default function WorkflowStepSelects({
@@ -13,6 +14,7 @@ export default function WorkflowStepSelects({
selects, selects,
placeholder, placeholder,
onSelectChange, onSelectChange,
isCompleted,
}: Props) { }: Props) {
return ( return (
@@ -39,6 +41,7 @@ export default function WorkflowStepSelects({
flat flat
isClearable isClearable
className={classes} className={classes}
disabled={isCompleted}
/> />
</div> </div>
); );

View File

@@ -26,7 +26,7 @@ async function put(req: NextApiRequest, res: NextApiResponse) {
if (id && approvalWorkflow) { if (id && approvalWorkflow) {
approvalWorkflow._id = new ObjectId(id); approvalWorkflow._id = new ObjectId(id);
await updateApprovalWorkflow("configured-workflows", approvalWorkflow); await updateApprovalWorkflow("active-workflows", approvalWorkflow);
return res.status(204).end(); return res.status(204).end();
} }
} }

View File

@@ -12,11 +12,10 @@ import { getApprovalWorkflow } 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 axios from "axios";
import { motion } from "framer-motion"; import { 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 { 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";
@@ -30,7 +29,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("configured-workflows", id); const workflow: ApprovalWorkflow | null = await getApprovalWorkflow("active-workflows", id);
if (!workflow) if (!workflow)
return redirect("/approval-workflows") return redirect("/approval-workflows")
@@ -53,9 +52,6 @@ interface Props {
export default function Home({ user, workflow, workflowEntityApprovers }: Props) { export default function Home({ user, workflow, workflowEntityApprovers }: Props) {
const [updatedWorkflow, setUpdatedWorkflow] = useState<EditableApprovalWorkflow | null>(null); const [updatedWorkflow, setUpdatedWorkflow] = useState<EditableApprovalWorkflow | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [isRedirecting, setIsRedirecting] = useState<boolean>(false);
const router = useRouter();
useEffect(() => { useEffect(() => {
const editableSteps: EditableWorkflowStep[] = workflow.steps.map(step => ({ const editableSteps: EditableWorkflowStep[] = workflow.steps.map(step => ({
@@ -114,8 +110,7 @@ export default function Home({ user, workflow, workflowEntityApprovers }: Props)
.put(`/api/approval-workflows/${updatedWorkflow.id}/edit`, filteredWorkflow) .put(`/api/approval-workflows/${updatedWorkflow.id}/edit`, filteredWorkflow)
.then(() => { .then(() => {
toast.success("Approval Workflow edited successfully."); toast.success("Approval Workflow edited successfully.");
setIsRedirecting(true); setIsLoading(false);
router.push("/approval-workflows");
}) })
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 401) { if (reason.response.status === 401) {
@@ -175,23 +170,24 @@ export default function Home({ user, workflow, workflowEntityApprovers }: Props)
</section> </section>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<motion.div <LayoutGroup key={workflow.name}>
key="form" <motion.div
initial={{ opacity: 0, y: -30 }} key="form"
animate={{ opacity: 1, y: 0 }} initial={{ opacity: 0, y: -30 }}
exit={{ opacity: 0, x: 60 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.20 }} exit={{ opacity: 0, x: 60 }}
> transition={{ duration: 0.20 }}
{/* {updatedWorkflow && >
<WorkflowForm {updatedWorkflow &&
workflow={updatedWorkflow} <WorkflowForm
onWorkflowChange={onWorkflowChange} workflow={updatedWorkflow}
entityApprovers={workflowEntityApprovers} onWorkflowChange={onWorkflowChange}
isLoading={isLoading} entityApprovers={workflowEntityApprovers}
isRedirecting={isRedirecting} isLoading={isLoading}
/> />
} */} }
</motion.div> </motion.div>
</LayoutGroup>
</form> </form>
</Layout> </Layout>
)} )}