implement rejection of steps

This commit is contained in:
Joao Correia
2025-01-31 20:56:40 +00:00
parent 662e3b0266
commit a0229cd971
8 changed files with 170 additions and 126 deletions

View File

@@ -17,6 +17,7 @@ export default function WorkflowStepComponent({
stepType, stepType,
stepNumber, stepNumber,
completed, completed,
rejected = false,
completedBy, completedBy,
assignees, assignees,
finalStep, finalStep,
@@ -31,11 +32,12 @@ export default function WorkflowStepComponent({
<div <div
onClick={onClick} onClick={onClick}
className={clsx("flex flex-row gap-5 w-[600px] p-6 mb-5 rounded-2xl transition ease-in-out duration-300 cursor-pointer", { className={clsx("flex flex-row gap-5 w-[600px] p-6 mb-5 rounded-2xl transition ease-in-out duration-300 cursor-pointer", {
"bg-mti-red-ultralight": rejected && selected,
"bg-mti-purple-ultralight": selected, "bg-mti-purple-ultralight": selected,
})} })}
> >
<div className="relative flex flex-col items-center"> <div className="relative flex flex-col items-center">
<WorkflowStepNumber stepNumber={stepNumber} selected={selected} completed={completed} finalStep={finalStep} /> <WorkflowStepNumber stepNumber={stepNumber} selected={selected} completed={completed} finalStep={finalStep} rejected={rejected} />
{/* Vertical Bar connecting steps */} {/* Vertical Bar connecting steps */}
{!finalStep && ( {!finalStep && (
@@ -53,71 +55,45 @@ export default function WorkflowStepComponent({
</div> </div>
<div className="mt-1 flex flex-col gap-0"> <div className="mt-1 flex flex-col gap-0">
{stepType === "form-intake" ? ( {completed && completedBy && rejected ? (
<> <div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
<p className="text-sm font-medium text-gray-800">Form: Intake</p> <p className="text-sm font-medium text-gray-800">{stepType === "approval-by" ? `Approval: ${getUserTypeLabel(completedByUser!.type)} Approval` : `Form Intake: ${getUserTypeLabel(completedByUser!.type)} Intake`} </p>
{completed && completedBy && ( <UserWithProfilePic
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}> prefix={`Rejected by: ${getUserTypeLabelShort(completedByUser!.type)}`}
<UserWithProfilePic name={completedByUser!.name}
prefix={`Completed by: ${getUserTypeLabelShort(completedByUser!.type)}`} profileImage={completedByUser!.profilePicture}
name={completedByUser!.name} />
profileImage={completedByUser!.profilePicture} </div>
/> ) : completed && completedBy && !rejected ? (
</div> <div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
)} <p className="text-sm font-medium text-gray-800">{stepType === "approval-by" ? `Approval: ${getUserTypeLabel(completedByUser!.type)} Approval` : `Form Intake: ${getUserTypeLabel(completedByUser!.type)} Intake`} </p>
{!completed && currentStep && ( <UserWithProfilePic
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}> prefix={`Completed by: ${getUserTypeLabelShort(completedByUser!.type)}`}
In Progress... Assignees: name={completedByUser!.name}
<div className="flex flex-row flex-wrap gap-3 items-center"> profileImage={completedByUser!.profilePicture}
{assigneesUsers.map(user => ( />
<span key={user.id}> </div>
<UserWithProfilePic ) : !completed && currentStep ? (
prefix={getUserTypeLabelShort(user.type)} <div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
name={user.name} <p className="text-sm font-medium text-gray-800">{stepType === "approval-by" ? `Approval:` : `Form Intake:`} </p>
profileImage={user.profilePicture} In Progress... Assignees:
/> <div className="flex flex-row flex-wrap gap-3 items-center">
</span> {assigneesUsers.map(user => (
))} <span key={user.id}>
</div>
</div>
)}
</>
) : (
stepType === "approval-by" && (
<>
{completed && completedBy ? (
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
<p className="text-sm font-medium text-gray-800">Approval: {getUserTypeLabel(completedByUser!.type)} Approval</p>
<UserWithProfilePic <UserWithProfilePic
prefix={`Approved by: ${getUserTypeLabelShort(completedByUser!.type)}`} prefix={getUserTypeLabelShort(user.type)}
name={completedByUser!.name} name={user.name}
profileImage={completedByUser!.profilePicture} profileImage={user.profilePicture}
/> />
</div> </span>
) : !completed && currentStep ? ( ))}
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}> </div>
<p className="text-sm font-medium text-gray-800">Approval: </p> </div>
In Progress... Assignees: ) : (
<div className="flex flex-row flex-wrap gap-3 items-center"> <div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
{assigneesUsers.map(user => ( <p className="text-sm font-medium text-gray-800">{stepType === "approval-by" ? `Approval:` : `Form Intake:`} </p>
<span key={user.id}> Waiting for previous steps...
<UserWithProfilePic </div>
prefix={getUserTypeLabelShort(user.type)}
name={user.name}
profileImage={user.profilePicture}
/>
</span>
))}
</div>
</div>
) : (
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
<p className="text-sm font-medium text-gray-800">Approval: </p>
Waiting for previous steps...
</div>
)}
</>
)
)} )}
</div> </div>
</div> </div>

View File

@@ -1,21 +1,25 @@
import { WorkflowStep } from "@/interfaces/approval.workflow"; import { WorkflowStep } from "@/interfaces/approval.workflow";
import clsx from "clsx"; import clsx from "clsx";
import { IoCheckmarkDoneSharp, IoCheckmarkSharp } from "react-icons/io5"; import { IoCheckmarkDoneSharp, IoCheckmarkSharp } from "react-icons/io5";
import { RxCross2 } from "react-icons/rx";
type Props = Pick<WorkflowStep, 'stepNumber' | 'completed' | 'finalStep' | 'selected'> type Props = Pick<WorkflowStep, 'stepNumber' | 'completed' | 'finalStep' | 'selected' | 'rejected'>
export default function WorkflowStepNumber({ stepNumber, selected = false, completed, finalStep }: Props) { export default function WorkflowStepNumber({ stepNumber, selected = false, completed, rejected, finalStep }: Props) {
return ( return (
<div <div
className={clsx( className={clsx(
'flex items-center justify-center min-w-11 min-h-11 rounded-full', 'flex items-center justify-center min-w-11 min-h-11 rounded-full',
{ {
'bg-mti-red-dark text-mti-red-ultralight': rejected,
'bg-mti-purple-dark text-mti-purple-ultralight': selected, 'bg-mti-purple-dark text-mti-purple-ultralight': selected,
'bg-mti-purple-ultralight text-gray-500': !selected, 'bg-mti-purple-ultralight text-gray-500': !selected,
} }
)} )}
> >
{completed && finalStep ? ( {rejected ? (
<RxCross2 className="text-xl font-bold" size={25}/>
) : completed && finalStep ? (
<IoCheckmarkDoneSharp className="text-xl font-bold" size={25} /> <IoCheckmarkDoneSharp className="text-xl font-bold" size={25} />
) : completed && !finalStep ? ( ) : completed && !finalStep ? (
<IoCheckmarkSharp className="text-xl font-bold" size={25} /> <IoCheckmarkSharp className="text-xl font-bold" size={25} />

View File

@@ -28,6 +28,7 @@ export interface WorkflowStep {
stepType: StepType, stepType: StepType,
stepNumber: number, stepNumber: number,
completed: boolean, completed: boolean,
rejected?: boolean,
completedBy?: User["id"], completedBy?: User["id"],
completedDate?: number, completedDate?: number,
assignees: (User["id"])[]; assignees: (User["id"])[];

View File

@@ -47,7 +47,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
user, user,
workflow, workflow,
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)[],
}), }),
}; };
}, sessionOptions); }, sessionOptions);
@@ -134,9 +134,7 @@ export default function Home({ user, workflow, userEntitiesWithLabel, userEntiti
.then(() => { .then(() => {
toast.success("Approval Workflow cloned successfully."); toast.success("Approval Workflow cloned successfully.");
setIsRedirecting(true); setIsRedirecting(true);
setTimeout(() => { router.push("/approval-workflows");
router.push("/approval-workflows");
}, 1000);
}) })
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 401) { if (reason.response.status === 401) {

View File

@@ -39,7 +39,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
props: serialize({ props: serialize({
user, user,
workflow, workflow,
workflowEntityApprovers: await getEntityUsers(workflow.entityId, undefined, { type: {$in: ["teacher", "corporate", "mastercorporate", "developer"]} }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[], workflowEntityApprovers: await getEntityUsers(workflow.entityId, undefined, { type: { $in: ["teacher", "corporate", "mastercorporate", "developer"] } }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
}), }),
}; };
}, sessionOptions); }, sessionOptions);
@@ -54,7 +54,7 @@ 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 [isRedirecting, setIsRedirecting] = useState<boolean>(false);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
@@ -86,10 +86,10 @@ export default function Home({ user, workflow, workflowEntityApprovers }: Props)
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
if (!updatedWorkflow){ if (!updatedWorkflow) {
setIsLoading(false); setIsLoading(false);
return; return;
} }
const filteredWorkflow: ApprovalWorkflow = { const filteredWorkflow: ApprovalWorkflow = {
...updatedWorkflow, ...updatedWorkflow,
@@ -99,15 +99,13 @@ export default function Home({ user, workflow, workflowEntityApprovers }: Props)
assignees: step.assignees.filter((assignee): assignee is string => assignee !== null && assignee !== undefined) assignees: step.assignees.filter((assignee): assignee is string => assignee !== null && assignee !== undefined)
})) }))
}; };
axios axios
.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); setIsRedirecting(true);
setTimeout(() => { router.push("/approval-workflows");
router.push("/approval-workflows");
}, 1000);
}) })
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 401) { if (reason.response.status === 401) {

View File

@@ -28,6 +28,7 @@ import { RiThumbUpLine } from "react-icons/ri";
import { toast, ToastContainer } from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import { IoMdCheckmarkCircleOutline } from "react-icons/io"; import { IoMdCheckmarkCircleOutline } from "react-icons/io";
import { FiSave } from "react-icons/fi"; import { FiSave } from "react-icons/fi";
import { RxCrossCircled } from "react-icons/rx";
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);
@@ -71,7 +72,7 @@ interface Props {
export default function Home({ user, workflow, workflowAssignees, workflowRequester }: Props) { export default function Home({ user, workflow, workflowAssignees, workflowRequester }: Props) {
const steps = workflow.steps; const steps = workflow.steps;
let currentStep = steps.findIndex(step => !step.completed); let currentStep = steps.findIndex(step => !step.completed || step.rejected);
if (currentStep === -1) if (currentStep === -1)
currentStep = steps.length - 1; currentStep = steps.length - 1;
@@ -110,9 +111,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
.then(() => { .then(() => {
toast.success("Comments saved successfully."); toast.success("Comments saved successfully.");
setIsRedirecting(true); setIsRedirecting(true);
setTimeout(() => { router.reload();
router.reload();
}, 1000);
}) })
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 401) { if (reason.response.status === 401) {
@@ -129,7 +128,6 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
}; };
const handleApproveStep = () => { const handleApproveStep = () => {
if (!confirm(`Are you sure you want to approve this step?`)) return;
setIsLoading(true); setIsLoading(true);
const updatedWorkflow: ApprovalWorkflow = { const updatedWorkflow: ApprovalWorkflow = {
@@ -151,9 +149,48 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
.then(() => { .then(() => {
toast.success("Step approved successfully."); toast.success("Step approved successfully.");
setIsRedirecting(true); setIsRedirecting(true);
setTimeout(() => { router.reload();
router.reload(); })
}, 1000); .catch((reason) => {
if (reason.response.status === 401) {
toast.error("Not logged in!");
} else if (reason.response.status === 403) {
toast.error("You do not have permission to approve this step!");
} else {
toast.error("Something went wrong, please try again later.");
}
setIsLoading(false);
console.log("Submitted Values:", updatedWorkflow);
return;
})
};
const handleRejectStep = () => {
if (!confirm(`Are you sure you want to reject this step?`)) return;
setIsLoading(true);
const updatedWorkflow: ApprovalWorkflow = {
...workflow,
status: "rejected",
steps: workflow.steps.map((step, index) =>
index === selectedStepIndex ?
{
...step,
completed: true,
completedBy: user.id,
completedDate: Date.now(),
rejected: true,
}
: step
)
};
axios
.put(`/api/approval-workflows/${workflow._id}`, updatedWorkflow)
.then(() => {
toast.success("Step rejected successfully.");
setIsRedirecting(true);
router.reload();
}) })
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 401) { if (reason.response.status === 401) {
@@ -219,6 +256,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
key={index} key={index}
completed={step.completed} completed={step.completed}
completedBy={step.completedBy} completedBy={step.completedBy}
rejected={step.rejected}
stepNumber={step.stepNumber} stepNumber={step.stepNumber}
stepType={step.stepType} stepType={step.stepType}
assignees={step.assignees} assignees={step.assignees}
@@ -276,7 +314,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
{selectedStep.completed ? ( {selectedStep.completed ? (
<div className={"text-base font-medium text-gray-500 flex flex-col gap-6"}> <div className={"text-base font-medium text-gray-500 flex flex-col gap-6"}>
Approved on {new Date(selectedStep.completedDate!).toLocaleString("en-CA", { {selectedStep.rejected ? "Rejected" : "Approved"} on {new Date(selectedStep.completedDate!).toLocaleString("en-CA", {
year: "numeric", year: "numeric",
month: "2-digit", month: "2-digit",
day: "2-digit", day: "2-digit",
@@ -286,7 +324,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
hour12: false, hour12: false,
}).replace(", ", " at ")} }).replace(", ", " at ")}
<div className="flex flex-row gap-1 text-sm"> <div className="flex flex-row gap-1 text-sm">
<p className="text-base">Approved by:</p> <p className="text-base">{selectedStep.rejected ? "Rejected" : "Approved"} by:</p>
{(() => { {(() => {
const assignee = workflowAssignees.find( const assignee = workflowAssignees.find(
(assignee) => assignee.id === selectedStep.completedBy (assignee) => assignee.id === selectedStep.completedBy
@@ -324,33 +362,61 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
</div> </div>
)} )}
{selectedStepIndex === currentStep && !selectedStep.completed && {selectedStepIndex === currentStep && !selectedStep.completed && !selectedStep.rejected &&
<Button <div className="flex flex-row gap-2 ">
type="submit" <Button
color="purple" type="submit"
variant="solid" color="purple"
disabled={!selectedStep.assignees.includes(user.id) || isLoading} variant="solid"
onClick={handleApproveStep} disabled={!selectedStep.assignees.includes(user.id) || isLoading}
padding="px-6 py-2" onClick={handleApproveStep}
className="mb-3 w-full text-lg flex items-center justify-center gap-2 text-left" padding="px-6 py-2"
> className="mb-3 w-full text-lg flex items-center justify-center gap-2 text-left"
{isRedirecting ? ( >
<> {isRedirecting ? (
<FaSpinner className="animate-spin size-5" /> <>
Reloading... <FaSpinner className="animate-spin size-5" />
</> Reloading...
) : isLoading ? ( </>
<> ) : isLoading ? (
<FaSpinner className="animate-spin size-5" /> <>
Loading... <FaSpinner className="animate-spin size-5" />
</> Loading...
) : ( </>
<> ) : (
<IoMdCheckmarkCircleOutline size={20} /> <>
Approve Step <IoMdCheckmarkCircleOutline size={20} />
</> Approve Step
)} </>
</Button> )}
</Button>
<Button
type="submit"
color="red"
variant="solid"
disabled={!selectedStep.assignees.includes(user.id) || isLoading}
onClick={handleRejectStep}
padding="px-6 py-2"
className="mb-3 w-1/2 text-lg flex items-center justify-center gap-2 text-left"
>
{isRedirecting ? (
<>
<FaSpinner className="animate-spin size-5" />
Reloading...
</>
) : isLoading ? (
<>
<FaSpinner className="animate-spin size-5" />
Loading...
</>
) : (
<>
<RxCrossCircled size={20} />
Reject Step
</>
)}
</Button>
</div>
} }
<hr className="my-4 h-[4px] bg-mti-purple-ultralight rounded-full w-full" /> <hr className="my-4 h-[4px] bg-mti-purple-ultralight rounded-full w-full" />
@@ -361,7 +427,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
placeholder="Input comments here" placeholder="Input comments here"
className="w-full h-64 p-2 border-2 rounded-xl shadow-lg focus:border-mti-purple focus:outline-none mt-3 resize-none" className="w-full h-64 p-2 border-2 rounded-xl shadow-lg focus:border-mti-purple focus:outline-none mt-3 resize-none"
/> />
<Button <Button
type="submit" type="submit"
color="purple" color="purple"

View File

@@ -38,7 +38,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
props: serialize({ props: serialize({
user, user,
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)[],
}), }),
}; };
}, sessionOptions); }, sessionOptions);
@@ -103,9 +103,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesApprover
.then(() => { .then(() => {
toast.success("Approval Workflows created successfully."); toast.success("Approval Workflows created successfully.");
setIsRedirecting(true); setIsRedirecting(true);
setTimeout(() => { router.push("/approval-workflows");
router.push("/approval-workflows");
}, 1000);
}) })
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 401) { if (reason.response.status === 401) {

View File

@@ -155,9 +155,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
.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.`);
setTimeout(() => { router.reload();
router.reload();
}, 1000);
}) })
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 404) { if (reason.response.status === 404) {
@@ -217,6 +215,9 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
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);
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);
@@ -243,10 +244,11 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
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);
return ( return (
<span className="font-medium"> <span className="font-medium">
{currentStep {currentStep && !rejected
? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}` ? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}`
: "Completed"} : "Completed"}
</span> </span>
@@ -259,6 +261,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
cell: ({ row }) => { cell: ({ row }) => {
const steps = row.original.steps; const steps = row.original.steps;
const currentStep = steps.find((step) => !step.completed); 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">
@@ -282,7 +285,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
<FaRegClone className="hover:text-mti-purple-light transition ease-in-out duration-300" /> <FaRegClone className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</Link> </Link>
{currentStep && ( {currentStep && !rejected && (
<Link <Link
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
data-tip="Edit" data-tip="Edit"