first commit
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
FormField,
|
||||
Label,
|
||||
Separator,
|
||||
Textarea,
|
||||
Input,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "ikoncomponents";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import z from "zod";
|
||||
|
||||
import { getProjectByIdentifierApi } from "@/app/utils/api/projectApi";
|
||||
import { IssueData } from "@/app/utils/interface/issue";
|
||||
import { useAppCache } from "@/app/utils/context/AppCacheContext";
|
||||
import { useParams } from "next/navigation";
|
||||
import { editIssueSchema } from "./schema";
|
||||
import RiskWorkflow from "../../../riskTab/components/riskWorkflow";
|
||||
import IssueWorkflow, { IssueStatus } from "../issueWorkflow";
|
||||
|
||||
interface EditIssueModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
issueData: IssueData;
|
||||
onSave: (updatedData: IssueData) => void;
|
||||
}
|
||||
|
||||
type IssueFormStatus = z.infer<typeof editIssueSchema>["issueStatus"];
|
||||
|
||||
// Map any incoming status (legacy "open", differently-cased, etc.) to a valid
|
||||
// enum value so the edit form doesn't fail zod validation on Save.
|
||||
function normalizeIssueStatus(raw: string = ""): IssueFormStatus {
|
||||
const key = raw.toLowerCase().replace(/[-_]/g, " ").trim();
|
||||
const map: Record<string, IssueFormStatus> = {
|
||||
open: "Active",
|
||||
active: "Active",
|
||||
"on hold": "On hold",
|
||||
onhold: "On hold",
|
||||
resolved: "Resolved",
|
||||
closed: "Resolved",
|
||||
};
|
||||
return map[key] ?? "Active";
|
||||
}
|
||||
|
||||
export default function EditIssueModal({
|
||||
open,
|
||||
onClose,
|
||||
issueData,
|
||||
onSave,
|
||||
}: EditIssueModalProps) {
|
||||
const params = useParams();
|
||||
const { userNameMap } = useAppCache();
|
||||
|
||||
const [assigneeOptions, setAssigneeOptions] = useState<string[]>([]);
|
||||
const [projectIdentifier] = useState(params.projectIdentifier as string);
|
||||
|
||||
type EditIssueFormValues = z.infer<typeof editIssueSchema>;
|
||||
|
||||
const form = useForm<EditIssueFormValues>({
|
||||
resolver: zodResolver(editIssueSchema),
|
||||
defaultValues: {
|
||||
issueStatus: normalizeIssueStatus(issueData.issueStatus),
|
||||
issueDescription: issueData.issueDescription,
|
||||
issueProbability: issueData.issueProbability,
|
||||
issueImpact: issueData.issueImpact,
|
||||
issueOwner: issueData.issueOwner,
|
||||
},
|
||||
});
|
||||
|
||||
const { control, handleSubmit } = form;
|
||||
|
||||
/* ---------- Project Team ---------- */
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const projectData = await getProjectByIdentifierApi(projectIdentifier);
|
||||
|
||||
const {
|
||||
projectTeam = [],
|
||||
projectTeamUnderProjectManager = [],
|
||||
projectManagerDelegates,
|
||||
} = projectData;
|
||||
|
||||
const managerDelegatesArr = projectManagerDelegates
|
||||
? Array.isArray(projectManagerDelegates)
|
||||
? projectManagerDelegates
|
||||
: [projectManagerDelegates]
|
||||
: [];
|
||||
|
||||
const mergedAssignees = [
|
||||
...projectTeam,
|
||||
...projectTeamUnderProjectManager,
|
||||
...managerDelegatesArr,
|
||||
];
|
||||
|
||||
const uniqueAssignees = [...new Set(mergedAssignees)];
|
||||
setAssigneeOptions(uniqueAssignees);
|
||||
} catch (error) {
|
||||
console.error("Error fetching project:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (projectIdentifier) fetchProjects();
|
||||
}, [projectIdentifier]);
|
||||
|
||||
/* ---------- Submit ---------- */
|
||||
const onSubmit = (values: EditIssueFormValues) => {
|
||||
const { issueId, issueCreatedDate, ...cleanIssueData } = issueData;
|
||||
console.log("Edit form ",values)
|
||||
onSave({
|
||||
...cleanIssueData,
|
||||
...values,
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="max-w-3xl!">
|
||||
{/* Header */}
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base font-semibold">
|
||||
Edit Issue
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs text-muted-foreground" />
|
||||
</DialogHeader>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Body */}
|
||||
<form
|
||||
onSubmit={handleSubmit(
|
||||
onSubmit,
|
||||
(errors) => {
|
||||
console.log("FORM ERRORS:", errors);
|
||||
}
|
||||
)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={control}
|
||||
name="issueStatus"
|
||||
render={({ field }) => (
|
||||
<IssueWorkflow
|
||||
value={field.value as IssueStatus}
|
||||
onChange={field.onChange}
|
||||
standalone={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Title */}
|
||||
{/* <FormField
|
||||
control={control}
|
||||
name="issueTitle"
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold">Title</Label>
|
||||
<Input {...field} className="text-sm h-9" />
|
||||
{fieldState.error && (
|
||||
<p className="text-xs text-red-500">{fieldState.error.message}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/> */}
|
||||
|
||||
{/* Description */}
|
||||
<FormField
|
||||
control={control}
|
||||
name="issueDescription"
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-semibold">Description</Label>
|
||||
<Textarea
|
||||
{...field}
|
||||
className="resize-none text-sm min-h-[90px]"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<p className="text-xs text-red-500">
|
||||
{fieldState.error.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Probability + Impact */}
|
||||
<div className="grid grid-cols-10 gap-4">
|
||||
<div className="col-span-2">
|
||||
<FormField
|
||||
control={control}
|
||||
name="issueProbability"
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold">Probability</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
className="text-sm h-9"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<p className="text-xs text-red-500">
|
||||
{fieldState.error.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-8">
|
||||
<FormField
|
||||
control={control}
|
||||
name="issueImpact"
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold">Impact</Label>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="text-sm h-9">
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="Catastrophic">
|
||||
5: Catastrophic
|
||||
</SelectItem>
|
||||
<SelectItem value="High">4: High</SelectItem>
|
||||
<SelectItem value="Medium">3: Medium</SelectItem>
|
||||
<SelectItem value="Low">2: Low</SelectItem>
|
||||
<SelectItem value="Minimal">1: Minimal</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{fieldState.error && (
|
||||
<p className="text-xs text-red-500">
|
||||
{fieldState.error.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Owner */}
|
||||
<FormField
|
||||
control={control}
|
||||
name="issueOwner"
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold">
|
||||
Issue Owner <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="text-sm h-9">
|
||||
<SelectValue placeholder="Select Issue Owner" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{assigneeOptions.map((id) => (
|
||||
<SelectItem key={id} value={id}>
|
||||
{userNameMap.get(id) || "Loading..."}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{fieldState.error && (
|
||||
<p className="text-xs text-red-500">
|
||||
{fieldState.error.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Mitigation */}
|
||||
{/* <FormField
|
||||
control={control}
|
||||
name="mitigationAction"
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-semibold">Mitigation Action</Label>
|
||||
<Textarea {...field} className="resize-none text-sm min-h-[80px]" />
|
||||
{fieldState.error && (
|
||||
<p className="text-xs text-red-500">{fieldState.error.message}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/> */}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-2 px-6 py-4">
|
||||
<Button type="button" variant="outline" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" size="sm" className="font-semibold">
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const editIssueSchema = z.object({
|
||||
|
||||
issueDescription: z.string().min(5, "Description must be at least 5 characters"),
|
||||
issueProbability: z.number().min(0).max(100),
|
||||
issueImpact: z.string().min(1, "Impact is required"),
|
||||
issueOwner: z.string().min(1, "Owner is required"),
|
||||
issueStatus: z.enum([
|
||||
"Active",
|
||||
"On hold",
|
||||
"Resolved",
|
||||
]),
|
||||
});
|
||||
@@ -0,0 +1,397 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "ikoncomponents";
|
||||
|
||||
import { Input } from "ikoncomponents";
|
||||
import { Textarea } from "ikoncomponents";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "ikoncomponents";
|
||||
import { Button } from "ikoncomponents";
|
||||
|
||||
import { IssueData } from "@/app/utils/interface/issue";
|
||||
import { getProjectByIdentifierApi } from "@/app/utils/api/projectApi";
|
||||
import { useAppCache } from "@/app/utils/context/AppCacheContext";
|
||||
import { issueSchema } from "./schema";
|
||||
|
||||
|
||||
type FormValues = z.infer<typeof issueSchema>;
|
||||
|
||||
interface Props {
|
||||
projectIdentifier: string;
|
||||
onSubmit: (data: IssueData) => void;
|
||||
}
|
||||
|
||||
/* ---------------- Component ---------------- */
|
||||
|
||||
export default function IssueForm({ projectIdentifier, onSubmit }: Props) {
|
||||
const { userNameMap } = useAppCache();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(issueSchema),
|
||||
defaultValues: {
|
||||
issueTitle: "",
|
||||
issueProbability: 0,
|
||||
|
||||
grossIssueValue: 0,
|
||||
probableIssueValue: 0,
|
||||
probableIssueValueInUSD: 0,
|
||||
|
||||
issueImpact: "",
|
||||
issueOwner: "",
|
||||
issueDescription: "",
|
||||
|
||||
projectIdentifier,
|
||||
financialIssue: false,
|
||||
|
||||
issueCreatedDate: new Date().toISOString(),
|
||||
issueOptionsSelectId: "manualIssues",
|
||||
|
||||
issueStatus: "Active",
|
||||
issueAge: 0,
|
||||
effectedSprintId: "",
|
||||
},
|
||||
});
|
||||
|
||||
const probability = form.watch("issueProbability");
|
||||
const gross = form.watch("grossIssueValue");
|
||||
const createdDate = form.watch("issueCreatedDate");
|
||||
|
||||
const [assigneeOptions, setAssigneeOptions] = useState<string[]>([]);
|
||||
|
||||
/* -------- Project Users -------- */
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const projectData = await getProjectByIdentifierApi(projectIdentifier);
|
||||
|
||||
const {
|
||||
projectTeam = [],
|
||||
projectTeamUnderProjectManager = [],
|
||||
projectManagerDelegates,
|
||||
} = projectData;
|
||||
|
||||
const managerDelegatesArr = projectManagerDelegates
|
||||
? Array.isArray(projectManagerDelegates)
|
||||
? projectManagerDelegates
|
||||
: [projectManagerDelegates]
|
||||
: [];
|
||||
|
||||
const mergedAssignees = [
|
||||
...projectTeam,
|
||||
...projectTeamUnderProjectManager,
|
||||
...managerDelegatesArr,
|
||||
];
|
||||
|
||||
const uniqueAssignees = [...new Set(mergedAssignees)];
|
||||
setAssigneeOptions(uniqueAssignees);
|
||||
} catch (error) {
|
||||
console.error("Error fetching project:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (projectIdentifier) fetchProjects();
|
||||
}, [projectIdentifier]);
|
||||
|
||||
/* -------- Calculations -------- */
|
||||
useEffect(() => {
|
||||
const probable =
|
||||
probability && gross ? (probability * gross) / 100 : 0;
|
||||
|
||||
form.setValue("probableIssueValue", probable);
|
||||
form.setValue("probableIssueValueInUSD", probable);
|
||||
}, [probability, gross, form]);
|
||||
|
||||
/* -------- Issue Age -------- */
|
||||
useEffect(() => {
|
||||
if (!createdDate) return;
|
||||
|
||||
const created = new Date(createdDate);
|
||||
const today = new Date();
|
||||
const diff = Math.floor(
|
||||
(today.getTime() - created.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
if (diff >= 0) {
|
||||
form.setValue("issueAge", diff);
|
||||
}
|
||||
}, [createdDate, form]);
|
||||
|
||||
/* -------- Submit -------- */
|
||||
const handleSubmit = (data: FormValues) => {
|
||||
onSubmit(data);
|
||||
form.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="grid grid-cols-12 gap-4"
|
||||
>
|
||||
{/* Issue Option */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="issueOptionsSelectId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2 mt-2 ml-2">
|
||||
Issue Options <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<FormControl className="w-full">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="manualIssues">Manual Issues</SelectItem>
|
||||
<SelectItem value="predefinedIssues">Predefined Issues</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Issue Title */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="issueTitle"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">
|
||||
Issue Title <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Probability */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="issueProbability"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">
|
||||
Probability % <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gross Value */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="grossIssueValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">Gross Issue Value</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Probable Value */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="probableIssueValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">Probable Issue Value</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" value={field.value} disabled />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Impact */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="issueImpact"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">
|
||||
Impact (1–5) <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(String(value))}
|
||||
defaultValue={field.value?.toString()}
|
||||
>
|
||||
<FormControl className="w-full">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select Impact" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="Catastrophic">5: Catastrophic</SelectItem>
|
||||
<SelectItem value="High">4: High</SelectItem>
|
||||
<SelectItem value="Medium">3: Medium</SelectItem>
|
||||
<SelectItem value="Low">2: Low</SelectItem>
|
||||
<SelectItem value="Minimal">1: Minimal</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Issue Owner */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="issueOwner"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">
|
||||
Issue Owner <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Issue Owner" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{assigneeOptions.map((id) => (
|
||||
<SelectItem key={id} value={id}>
|
||||
{userNameMap.get(id) || "Loading..."}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Financial Issue */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="financialIssue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">Financial Issue</FormLabel>
|
||||
<Select
|
||||
value={String(field.value)}
|
||||
onValueChange={(val) =>
|
||||
field.onChange(val === "true")
|
||||
}
|
||||
>
|
||||
<FormControl className="w-full">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="false">No</SelectItem>
|
||||
<SelectItem value="true">Yes</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Created Date */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="issueCreatedDate"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">Created On</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
value={new Date(field.value).toLocaleDateString()}
|
||||
disabled
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="col-span-12">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="issueDescription"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">
|
||||
Issue Description <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea rows={4} {...field} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 flex justify-end">
|
||||
<Button type="submit">Add Issue</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import z from "zod";
|
||||
|
||||
export const issueSchema = z.object({
|
||||
issueTitle: z.string().min(1),
|
||||
issueProbability: z.number().min(0).max(100),
|
||||
|
||||
grossIssueValue: z.number().min(0),
|
||||
probableIssueValue: z.number(),
|
||||
probableIssueValueInUSD: z.number(),
|
||||
|
||||
issueImpact: z.string(),
|
||||
issueOwner: z.string().min(1),
|
||||
issueDescription: z.string().min(1),
|
||||
|
||||
projectIdentifier: z.string(),
|
||||
|
||||
financialIssue: z.boolean(),
|
||||
issueCreatedDate: z.string(),
|
||||
|
||||
issueOptionsSelectId: z.string(),
|
||||
|
||||
issueStatus: z.string().optional(),
|
||||
issueAge: z.number().optional(),
|
||||
effectedSprintId: z.string().optional(),
|
||||
});
|
||||
@@ -0,0 +1,456 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Play,
|
||||
PauseCircle,
|
||||
CheckCircle,
|
||||
LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Separator,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
cn,
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "ikoncomponents";
|
||||
|
||||
/* ───────────────── Types ───────────────── */
|
||||
|
||||
export type IssueStatus = "Active" | "On hold" | "Resolved";
|
||||
|
||||
interface Stage {
|
||||
id: IssueStatus;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
interface NavRule {
|
||||
canProceed: IssueStatus[];
|
||||
canGoBack: IssueStatus[];
|
||||
}
|
||||
|
||||
interface StatusConfig {
|
||||
hex: string;
|
||||
}
|
||||
|
||||
interface IssueWorkflowProps {
|
||||
value?: IssueStatus; // controlled
|
||||
initialStatus?: IssueStatus; // uncontrolled
|
||||
onChange?: (status: IssueStatus) => void;
|
||||
standalone?: boolean;
|
||||
}
|
||||
|
||||
/* ───────────────── Data ───────────────── */
|
||||
|
||||
const STAGES: Stage[] = [
|
||||
{ id: "Active", label: "Active", icon: Play },
|
||||
{ id: "On hold", label: "On Hold", icon: PauseCircle },
|
||||
{ id: "Resolved", label: "Resolved", icon: CheckCircle }
|
||||
];
|
||||
|
||||
const NAV_RULES: Record<IssueStatus, NavRule> = {
|
||||
"Active": {
|
||||
canProceed: ["On hold", "Resolved"],
|
||||
canGoBack: [],
|
||||
},
|
||||
"On hold": {
|
||||
canProceed: ["Active", "Resolved"],
|
||||
canGoBack: ["Active"],
|
||||
},
|
||||
"Resolved": {
|
||||
canProceed: [],
|
||||
canGoBack: ["Active", "On hold"],
|
||||
},
|
||||
};
|
||||
|
||||
const STATUS_CONFIG: Record<IssueStatus, StatusConfig> = {
|
||||
"Active": { hex: "#3b82f6" },
|
||||
"On hold": { hex: "#f59e0b" },
|
||||
"Resolved": { hex: "#22c55e" },
|
||||
};
|
||||
|
||||
const STAGE_IDS = STAGES.map((s) => s.id);
|
||||
|
||||
function getLabel(id: IssueStatus): string {
|
||||
return STAGES.find((s) => s.id === id)?.label ?? id;
|
||||
}
|
||||
|
||||
/* ───────────────── Stage Node ───────────────── */
|
||||
|
||||
interface StageNodeProps {
|
||||
stage: Stage;
|
||||
isActive: boolean;
|
||||
isCompleted: boolean;
|
||||
cfg: StatusConfig;
|
||||
isClickable: boolean;
|
||||
onClickNode: () => void;
|
||||
tooltipText: string;
|
||||
}
|
||||
|
||||
function StageNode({
|
||||
stage,
|
||||
isActive,
|
||||
isCompleted,
|
||||
cfg,
|
||||
isClickable,
|
||||
onClickNode,
|
||||
tooltipText,
|
||||
}: StageNodeProps) {
|
||||
const Icon = stage.icon;
|
||||
|
||||
const circleStyle: React.CSSProperties = isActive
|
||||
? {
|
||||
borderColor: cfg.hex,
|
||||
backgroundColor: `${cfg.hex}20`,
|
||||
boxShadow: `0 0 0 3px ${cfg.hex}30`,
|
||||
}
|
||||
: isCompleted
|
||||
? {
|
||||
borderColor: `${cfg.hex}60`,
|
||||
backgroundColor: `${cfg.hex}12`,
|
||||
}
|
||||
: {};
|
||||
|
||||
const iconColor = isActive
|
||||
? cfg.hex
|
||||
: isCompleted
|
||||
? `${cfg.hex}80`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={isClickable ? onClickNode : undefined}
|
||||
disabled={!isClickable}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-2.5 select-none outline-none group",
|
||||
isClickable ? "cursor-pointer" : "cursor-default"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-11 h-11 rounded-full flex items-center justify-center border-2 transition-all duration-500 mb-2",
|
||||
isActive && "scale-110 shadow-lg",
|
||||
!isActive && !isCompleted && "border-muted bg-muted/20",
|
||||
isClickable && !isActive && "group-hover:scale-105 group-hover:opacity-90"
|
||||
)}
|
||||
style={circleStyle}
|
||||
>
|
||||
<Icon
|
||||
size={18}
|
||||
className="transition-colors duration-300"
|
||||
style={{
|
||||
color:
|
||||
iconColor ??
|
||||
(isActive || isCompleted
|
||||
? undefined
|
||||
: "hsl(var(--muted-foreground) / 0.3)"),
|
||||
}}
|
||||
/>
|
||||
|
||||
{isCompleted && (
|
||||
<span
|
||||
className="absolute -top-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-background"
|
||||
style={{ backgroundColor: cfg.hex }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"text-[11px] font-medium text-center leading-tight max-w-[76px] transition-colors duration-300",
|
||||
isActive && "font-semibold",
|
||||
!isActive && !isCompleted && "text-muted-foreground/30"
|
||||
)}
|
||||
style={{
|
||||
color: isActive
|
||||
? cfg.hex
|
||||
: isCompleted
|
||||
? "hsl(var(--muted-foreground) / 0.6)"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{stage.label}
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent side="bottom" className="text-xs max-w-[160px] text-center">
|
||||
{tooltipText}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/* ───────────────── Connector ───────────────── */
|
||||
|
||||
function Connector({ isCompleted, cfg }: { isCompleted: boolean; cfg: StatusConfig }) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center pb-7 mx-1 min-w-5">
|
||||
<div className="relative w-full h-[2px] rounded-full bg-muted/40 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 rounded-full transition-all duration-700 ease-in-out"
|
||||
style={{
|
||||
width: isCompleted ? "100%" : "0%",
|
||||
backgroundColor: isCompleted ? cfg.hex : "transparent",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ───────────────── Confirmation Dialog ───────────────── */
|
||||
|
||||
function ConfirmDialog({
|
||||
open,
|
||||
targetStatus,
|
||||
currentStatus,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
open: boolean;
|
||||
targetStatus: IssueStatus | null;
|
||||
currentStatus: IssueStatus;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
if (!targetStatus) return null;
|
||||
|
||||
const currentIndex = STAGE_IDS.indexOf(currentStatus);
|
||||
const targetIndex = STAGE_IDS.indexOf(targetStatus);
|
||||
const isGoingBack = targetIndex < currentIndex;
|
||||
const targetCfg = STATUS_CONFIG[targetStatus];
|
||||
const currentCfg = STATUS_CONFIG[currentStatus];
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={(v) => !v && onCancel()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
{isGoingBack ? "Go back to" : "Proceed to"}{" "}
|
||||
<span className="font-bold" style={{ color: targetCfg.hex }}>
|
||||
{getLabel(targetStatus)}
|
||||
</span>
|
||||
?
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription className="text-sm text-muted-foreground">
|
||||
{isGoingBack
|
||||
? `This will move the issue status back from "${getLabel(
|
||||
currentStatus
|
||||
)}" to "${getLabel(targetStatus)}".`
|
||||
: `This will advance the issue status from "${getLabel(
|
||||
currentStatus
|
||||
)}" to "${getLabel(targetStatus)}".`}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<div className="flex items-center justify-center gap-3 py-2">
|
||||
<span
|
||||
className="text-xs font-semibold px-3 py-1 rounded-full border"
|
||||
style={{
|
||||
color: currentCfg.hex,
|
||||
backgroundColor: `${currentCfg.hex}15`,
|
||||
borderColor: `${currentCfg.hex}30`,
|
||||
}}
|
||||
>
|
||||
{getLabel(currentStatus)}
|
||||
</span>
|
||||
|
||||
<span className="text-muted-foreground text-sm">→</span>
|
||||
|
||||
<span
|
||||
className="text-xs font-semibold px-3 py-1 rounded-full border"
|
||||
style={{
|
||||
color: targetCfg.hex,
|
||||
backgroundColor: `${targetCfg.hex}15`,
|
||||
borderColor: `${targetCfg.hex}30`,
|
||||
}}
|
||||
>
|
||||
{getLabel(targetStatus)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onConfirm}
|
||||
style={{
|
||||
backgroundColor: targetCfg.hex,
|
||||
color: "#0d1117",
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
{isGoingBack ? "Yes, Go Back" : "Yes, Proceed"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
/* ───────────────── IssueWorkflow Component ───────────────── */
|
||||
|
||||
export default function IssueWorkflow({
|
||||
value,
|
||||
initialStatus = "Active",
|
||||
onChange,
|
||||
standalone,
|
||||
}: IssueWorkflowProps) {
|
||||
const isControlled = value !== undefined;
|
||||
const [internalStatus, setInternalStatus] = useState<IssueStatus>(initialStatus);
|
||||
|
||||
const currentStatus: IssueStatus = isControlled ? value! : internalStatus;
|
||||
|
||||
const [pendingTarget, setPendingTarget] = useState<IssueStatus | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const cfg = STATUS_CONFIG[currentStatus];
|
||||
const activeIndex = STAGE_IDS.indexOf(currentStatus);
|
||||
const rules = NAV_RULES[currentStatus];
|
||||
|
||||
function requestNavigate(targetId: IssueStatus) {
|
||||
setPendingTarget(targetId);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function confirmNavigate() {
|
||||
if (!pendingTarget) return;
|
||||
if (!isControlled) setInternalStatus(pendingTarget);
|
||||
onChange?.(pendingTarget);
|
||||
setDialogOpen(false);
|
||||
setPendingTarget(null);
|
||||
}
|
||||
|
||||
function cancelNavigate() {
|
||||
setDialogOpen(false);
|
||||
setPendingTarget(null);
|
||||
}
|
||||
|
||||
function getTooltip(stageId: IssueStatus, idx: number): string {
|
||||
if (stageId === currentStatus) return "Current stage";
|
||||
if (rules.canProceed.includes(stageId)) return `Click to proceed → ${getLabel(stageId)}`;
|
||||
if (rules.canGoBack.includes(stageId)) return `Click to go back → ${getLabel(stageId)}`;
|
||||
if (idx < activeIndex) return "Completed";
|
||||
return "Not reachable from current stage";
|
||||
}
|
||||
|
||||
function isClickable(stageId: IssueStatus): boolean {
|
||||
return rules.canProceed.includes(stageId) || rules.canGoBack.includes(stageId);
|
||||
}
|
||||
|
||||
const card = (
|
||||
<>
|
||||
<Card
|
||||
className="w-full transition-all duration-500"
|
||||
style={{
|
||||
borderColor: `${cfg.hex}28`,
|
||||
boxShadow: `0 0 0 1px ${cfg.hex}12, 0 8px 40px ${cfg.hex}12`,
|
||||
}}
|
||||
>
|
||||
<CardHeader className="pb-3 pt-5 px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
Issue Status
|
||||
</CardTitle>
|
||||
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs font-semibold px-2.5 py-0.5 rounded-full border"
|
||||
style={{
|
||||
color: cfg.hex,
|
||||
backgroundColor: `${cfg.hex}15`,
|
||||
borderColor: `${cfg.hex}30`,
|
||||
}}
|
||||
>
|
||||
{getLabel(currentStatus)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<Separator />
|
||||
|
||||
<CardContent className="px-6 pt-5 pb-6">
|
||||
<div className="flex items-start">
|
||||
{STAGES.map((stage, i) => {
|
||||
const isActive = i === activeIndex;
|
||||
const isCompleted = i < activeIndex;
|
||||
|
||||
return (
|
||||
<div key={stage.id} className="flex items-center flex-1 last:flex-none">
|
||||
<StageNode
|
||||
stage={stage}
|
||||
isActive={isActive}
|
||||
isCompleted={isCompleted}
|
||||
cfg={cfg}
|
||||
isClickable={isClickable(stage.id)}
|
||||
onClickNode={() => requestNavigate(stage.id)}
|
||||
tooltipText={getTooltip(stage.id, i)}
|
||||
/>
|
||||
|
||||
{i < STAGES.length - 1 && (
|
||||
<Connector isCompleted={isCompleted} cfg={cfg} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{currentStatus === "Resolved" && (
|
||||
<div className="flex justify-center mt-4">
|
||||
<Badge
|
||||
className="text-xs px-3 py-1 rounded-full"
|
||||
style={{
|
||||
backgroundColor: `${cfg.hex}20`,
|
||||
color: cfg.hex,
|
||||
border: `1px solid ${cfg.hex}30`,
|
||||
}}
|
||||
>
|
||||
✓ Issue Resolved
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ConfirmDialog
|
||||
open={dialogOpen}
|
||||
targetStatus={pendingTarget}
|
||||
currentStatus={currentStatus}
|
||||
onConfirm={confirmNavigate}
|
||||
onCancel={cancelNavigate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (standalone) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-8">
|
||||
<div className="w-full max-w-[560px]">{card}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { DataTable } from "ikoncomponents";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "ikoncomponents";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Plus, SquarePenIcon, Trash } from "lucide-react";
|
||||
|
||||
import IssueForm from "./components/issueModal";
|
||||
import { IconTextButtonWithTooltip } from "ikoncomponents";
|
||||
import { DTExtraParamsProps } from "ikoncomponents";
|
||||
import EditIssueModal from "./components/editIssue";
|
||||
import {
|
||||
createIssueApi,
|
||||
issuesApi,
|
||||
updateIssueApi,
|
||||
} from "@/app/utils/api/issueApi";
|
||||
import { IssueData } from "@/app/utils/interface/issue";
|
||||
import { useAppCache } from "@/app/utils/context/AppCacheContext";
|
||||
|
||||
interface IssueComponentProps {
|
||||
projectIdentifier: string;
|
||||
}
|
||||
|
||||
export default function IssueComponent({
|
||||
projectIdentifier,
|
||||
}: IssueComponentProps) {
|
||||
const { userNameMap } = useAppCache();
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [selectedIssue, setSelectedIssue] = useState<IssueData | null>(null);
|
||||
const [data, setData] = useState<IssueData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// ── Fetch Issues from API ──────────────────────────────────────
|
||||
const fetchIssues = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await issuesApi(projectIdentifier);
|
||||
|
||||
console.log("Issue list response -----> ", response);
|
||||
|
||||
setData(response?.data || response || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching issues:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchIssues();
|
||||
}, []);
|
||||
|
||||
// ── Handlers ───────────────────────────────────────────────────
|
||||
const openForCreate = () => setCreateOpen(true);
|
||||
|
||||
const openForEdit = (issue: IssueData) => {
|
||||
setSelectedIssue(issue);
|
||||
setEditOpen(true);
|
||||
};
|
||||
|
||||
const handleCreateSubmit = async (formData: IssueData) => {
|
||||
try {
|
||||
const response = await createIssueApi(formData);
|
||||
console.log("Issue created -----> ", response);
|
||||
|
||||
setCreateOpen(false);
|
||||
fetchIssues();
|
||||
} catch (error) {
|
||||
console.error("Error creating issue:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSave = async (updatedValues: IssueData) => {
|
||||
if (!selectedIssue?.issueId) {
|
||||
console.error("No issue selected for update");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Updated Issue:", updatedValues);
|
||||
|
||||
const response = await updateIssueApi(
|
||||
selectedIssue.issueId,
|
||||
updatedValues
|
||||
);
|
||||
|
||||
console.log(response);
|
||||
|
||||
setEditOpen(false);
|
||||
setSelectedIssue(null);
|
||||
fetchIssues();
|
||||
};
|
||||
|
||||
// ── Table Columns ─────────────────────────────────────────────
|
||||
const columns: ColumnDef<IssueData>[] = [
|
||||
{
|
||||
accessorKey: "issueDescription",
|
||||
header: () => <div className="text-center">Description</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "issueProbability",
|
||||
header: () => <div className="text-center">Probability (%)</div>,
|
||||
cell: ({ row }) => <span>{row.original.issueProbability}%</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "issueImpact",
|
||||
header: () => <div className="text-center">Impact</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "probableIssueValue",
|
||||
header: () => <div className="text-center">Score</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "issueOwner",
|
||||
header: () => <div className="text-center">Owner</div>,
|
||||
cell: ({ row }) => (
|
||||
<span>
|
||||
{row.original.issueOwner
|
||||
? userNameMap.get(row.original.issueOwner)
|
||||
: "N/A"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "issueStatus",
|
||||
header: () => <div className="text-center">Status</div>,
|
||||
},
|
||||
];
|
||||
|
||||
// ── DataTable Extra Params ────────────────────────────────────
|
||||
const extraParams: DTExtraParamsProps = {
|
||||
extraTools: [
|
||||
<IconTextButtonWithTooltip
|
||||
key="create-issue"
|
||||
tooltipContent="Add new issue"
|
||||
variant="outline"
|
||||
onClick={openForCreate}
|
||||
>
|
||||
<Plus />
|
||||
</IconTextButtonWithTooltip>,
|
||||
],
|
||||
|
||||
actionMenu: {
|
||||
items: [
|
||||
{
|
||||
label: "Edit",
|
||||
icon: SquarePenIcon,
|
||||
onClick: (row: IssueData) => openForEdit(row),
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
icon: Trash,
|
||||
onClick: (row: IssueData) => {
|
||||
console.log("Delete", row.issueId);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
extraParams={extraParams}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
{selectedIssue && (
|
||||
<EditIssueModal
|
||||
open={editOpen}
|
||||
onClose={() => {
|
||||
setEditOpen(false);
|
||||
setSelectedIssue(null);
|
||||
}}
|
||||
issueData={selectedIssue}
|
||||
onSave={handleEditSave}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogContent className="max-w-6xl! w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Issue</DialogTitle>
|
||||
</DialogHeader>
|
||||
<IssueForm
|
||||
projectIdentifier={projectIdentifier}
|
||||
onSubmit={handleCreateSubmit}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user