2024-01-16 07:16:03 +00:00
|
|
|
import React from "react";
|
2024-05-08 17:31:20 +00:00
|
|
|
import { observer } from "mobx-react";
|
2024-03-15 11:59:22 +00:00
|
|
|
import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
2024-03-19 14:38:35 +00:00
|
|
|
import { TIssue } from "@plane/types";
|
2024-01-16 07:16:03 +00:00
|
|
|
// components
|
2024-03-06 13:09:14 +00:00
|
|
|
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
2024-03-19 14:38:35 +00:00
|
|
|
import { cn } from "@/helpers/common.helper";
|
|
|
|
import { useIssueDetail, useProject, useProjectState } from "@/hooks/store";
|
|
|
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
2024-01-16 07:16:03 +00:00
|
|
|
import { IssueList } from "./issues-list";
|
|
|
|
import { IssueProperty } from "./properties";
|
|
|
|
// ui
|
|
|
|
// types
|
|
|
|
import { TSubIssueOperations } from "./root";
|
|
|
|
// import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
|
|
|
|
|
|
|
export interface ISubIssues {
|
|
|
|
workspaceSlug: string;
|
|
|
|
projectId: string;
|
|
|
|
parentIssueId: string;
|
2024-05-22 10:06:11 +00:00
|
|
|
rootIssueId: string;
|
2024-01-16 07:16:03 +00:00
|
|
|
spacingLeft: number;
|
|
|
|
disabled: boolean;
|
|
|
|
handleIssueCrudState: (
|
|
|
|
key: "create" | "existing" | "update" | "delete",
|
|
|
|
issueId: string,
|
|
|
|
issue?: TIssue | null
|
|
|
|
) => void;
|
|
|
|
subIssueOperations: TSubIssueOperations;
|
|
|
|
issueId: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
|
|
|
const {
|
|
|
|
workspaceSlug,
|
|
|
|
projectId,
|
|
|
|
parentIssueId,
|
2024-05-22 10:06:11 +00:00
|
|
|
rootIssueId,
|
2024-01-23 11:26:22 +00:00
|
|
|
issueId,
|
2024-01-16 07:16:03 +00:00
|
|
|
spacingLeft = 10,
|
|
|
|
disabled,
|
|
|
|
handleIssueCrudState,
|
|
|
|
subIssueOperations,
|
|
|
|
} = props;
|
|
|
|
|
|
|
|
const {
|
2024-04-30 11:50:02 +00:00
|
|
|
getIsIssuePeeked,
|
2024-01-16 07:16:03 +00:00
|
|
|
setPeekIssue,
|
|
|
|
issue: { getIssueById },
|
|
|
|
subIssues: { subIssueHelpersByIssueId, setSubIssueHelpers },
|
2024-03-20 14:30:34 +00:00
|
|
|
toggleCreateIssueModal,
|
|
|
|
toggleDeleteIssueModal,
|
2024-01-16 07:16:03 +00:00
|
|
|
} = useIssueDetail();
|
|
|
|
const project = useProject();
|
|
|
|
const { getProjectStates } = useProjectState();
|
2024-03-12 15:09:36 +00:00
|
|
|
const { isMobile } = usePlatformOS();
|
2024-01-16 07:16:03 +00:00
|
|
|
const issue = getIssueById(issueId);
|
|
|
|
const projectDetail = (issue && issue.project_id && project.getProjectById(issue.project_id)) || undefined;
|
|
|
|
const currentIssueStateDetail =
|
|
|
|
(issue?.project_id && getProjectStates(issue?.project_id)?.find((state) => issue?.state_id == state.id)) ||
|
|
|
|
undefined;
|
|
|
|
|
|
|
|
const subIssueHelpers = subIssueHelpersByIssueId(parentIssueId);
|
2024-05-24 14:09:28 +00:00
|
|
|
const subIssueCount = issue?.sub_issues_count ?? 0;
|
2024-01-16 07:16:03 +00:00
|
|
|
|
|
|
|
const handleIssuePeekOverview = (issue: TIssue) =>
|
|
|
|
workspaceSlug &&
|
|
|
|
issue &&
|
|
|
|
issue.project_id &&
|
|
|
|
issue.id &&
|
2024-04-30 11:50:02 +00:00
|
|
|
!getIsIssuePeeked(issue.id) &&
|
2024-01-16 07:16:03 +00:00
|
|
|
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
|
|
|
|
|
|
|
if (!issue) return <></>;
|
2024-05-22 10:06:11 +00:00
|
|
|
|
|
|
|
// check if current issue is the root issue
|
|
|
|
const isCurrentIssueRoot = issueId === rootIssueId;
|
|
|
|
|
2024-01-16 07:16:03 +00:00
|
|
|
return (
|
|
|
|
<div key={issueId}>
|
|
|
|
{issue && (
|
|
|
|
<div
|
|
|
|
className="group relative flex h-full w-full items-center gap-2 border-b border-custom-border-100 px-2 py-1 transition-all hover:bg-custom-background-90"
|
|
|
|
style={{ paddingLeft: `${spacingLeft}px` }}
|
|
|
|
>
|
|
|
|
<div className="h-[22px] w-[22px] flex-shrink-0">
|
2024-05-22 10:06:11 +00:00
|
|
|
{/* disable the chevron when current issue is also the root issue*/}
|
|
|
|
{subIssueCount > 0 && !isCurrentIssueRoot && (
|
2024-01-16 07:16:03 +00:00
|
|
|
<>
|
|
|
|
{subIssueHelpers.preview_loader.includes(issue.id) ? (
|
|
|
|
<div className="flex h-full w-full cursor-not-allowed items-center justify-center rounded-sm bg-custom-background-80 transition-all">
|
|
|
|
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
<div
|
|
|
|
className="flex h-full w-full cursor-pointer items-center justify-center rounded-sm transition-all hover:bg-custom-background-80"
|
2024-01-16 15:46:12 +00:00
|
|
|
onClick={async () => {
|
2024-01-23 11:26:22 +00:00
|
|
|
if (!subIssueHelpers.issue_visibility.includes(issueId)) {
|
|
|
|
setSubIssueHelpers(parentIssueId, "preview_loader", issueId);
|
|
|
|
await subIssueOperations.fetchSubIssues(workspaceSlug, projectId, issueId);
|
|
|
|
setSubIssueHelpers(parentIssueId, "preview_loader", issueId);
|
2024-01-16 15:46:12 +00:00
|
|
|
}
|
2024-01-23 11:26:22 +00:00
|
|
|
setSubIssueHelpers(parentIssueId, "issue_visibility", issueId);
|
2024-01-16 07:16:03 +00:00
|
|
|
}}
|
|
|
|
>
|
2024-03-15 11:59:22 +00:00
|
|
|
<ChevronRight
|
|
|
|
className={cn("h-3 w-3 transition-all", {
|
|
|
|
"rotate-90": subIssueHelpers.issue_visibility.includes(issue.id),
|
|
|
|
})}
|
|
|
|
strokeWidth={2}
|
|
|
|
/>
|
2024-01-16 07:16:03 +00:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="flex w-full cursor-pointer items-center gap-2">
|
|
|
|
<div
|
|
|
|
className="h-[6px] w-[6px] flex-shrink-0 rounded-full"
|
|
|
|
style={{
|
|
|
|
backgroundColor: currentIssueStateDetail?.color,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<div className="flex-shrink-0 text-xs text-custom-text-200">
|
|
|
|
{projectDetail?.identifier}-{issue?.sequence_id}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<ControlLink
|
2024-04-08 13:08:05 +00:00
|
|
|
id={`issue-${issue.id}`}
|
2024-01-16 07:16:03 +00:00
|
|
|
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
|
|
|
|
target="_blank"
|
|
|
|
onClick={() => handleIssuePeekOverview(issue)}
|
|
|
|
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
|
|
|
>
|
2024-03-12 15:09:36 +00:00
|
|
|
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
2024-01-16 07:16:03 +00:00
|
|
|
<span>{issue.name}</span>
|
|
|
|
</Tooltip>
|
|
|
|
</ControlLink>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="flex-shrink-0 text-sm">
|
|
|
|
<IssueProperty
|
|
|
|
workspaceSlug={workspaceSlug}
|
|
|
|
parentIssueId={parentIssueId}
|
|
|
|
issueId={issueId}
|
|
|
|
disabled={disabled}
|
|
|
|
subIssueOperations={subIssueOperations}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="flex-shrink-0 text-sm">
|
2024-01-23 08:55:09 +00:00
|
|
|
<CustomMenu placement="bottom-end" ellipsis>
|
2024-01-16 07:16:03 +00:00
|
|
|
{disabled && (
|
2024-03-20 14:30:34 +00:00
|
|
|
<CustomMenu.MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
handleIssueCrudState("update", parentIssueId, { ...issue });
|
|
|
|
toggleCreateIssueModal(true);
|
|
|
|
}}
|
|
|
|
>
|
2024-01-16 07:16:03 +00:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<Pencil className="h-3.5 w-3.5" strokeWidth={2} />
|
|
|
|
<span>Edit issue</span>
|
|
|
|
</div>
|
|
|
|
</CustomMenu.MenuItem>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{disabled && (
|
2024-03-20 14:30:34 +00:00
|
|
|
<CustomMenu.MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
handleIssueCrudState("delete", parentIssueId, issue);
|
2024-03-22 13:07:11 +00:00
|
|
|
toggleDeleteIssueModal(issue.id);
|
2024-03-20 14:30:34 +00:00
|
|
|
}}
|
|
|
|
>
|
2024-01-16 07:16:03 +00:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
|
|
|
|
<span>Delete issue</span>
|
|
|
|
</div>
|
|
|
|
</CustomMenu.MenuItem>
|
|
|
|
)}
|
|
|
|
|
|
|
|
<CustomMenu.MenuItem
|
|
|
|
onClick={() =>
|
|
|
|
subIssueOperations.copyText(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`)
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<LinkIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
|
|
|
<span>Copy issue link</span>
|
|
|
|
</div>
|
|
|
|
</CustomMenu.MenuItem>
|
|
|
|
</CustomMenu>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{disabled && (
|
|
|
|
<>
|
|
|
|
{subIssueHelpers.issue_loader.includes(issue.id) ? (
|
|
|
|
<div className="flex h-[22px] w-[22px] flex-shrink-0 cursor-not-allowed items-center justify-center overflow-hidden rounded-sm transition-all">
|
|
|
|
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
<div
|
|
|
|
className="invisible flex h-[22px] w-[22px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80 group-hover:visible"
|
|
|
|
onClick={() => {
|
|
|
|
subIssueOperations.removeSubIssue(workspaceSlug, issue.project_id, parentIssueId, issue.id);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<X width={14} strokeWidth={2} />
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
2024-05-22 10:06:11 +00:00
|
|
|
{/* should not expand the current issue if it is also the root issue*/}
|
|
|
|
{subIssueHelpers.issue_visibility.includes(issueId) && subIssueCount > 0 && !isCurrentIssueRoot && (
|
2024-01-16 07:16:03 +00:00
|
|
|
<IssueList
|
|
|
|
workspaceSlug={workspaceSlug}
|
|
|
|
projectId={issue.project_id}
|
|
|
|
parentIssueId={issue.id}
|
2024-05-22 10:06:11 +00:00
|
|
|
rootIssueId={rootIssueId}
|
2024-01-16 07:16:03 +00:00
|
|
|
spacingLeft={spacingLeft + 22}
|
|
|
|
disabled={disabled}
|
|
|
|
handleIssueCrudState={handleIssueCrudState}
|
|
|
|
subIssueOperations={subIssueOperations}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
});
|