2024-06-10 09:43:10 +00:00
|
|
|
"use client";
|
|
|
|
|
2024-01-18 10:19:54 +00:00
|
|
|
import isToday from "date-fns/isToday";
|
2024-03-06 13:09:14 +00:00
|
|
|
import { observer } from "mobx-react-lite";
|
2024-03-19 14:38:35 +00:00
|
|
|
import { TIssue, TWidgetIssue } from "@plane/types";
|
2024-01-18 10:19:54 +00:00
|
|
|
// hooks
|
|
|
|
// ui
|
|
|
|
import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui";
|
|
|
|
// helpers
|
2024-03-19 14:38:35 +00:00
|
|
|
import { findTotalDaysInRange, getDate, renderFormattedDate } from "@/helpers/date-time.helper";
|
|
|
|
import { useIssueDetail, useMember, useProject } from "@/hooks/store";
|
2024-01-18 10:19:54 +00:00
|
|
|
// types
|
|
|
|
|
|
|
|
export type IssueListItemProps = {
|
|
|
|
issueId: string;
|
|
|
|
onClick: (issue: TIssue) => void;
|
|
|
|
workspaceSlug: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const AssignedUpcomingIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
|
|
|
|
const { issueId, onClick, workspaceSlug } = props;
|
|
|
|
// store hooks
|
|
|
|
const { getProjectById } = useProject();
|
|
|
|
const {
|
|
|
|
issue: { getIssueById },
|
|
|
|
} = useIssueDetail();
|
|
|
|
// derived values
|
|
|
|
const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined;
|
|
|
|
|
2024-06-10 14:45:03 +00:00
|
|
|
if (!issueDetails || !issueDetails.project_id) return null;
|
2024-01-18 10:19:54 +00:00
|
|
|
|
|
|
|
const projectDetails = getProjectById(issueDetails.project_id);
|
|
|
|
|
|
|
|
const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? [];
|
|
|
|
|
|
|
|
const blockedByIssueProjectDetails =
|
|
|
|
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
|
|
|
|
|
2024-03-19 12:10:20 +00:00
|
|
|
const targetDate = getDate(issueDetails.target_date);
|
|
|
|
|
2024-01-18 10:19:54 +00:00
|
|
|
return (
|
|
|
|
<ControlLink
|
|
|
|
href={`/${workspaceSlug}/projects/${issueDetails.project_id}/issues/${issueDetails.id}`}
|
|
|
|
onClick={() => onClick(issueDetails)}
|
2024-05-08 17:31:20 +00:00
|
|
|
className="grid grid-cols-6 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
|
2024-01-18 10:19:54 +00:00
|
|
|
>
|
|
|
|
<div className="col-span-4 flex items-center gap-3">
|
|
|
|
<PriorityIcon priority={issueDetails.priority} withContainer />
|
2024-05-08 17:31:20 +00:00
|
|
|
<span className="flex-shrink-0 text-xs font-medium">
|
2024-01-18 10:19:54 +00:00
|
|
|
{projectDetails?.identifier} {issueDetails.sequence_id}
|
|
|
|
</span>
|
2024-05-08 17:31:20 +00:00
|
|
|
<h6 className="flex-grow truncate text-sm">{issueDetails.name}</h6>
|
2024-01-18 10:19:54 +00:00
|
|
|
</div>
|
2024-05-08 17:31:20 +00:00
|
|
|
<div className="text-center text-xs">
|
2024-03-19 12:10:20 +00:00
|
|
|
{targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"}
|
2024-01-18 10:19:54 +00:00
|
|
|
</div>
|
2024-05-08 17:31:20 +00:00
|
|
|
<div className="text-center text-xs">
|
2024-01-18 10:19:54 +00:00
|
|
|
{blockedByIssues.length > 0
|
|
|
|
? blockedByIssues.length > 1
|
|
|
|
? `${blockedByIssues.length} blockers`
|
|
|
|
: `${blockedByIssueProjectDetails?.identifier} ${blockedByIssues[0]?.sequence_id}`
|
|
|
|
: "-"}
|
|
|
|
</div>
|
|
|
|
</ControlLink>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
export const AssignedOverdueIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
|
|
|
|
const { issueId, onClick, workspaceSlug } = props;
|
|
|
|
// store hooks
|
|
|
|
const { getProjectById } = useProject();
|
|
|
|
const {
|
|
|
|
issue: { getIssueById },
|
|
|
|
} = useIssueDetail();
|
|
|
|
// derived values
|
|
|
|
const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined;
|
|
|
|
|
2024-06-10 14:45:03 +00:00
|
|
|
if (!issueDetails || !issueDetails.project_id) return null;
|
2024-01-18 10:19:54 +00:00
|
|
|
|
|
|
|
const projectDetails = getProjectById(issueDetails.project_id);
|
|
|
|
const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? [];
|
|
|
|
|
|
|
|
const blockedByIssueProjectDetails =
|
|
|
|
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
|
|
|
|
|
2024-03-19 12:10:20 +00:00
|
|
|
const dueBy = findTotalDaysInRange(getDate(issueDetails.target_date), new Date(), false) ?? 0;
|
2024-01-18 10:19:54 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<ControlLink
|
|
|
|
href={`/${workspaceSlug}/projects/${issueDetails.project_id}/issues/${issueDetails.id}`}
|
|
|
|
onClick={() => onClick(issueDetails)}
|
2024-05-08 17:31:20 +00:00
|
|
|
className="grid grid-cols-6 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
|
2024-01-18 10:19:54 +00:00
|
|
|
>
|
|
|
|
<div className="col-span-4 flex items-center gap-3">
|
|
|
|
<PriorityIcon priority={issueDetails.priority} withContainer />
|
2024-05-08 17:31:20 +00:00
|
|
|
<span className="flex-shrink-0 text-xs font-medium">
|
2024-01-18 10:19:54 +00:00
|
|
|
{projectDetails?.identifier} {issueDetails.sequence_id}
|
|
|
|
</span>
|
2024-05-08 17:31:20 +00:00
|
|
|
<h6 className="flex-grow truncate text-sm">{issueDetails.name}</h6>
|
2024-01-18 10:19:54 +00:00
|
|
|
</div>
|
2024-05-08 17:31:20 +00:00
|
|
|
<div className="text-center text-xs">
|
2024-01-18 10:19:54 +00:00
|
|
|
{dueBy} {`day${dueBy > 1 ? "s" : ""}`}
|
|
|
|
</div>
|
2024-05-08 17:31:20 +00:00
|
|
|
<div className="text-center text-xs">
|
2024-01-18 10:19:54 +00:00
|
|
|
{blockedByIssues.length > 0
|
|
|
|
? blockedByIssues.length > 1
|
|
|
|
? `${blockedByIssues.length} blockers`
|
|
|
|
: `${blockedByIssueProjectDetails?.identifier} ${blockedByIssues[0]?.sequence_id}`
|
|
|
|
: "-"}
|
|
|
|
</div>
|
|
|
|
</ControlLink>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
export const AssignedCompletedIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
|
|
|
|
const { issueId, onClick, workspaceSlug } = props;
|
|
|
|
// store hooks
|
|
|
|
const {
|
|
|
|
issue: { getIssueById },
|
|
|
|
} = useIssueDetail();
|
|
|
|
const { getProjectById } = useProject();
|
|
|
|
// derived values
|
|
|
|
const issueDetails = getIssueById(issueId);
|
|
|
|
|
2024-06-10 14:45:03 +00:00
|
|
|
if (!issueDetails || !issueDetails.project_id) return null;
|
2024-01-18 10:19:54 +00:00
|
|
|
|
|
|
|
const projectDetails = getProjectById(issueDetails.project_id);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ControlLink
|
|
|
|
href={`/${workspaceSlug}/projects/${issueDetails.project_id}/issues/${issueDetails.id}`}
|
|
|
|
onClick={() => onClick(issueDetails)}
|
2024-05-08 17:31:20 +00:00
|
|
|
className="grid grid-cols-6 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
|
2024-01-18 10:19:54 +00:00
|
|
|
>
|
|
|
|
<div className="col-span-6 flex items-center gap-3">
|
|
|
|
<PriorityIcon priority={issueDetails.priority} withContainer />
|
2024-05-08 17:31:20 +00:00
|
|
|
<span className="flex-shrink-0 text-xs font-medium">
|
2024-01-18 10:19:54 +00:00
|
|
|
{projectDetails?.identifier} {issueDetails.sequence_id}
|
|
|
|
</span>
|
2024-05-08 17:31:20 +00:00
|
|
|
<h6 className="flex-grow truncate text-sm">{issueDetails.name}</h6>
|
2024-01-18 10:19:54 +00:00
|
|
|
</div>
|
|
|
|
</ControlLink>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
export const CreatedUpcomingIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
|
|
|
|
const { issueId, onClick, workspaceSlug } = props;
|
|
|
|
// store hooks
|
|
|
|
const { getUserDetails } = useMember();
|
|
|
|
const {
|
|
|
|
issue: { getIssueById },
|
|
|
|
} = useIssueDetail();
|
|
|
|
const { getProjectById } = useProject();
|
|
|
|
// derived values
|
|
|
|
const issue = getIssueById(issueId);
|
|
|
|
|
2024-06-10 14:45:03 +00:00
|
|
|
if (!issue || !issue.project_id) return null;
|
2024-01-18 10:19:54 +00:00
|
|
|
|
|
|
|
const projectDetails = getProjectById(issue.project_id);
|
2024-03-20 08:14:08 +00:00
|
|
|
const targetDate = getDate(issue.target_date);
|
2024-01-18 10:19:54 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<ControlLink
|
|
|
|
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
|
|
|
|
onClick={() => onClick(issue)}
|
2024-05-08 17:31:20 +00:00
|
|
|
className="grid grid-cols-6 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
|
2024-01-18 10:19:54 +00:00
|
|
|
>
|
|
|
|
<div className="col-span-4 flex items-center gap-3">
|
|
|
|
<PriorityIcon priority={issue.priority} withContainer />
|
2024-05-08 17:31:20 +00:00
|
|
|
<span className="flex-shrink-0 text-xs font-medium">
|
2024-01-18 10:19:54 +00:00
|
|
|
{projectDetails?.identifier} {issue.sequence_id}
|
|
|
|
</span>
|
2024-05-08 17:31:20 +00:00
|
|
|
<h6 className="flex-grow truncate text-sm">{issue.name}</h6>
|
2024-01-18 10:19:54 +00:00
|
|
|
</div>
|
2024-05-08 17:31:20 +00:00
|
|
|
<div className="text-center text-xs">
|
2024-03-20 08:14:08 +00:00
|
|
|
{targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"}
|
2024-01-18 10:19:54 +00:00
|
|
|
</div>
|
2024-05-08 17:31:20 +00:00
|
|
|
<div className="flex justify-center text-xs">
|
2024-02-21 15:49:00 +00:00
|
|
|
{issue.assignee_ids && issue.assignee_ids?.length > 0 ? (
|
2024-01-18 10:19:54 +00:00
|
|
|
<AvatarGroup>
|
|
|
|
{issue.assignee_ids?.map((assigneeId) => {
|
|
|
|
const userDetails = getUserDetails(assigneeId);
|
|
|
|
|
|
|
|
if (!userDetails) return null;
|
|
|
|
|
|
|
|
return <Avatar key={assigneeId} src={userDetails.avatar} name={userDetails.display_name} />;
|
|
|
|
})}
|
|
|
|
</AvatarGroup>
|
|
|
|
) : (
|
|
|
|
"-"
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</ControlLink>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
export const CreatedOverdueIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
|
|
|
|
const { issueId, onClick, workspaceSlug } = props;
|
|
|
|
// store hooks
|
|
|
|
const { getUserDetails } = useMember();
|
|
|
|
const {
|
|
|
|
issue: { getIssueById },
|
|
|
|
} = useIssueDetail();
|
|
|
|
const { getProjectById } = useProject();
|
|
|
|
// derived values
|
|
|
|
const issue = getIssueById(issueId);
|
|
|
|
|
2024-06-10 14:45:03 +00:00
|
|
|
if (!issue || !issue.project_id) return null;
|
2024-01-18 10:19:54 +00:00
|
|
|
|
|
|
|
const projectDetails = getProjectById(issue.project_id);
|
|
|
|
|
2024-03-20 08:14:08 +00:00
|
|
|
const dueBy: number = findTotalDaysInRange(getDate(issue.target_date), new Date(), false) ?? 0;
|
2024-01-18 10:19:54 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<ControlLink
|
|
|
|
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
|
|
|
|
onClick={() => onClick(issue)}
|
2024-05-08 17:31:20 +00:00
|
|
|
className="grid grid-cols-6 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
|
2024-01-18 10:19:54 +00:00
|
|
|
>
|
|
|
|
<div className="col-span-4 flex items-center gap-3">
|
|
|
|
<PriorityIcon priority={issue.priority} withContainer />
|
2024-05-08 17:31:20 +00:00
|
|
|
<span className="flex-shrink-0 text-xs font-medium">
|
2024-01-18 10:19:54 +00:00
|
|
|
{projectDetails?.identifier} {issue.sequence_id}
|
|
|
|
</span>
|
2024-05-08 17:31:20 +00:00
|
|
|
<h6 className="flex-grow truncate text-sm">{issue.name}</h6>
|
2024-01-18 10:19:54 +00:00
|
|
|
</div>
|
2024-05-08 17:31:20 +00:00
|
|
|
<div className="text-center text-xs">
|
2024-01-18 10:19:54 +00:00
|
|
|
{dueBy} {`day${dueBy > 1 ? "s" : ""}`}
|
|
|
|
</div>
|
2024-05-08 17:31:20 +00:00
|
|
|
<div className="flex justify-center text-xs">
|
2024-01-18 10:19:54 +00:00
|
|
|
{issue.assignee_ids.length > 0 ? (
|
|
|
|
<AvatarGroup>
|
|
|
|
{issue.assignee_ids?.map((assigneeId) => {
|
|
|
|
const userDetails = getUserDetails(assigneeId);
|
|
|
|
|
|
|
|
if (!userDetails) return null;
|
|
|
|
|
|
|
|
return <Avatar key={assigneeId} src={userDetails.avatar} name={userDetails.display_name} />;
|
|
|
|
})}
|
|
|
|
</AvatarGroup>
|
|
|
|
) : (
|
|
|
|
"-"
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</ControlLink>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
export const CreatedCompletedIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
|
|
|
|
const { issueId, onClick, workspaceSlug } = props;
|
|
|
|
// store hooks
|
|
|
|
const { getUserDetails } = useMember();
|
|
|
|
const {
|
|
|
|
issue: { getIssueById },
|
|
|
|
} = useIssueDetail();
|
|
|
|
const { getProjectById } = useProject();
|
|
|
|
// derived values
|
|
|
|
const issue = getIssueById(issueId);
|
|
|
|
|
2024-06-10 14:45:03 +00:00
|
|
|
if (!issue || !issue.project_id) return null;
|
2024-01-18 10:19:54 +00:00
|
|
|
|
|
|
|
const projectDetails = getProjectById(issue.project_id);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ControlLink
|
|
|
|
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
|
|
|
|
onClick={() => onClick(issue)}
|
2024-05-08 17:31:20 +00:00
|
|
|
className="grid grid-cols-6 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
|
2024-01-18 10:19:54 +00:00
|
|
|
>
|
|
|
|
<div className="col-span-5 flex items-center gap-3">
|
|
|
|
<PriorityIcon priority={issue.priority} withContainer />
|
2024-05-08 17:31:20 +00:00
|
|
|
<span className="flex-shrink-0 text-xs font-medium">
|
2024-01-18 10:19:54 +00:00
|
|
|
{projectDetails?.identifier} {issue.sequence_id}
|
|
|
|
</span>
|
2024-05-08 17:31:20 +00:00
|
|
|
<h6 className="flex-grow truncate text-sm">{issue.name}</h6>
|
2024-01-18 10:19:54 +00:00
|
|
|
</div>
|
2024-05-08 17:31:20 +00:00
|
|
|
<div className="flex justify-center text-xs">
|
2024-01-18 10:19:54 +00:00
|
|
|
{issue.assignee_ids.length > 0 ? (
|
|
|
|
<AvatarGroup>
|
|
|
|
{issue.assignee_ids?.map((assigneeId) => {
|
|
|
|
const userDetails = getUserDetails(assigneeId);
|
|
|
|
|
|
|
|
if (!userDetails) return null;
|
|
|
|
|
|
|
|
return <Avatar key={assigneeId} src={userDetails.avatar} name={userDetails.display_name} />;
|
|
|
|
})}
|
|
|
|
</AvatarGroup>
|
|
|
|
) : (
|
|
|
|
"-"
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</ControlLink>
|
|
|
|
);
|
|
|
|
});
|