plane/web/components/issues/sub-issues/root.tsx
rahulramesha fadda7cf04 fix: refactor related bugs (#3384)
* fix sub issues inside issue detail

* close peek over view after opening issue detail

* fix error while opening peek overview

* fix saving project views

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:19:44 +05:30

429 lines
16 KiB
TypeScript

import { FC, useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react";
import useSWR from "swr";
// hooks
import { useIssueDetail } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { ExistingIssuesListModal } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { IssueList } from "./issues-list";
import { ProgressBar } from "./progressbar";
// ui
import { CustomMenu } from "@plane/ui";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { IUser, TIssue } from "@plane/types";
export interface ISubIssuesRoot {
workspaceSlug: string;
projectId: string;
parentIssueId: string;
currentUser: IUser;
disabled: boolean;
}
export type TSubIssueOperations = {
copyText: (text: string) => void;
fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise<void>;
addSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => Promise<void>;
updateSubIssue: (
workspaceSlug: string,
projectId: string,
parentIssueId: string,
issueId: string,
data: Partial<TIssue>
) => Promise<void>;
removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
};
export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
const { workspaceSlug, projectId, parentIssueId, disabled = false } = props;
// store hooks
const { setToastAlert } = useToast();
const {
issue: { getIssueById },
subIssues: { subIssuesByIssueId, stateDistributionByIssueId, subIssueHelpersByIssueId, setSubIssueHelpers },
fetchSubIssues,
createSubIssues,
updateSubIssue,
removeSubIssue,
deleteSubIssue,
} = useIssueDetail();
// state
type TIssueCrudState = { toggle: boolean; parentIssueId: string | undefined; issue: TIssue | undefined };
const [issueCrudState, setIssueCrudState] = useState<{
create: TIssueCrudState;
existing: TIssueCrudState;
update: TIssueCrudState;
delete: TIssueCrudState;
}>({
create: {
toggle: false,
parentIssueId: undefined,
issue: undefined,
},
existing: {
toggle: false,
parentIssueId: undefined,
issue: undefined,
},
update: {
toggle: false,
parentIssueId: undefined,
issue: undefined,
},
delete: {
toggle: false,
parentIssueId: undefined,
issue: undefined,
},
});
useSWR(
workspaceSlug && projectId && parentIssueId
? `ISSUE_DETAIL_SUB_ISSUES_${workspaceSlug}_${projectId}_${parentIssueId}`
: null,
async () => {
workspaceSlug &&
projectId &&
parentIssueId &&
(await subIssueOperations.fetchSubIssues(workspaceSlug, projectId, parentIssueId));
}
);
const handleIssueCrudState = (
key: "create" | "existing" | "update" | "delete",
_parentIssueId: string | null,
issue: TIssue | null = null
) => {
setIssueCrudState({
...issueCrudState,
[key]: {
toggle: !issueCrudState[key].toggle,
parentIssueId: _parentIssueId,
issue: issue,
},
});
};
const subIssueOperations: TSubIssueOperations = useMemo(
() => ({
copyText: (text: string) => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${text}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Issue link copied to clipboard.",
});
});
},
fetchSubIssues: async (workspaceSlug: string, projectId: string, parentIssueId: string) => {
try {
await fetchSubIssues(workspaceSlug, projectId, parentIssueId);
} catch (error) {
setToastAlert({
type: "error",
title: "Error fetching sub-issues",
message: "Error fetching sub-issues",
});
}
},
addSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => {
try {
await createSubIssues(workspaceSlug, projectId, parentIssueId, issueIds);
setToastAlert({
type: "success",
title: "Sub-issues added successfully",
message: "Sub-issues added successfully",
});
} catch (error) {
setToastAlert({
type: "error",
title: "Error adding sub-issue",
message: "Error adding sub-issue",
});
}
},
updateSubIssue: async (
workspaceSlug: string,
projectId: string,
parentIssueId: string,
issueId: string,
data: Partial<TIssue>
) => {
try {
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
await updateSubIssue(workspaceSlug, projectId, parentIssueId, issueId, data);
setToastAlert({
type: "success",
title: "Sub-issue updated successfully",
message: "Sub-issue updated successfully",
});
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
} catch (error) {
setToastAlert({
type: "error",
title: "Error updating sub-issue",
message: "Error updating sub-issue",
});
}
},
removeSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
try {
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
setToastAlert({
type: "success",
title: "Sub-issue removed successfully",
message: "Sub-issue removed successfully",
});
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
} catch (error) {
setToastAlert({
type: "error",
title: "Error removing sub-issue",
message: "Error removing sub-issue",
});
}
},
deleteSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
try {
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
await deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
setToastAlert({
type: "success",
title: "Issue deleted successfully",
message: "Issue deleted successfully",
});
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
} catch (error) {
setToastAlert({
type: "error",
title: "Error deleting issue",
message: "Error deleting issue",
});
}
},
}),
[fetchSubIssues, createSubIssues, updateSubIssue, removeSubIssue, deleteSubIssue, setToastAlert, setSubIssueHelpers]
);
const issue = getIssueById(parentIssueId);
const subIssuesDistribution = stateDistributionByIssueId(parentIssueId);
const subIssues = subIssuesByIssueId(parentIssueId);
const subIssueHelpers = subIssueHelpersByIssueId(`${parentIssueId}_root`);
if (!issue) return <></>;
return (
<div className="h-full w-full space-y-2">
{!subIssues ? (
<div className="py-3 text-center text-sm font-medium text-custom-text-300">Loading...</div>
) : (
<>
{subIssues && subIssues?.length > 0 ? (
<>
<div className="relative flex items-center gap-4 text-xs">
<div
className="flex cursor-pointer select-none items-center gap-1 rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
onClick={() => setSubIssueHelpers(`${parentIssueId}_root`, "issue_visibility", parentIssueId)}
>
<div className="flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center">
{subIssueHelpers.preview_loader.includes(parentIssueId) ? (
<Loader width={14} strokeWidth={2} className="animate-spin" />
) : subIssueHelpers.issue_visibility.includes(parentIssueId) ? (
<ChevronDown width={16} strokeWidth={2} />
) : (
<ChevronRight width={14} strokeWidth={2} />
)}
</div>
<div>Sub-issues</div>
<div>({subIssues?.length || 0})</div>
</div>
<div className="w-full max-w-[250px] select-none">
<ProgressBar
total={subIssues?.length || 0}
done={
((subIssuesDistribution?.cancelled ?? []).length || 0) +
((subIssuesDistribution?.completed ?? []).length || 0)
}
/>
</div>
{!disabled && (
<div className="ml-auto flex flex-shrink-0 select-none items-center gap-2">
<div
className="cursor-pointer rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
onClick={() => handleIssueCrudState("create", parentIssueId, null)}
>
Add sub-issue
</div>
<div
className="cursor-pointer rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
onClick={() => handleIssueCrudState("existing", parentIssueId, null)}
>
Add an existing issue
</div>
</div>
)}
</div>
{subIssueHelpers.issue_visibility.includes(parentIssueId) && (
<div className="border border-b-0 border-custom-border-100">
<IssueList
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssueId={parentIssueId}
spacingLeft={10}
disabled={!disabled}
handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations}
/>
</div>
)}
<div>
<CustomMenu
label={
<div className="flex items-center gap-1">
<Plus className="h-3 w-3" />
Add sub-issue
</div>
}
buttonClassName="whitespace-nowrap"
placement="bottom-end"
noBorder
noChevron
>
<CustomMenu.MenuItem
onClick={() => {
handleIssueCrudState("create", parentIssueId, null);
}}
>
Create new
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
handleIssueCrudState("existing", parentIssueId, null);
}}
>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</>
) : (
!disabled && (
<div className="flex items-center justify-between">
<div className="py-2 text-xs italic text-custom-text-300">No Sub-Issues yet</div>
<div>
<CustomMenu
label={
<>
<Plus className="h-3 w-3" />
Add sub-issue
</>
}
buttonClassName="whitespace-nowrap"
placement="bottom-end"
noBorder
noChevron
>
<CustomMenu.MenuItem
onClick={() => {
handleIssueCrudState("create", parentIssueId, null);
}}
>
Create new
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
handleIssueCrudState("existing", parentIssueId, null);
}}
>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
)
)}
{/* issue create, add from existing , update and delete modals */}
{issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && (
<CreateUpdateIssueModal
isOpen={issueCrudState?.create?.toggle}
data={{
parent_id: issueCrudState?.create?.parentIssueId,
}}
onClose={() => handleIssueCrudState("create", null, null)}
onSubmit={async (_issue: TIssue) => {
await subIssueOperations.addSubIssue(workspaceSlug, projectId, parentIssueId, [_issue.id]);
}}
/>
)}
{issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && (
<ExistingIssuesListModal
isOpen={issueCrudState?.existing?.toggle}
handleClose={() => handleIssueCrudState("existing", null, null)}
searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }}
handleOnSubmit={(_issue) =>
subIssueOperations.addSubIssue(
workspaceSlug,
projectId,
parentIssueId,
_issue.map((issue) => issue.id)
)
}
workspaceLevelToggle
/>
)}
{issueCrudState?.update?.toggle && issueCrudState?.update?.issue && (
<>
<CreateUpdateIssueModal
isOpen={issueCrudState?.update?.toggle}
onClose={() => {
handleIssueCrudState("update", null, null);
}}
data={issueCrudState?.update?.issue ?? undefined}
onSubmit={async (_issue: TIssue) => {
await subIssueOperations.updateSubIssue(workspaceSlug, projectId, parentIssueId, _issue.id, _issue);
}}
/>
</>
)}
{issueCrudState?.delete?.toggle &&
issueCrudState?.delete?.issue &&
issueCrudState.delete.parentIssueId &&
issueCrudState.delete.issue.id && (
<DeleteIssueModal
isOpen={issueCrudState?.delete?.toggle}
handleClose={() => {
handleIssueCrudState("delete", null, null);
}}
data={issueCrudState?.delete?.issue as TIssue}
onSubmit={async () =>
await subIssueOperations.deleteSubIssue(
workspaceSlug,
projectId,
issueCrudState?.delete?.parentIssueId as string,
issueCrudState?.delete?.issue?.id as string
)
}
/>
)}
</>
)}
</div>
);
});