[WEB-699] chore: implement sub-issues and attachments in the peek overview (#3956)

* chore: implement sub-issues and attachments in the peek overview

* chore: add the same to full-screen view
This commit is contained in:
Aaryan Khandelwal 2024-03-15 17:29:22 +05:30 committed by GitHub
parent 92a077dce1
commit 13bbb9cde4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 312 additions and 113 deletions

View File

@ -21,23 +21,21 @@ export const IssueAttachmentsList: FC<TIssueAttachmentsList> = observer((props)
const { const {
attachment: { getAttachmentsByIssueId }, attachment: { getAttachmentsByIssueId },
} = useIssueDetail(); } = useIssueDetail();
// derived values
const issueAttachments = getAttachmentsByIssueId(issueId); const issueAttachments = getAttachmentsByIssueId(issueId);
if (!issueAttachments) return <></>; if (!issueAttachments) return <></>;
return ( return (
<> <>
{issueAttachments && {issueAttachments?.map((attachmentId) => (
issueAttachments.length > 0 && <IssueAttachmentsDetail
issueAttachments.map((attachmentId) => ( key={attachmentId}
<IssueAttachmentsDetail attachmentId={attachmentId}
key={attachmentId} disabled={disabled}
attachmentId={attachmentId} handleAttachmentOperations={handleAttachmentOperations}
disabled={disabled} />
handleAttachmentOperations={handleAttachmentOperations} ))}
/>
))}
</> </>
); );
}); });

View File

@ -69,7 +69,7 @@ export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
</div> </div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left"> <div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100"> <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
Delete Attachment Delete attachment
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-custom-text-200"> <p className="text-sm text-custom-text-200">
@ -94,7 +94,7 @@ export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
}} }}
disabled={loader} disabled={loader}
> >
{loader ? "Deleting..." : "Delete"} {loader ? "Deleting" : "Delete"}
</Button> </Button>
</div> </div>
</Dialog.Panel> </Dialog.Panel>

View File

@ -101,7 +101,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
return ( return (
<div className="relative py-3 space-y-3"> <div className="relative py-3 space-y-3">
<h3 className="text-lg">Attachments</h3> <h3 className="text-lg">Attachments</h3>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<IssueAttachmentUpload <IssueAttachmentUpload
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
disabled={disabled} disabled={disabled}

View File

@ -1,6 +1,6 @@
import { FC } from "react"; import { FC } from "react";
import Link from "next/link";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react"; import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react";
// ui // ui
import { import {
@ -74,8 +74,6 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
handleRestoreIssue, handleRestoreIssue,
isSubmitting, isSubmitting,
} = props; } = props;
// router
const router = useRouter();
// store hooks // store hooks
const { currentUser } = useUser(); const { currentUser } = useUser();
const { const {
@ -101,10 +99,6 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
}); });
}); });
}; };
const redirectToIssueDetail = () => {
router.push({ pathname: `/${issueLink}` });
removeRoutePeekId();
};
// auth // auth
const isArchivingAllowed = !isArchived && !disabled; const isArchivingAllowed = !isArchived && !disabled;
const isInArchivableGroup = const isInArchivableGroup =
@ -122,9 +116,9 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" /> <MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</button> </button>
<button onClick={redirectToIssueDetail}> <Link href={`/${issueLink}`} onClick={() => removeRoutePeekId()}>
<MoveDiagonal className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" /> <MoveDiagonal className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</button> </Link>
{currentMode && ( {currentMode && (
<div className="flex flex-shrink-0 items-center gap-2"> <div className="flex flex-shrink-0 items-center gap-2">
<CustomSelect <CustomSelect

View File

@ -1,5 +1,6 @@
export * from "./header";
export * from "./issue-attachments";
export * from "./issue-detail"; export * from "./issue-detail";
export * from "./properties"; export * from "./properties";
export * from "./root"; export * from "./root";
export * from "./view"; export * from "./view";
export * from "./header";

View File

@ -0,0 +1,111 @@
import { useMemo } from "react";
// hooks
import { useEventTracker, useIssueDetail } from "hooks/store";
// components
import { IssueAttachmentUpload, IssueAttachmentsList, TAttachmentOperations } from "components/issues";
// ui
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
type Props = {
disabled: boolean;
issueId: string;
projectId: string;
workspaceSlug: string;
};
export const PeekOverviewIssueAttachments: React.FC<Props> = (props) => {
const { disabled, issueId, projectId, workspaceSlug } = props;
// store hooks
const { captureIssueEvent } = useEventTracker();
const {
attachment: { createAttachment, removeAttachment },
} = useIssueDetail();
const handleAttachmentOperations: TAttachmentOperations = useMemo(
() => ({
create: async (data: FormData) => {
try {
const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, data);
setPromiseToast(attachmentUploadPromise, {
loading: "Uploading attachment...",
success: {
title: "Attachment uploaded",
message: () => "The attachment has been successfully uploaded",
},
error: {
title: "Attachment not uploaded",
message: () => "The attachment could not be uploaded",
},
});
const res = await attachmentUploadPromise;
captureIssueEvent({
eventName: "Issue attachment added",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "attachment",
change_details: res.id,
},
});
} catch (error) {
captureIssueEvent({
eventName: "Issue attachment added",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
});
}
},
remove: async (attachmentId: string) => {
try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
await removeAttachment(workspaceSlug, projectId, issueId, attachmentId);
setToast({
message: "The attachment has been successfully removed",
type: TOAST_TYPE.SUCCESS,
title: "Attachment removed",
});
captureIssueEvent({
eventName: "Issue attachment deleted",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "attachment",
change_details: "",
},
});
} catch (error) {
captureIssueEvent({
eventName: "Issue attachment deleted",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "attachment",
change_details: "",
},
});
setToast({
message: "The Attachment could not be removed",
type: TOAST_TYPE.ERROR,
title: "Attachment not removed",
});
}
},
}),
[workspaceSlug, projectId, issueId, captureIssueEvent, createAttachment, removeAttachment]
);
return (
<div>
<h6 className="text-sm font-medium">Attachments</h6>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2 mt-3">
<IssueAttachmentUpload
workspaceSlug={workspaceSlug}
disabled={disabled}
handleAttachmentOperations={handleAttachmentOperations}
/>
<IssueAttachmentsList
issueId={issueId}
disabled={disabled}
handleAttachmentOperations={handleAttachmentOperations}
/>
</div>
</div>
);
};

View File

@ -55,7 +55,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
: undefined; : undefined;
return ( return (
<> <div className="space-y-2">
<span className="text-base font-medium text-custom-text-400"> <span className="text-base font-medium text-custom-text-400">
{projectDetails?.identifier}-{issue?.sequence_id} {projectDetails?.identifier}-{issue?.sequence_id}
</span> </span>
@ -89,6 +89,6 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
currentUser={currentUser} currentUser={currentUser}
/> />
)} )}
</> </div>
); );
}); });

View File

@ -61,7 +61,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
maxDate?.setDate(maxDate.getDate()); maxDate?.setDate(maxDate.getDate());
return ( return (
<div className="mt-1"> <div>
<h6 className="text-sm font-medium">Properties</h6> <h6 className="text-sm font-medium">Properties</h6>
{/* TODO: render properties using a common component */} {/* TODO: render properties using a common component */}
<div className={`w-full space-y-2 mt-3 ${disabled ? "opacity-60" : ""}`}> <div className={`w-full space-y-2 mt-3 ${disabled ? "opacity-60" : ""}`}>

View File

@ -11,13 +11,15 @@ import {
PeekOverviewProperties, PeekOverviewProperties,
TIssueOperations, TIssueOperations,
ArchiveIssueModal, ArchiveIssueModal,
PeekOverviewIssueAttachments,
} from "components/issues"; } from "components/issues";
// hooks // hooks
import { useIssueDetail } from "hooks/store"; import { useIssueDetail, useUser } from "hooks/store";
import useKeypress from "hooks/use-keypress"; import useKeypress from "hooks/use-keypress";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// store hooks // store hooks
import { IssueActivity } from "../issue-detail/issue-activity"; import { IssueActivity } from "../issue-detail/issue-activity";
import { SubIssuesRoot } from "../sub-issues";
interface IIssueView { interface IIssueView {
workspaceSlug: string; workspaceSlug: string;
@ -37,6 +39,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
// ref // ref
const issuePeekOverviewRef = useRef<HTMLDivElement>(null); const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
// store hooks // store hooks
const { currentUser } = useUser();
const { const {
setPeekIssue, setPeekIssue,
isAnyModalOpen, isAnyModalOpen,
@ -147,7 +150,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issue && ( issue && (
<> <>
{["side-peek", "modal"].includes(peekMode) ? ( {["side-peek", "modal"].includes(peekMode) ? (
<div className="relative flex flex-col gap-3 px-8 py-5"> <div className="relative flex flex-col gap-3 px-8 py-5 space-y-3">
<PeekOverviewIssueDetails <PeekOverviewIssueDetails
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
@ -158,6 +161,23 @@ export const IssueView: FC<IIssueView> = observer((props) => {
setIsSubmitting={(value) => setIsSubmitting(value)} setIsSubmitting={(value) => setIsSubmitting(value)}
/> />
{currentUser && (
<SubIssuesRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssueId={issueId}
currentUser={currentUser}
disabled={disabled || is_archived}
/>
)}
<PeekOverviewIssueAttachments
disabled={disabled || is_archived}
issueId={issueId}
projectId={projectId}
workspaceSlug={workspaceSlug}
/>
<PeekOverviewProperties <PeekOverviewProperties
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
@ -169,9 +189,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} /> <IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
</div> </div>
) : ( ) : (
<div className={`vertical-scrollbar flex h-full w-full overflow-auto`}> <div className="vertical-scrollbar flex h-full w-full overflow-auto">
<div className="relative h-full w-full space-y-6 overflow-auto p-4 py-5"> <div className="relative h-full w-full space-y-6 overflow-auto p-4 py-5">
<div> <div className="space-y-3">
<PeekOverviewIssueDetails <PeekOverviewIssueDetails
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
@ -182,6 +202,23 @@ export const IssueView: FC<IIssueView> = observer((props) => {
setIsSubmitting={(value) => setIsSubmitting(value)} setIsSubmitting={(value) => setIsSubmitting(value)}
/> />
{currentUser && (
<SubIssuesRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssueId={issueId}
currentUser={currentUser}
disabled={disabled || is_archived}
/>
)}
<PeekOverviewIssueAttachments
disabled={disabled || is_archived}
issueId={issueId}
projectId={projectId}
workspaceSlug={workspaceSlug}
/>
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} /> <IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
// components // components
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
import { useIssueDetail, useProject, useProjectState } from "hooks/store"; import { useIssueDetail, useProject, useProjectState } from "hooks/store";
@ -11,6 +11,7 @@ import { IssueProperty } from "./properties";
// ui // ui
// types // types
import { TSubIssueOperations } from "./root"; import { TSubIssueOperations } from "./root";
import { cn } from "helpers/common.helper";
// import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; // import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
export interface ISubIssues { export interface ISubIssues {
@ -90,11 +91,12 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
setSubIssueHelpers(parentIssueId, "issue_visibility", issueId); setSubIssueHelpers(parentIssueId, "issue_visibility", issueId);
}} }}
> >
{subIssueHelpers.issue_visibility.includes(issue.id) ? ( <ChevronRight
<ChevronDown width={14} strokeWidth={2} /> className={cn("h-3 w-3 transition-all", {
) : ( "rotate-90": subIssueHelpers.issue_visibility.includes(issue.id),
<ChevronRight width={14} strokeWidth={2} /> })}
)} strokeWidth={2}
/>
</div> </div>
)} )}
</> </>

View File

@ -1,9 +1,9 @@
import { FC, useCallback, useEffect, useMemo, useState } from "react"; import { FC, useCallback, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react"; import { Plus, ChevronRight, Loader, Pencil } from "lucide-react";
// hooks // hooks
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { CircularProgressIndicator, CustomMenu, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
import { ExistingIssuesListModal } from "components/core"; import { ExistingIssuesListModal } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
@ -11,7 +11,7 @@ import { useEventTracker, useIssueDetail } from "hooks/store";
// components // components
import { IUser, TIssue } from "@plane/types"; import { IUser, TIssue } from "@plane/types";
import { IssueList } from "./issues-list"; import { IssueList } from "./issues-list";
import { ProgressBar } from "./progressbar"; import { cn } from "helpers/common.helper";
// ui // ui
// helpers // helpers
// types // types
@ -53,6 +53,10 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
updateSubIssue, updateSubIssue,
removeSubIssue, removeSubIssue,
deleteSubIssue, deleteSubIssue,
isCreateIssueModalOpen,
toggleCreateIssueModal,
isSubIssuesModalOpen,
toggleSubIssuesModal,
} = useIssueDetail(); } = useIssueDetail();
const { setTrackElement, captureIssueEvent } = useEventTracker(); const { setTrackElement, captureIssueEvent } = useEventTracker();
// state // state
@ -310,55 +314,81 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
<> <>
{subIssues && subIssues?.length > 0 ? ( {subIssues && subIssues?.length > 0 ? (
<> <>
<div className="relative flex items-center gap-4 text-xs"> <div className="relative flex items-center justify-between gap-2 text-xs">
<div <div className="flex items-center gap-2">
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" <button
onClick={handleFetchSubIssues} type="button"
> className="flex items-center gap-1 rounded py-1 px-2 transition-all hover:bg-custom-background-80 font-medium"
<div className="flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center"> onClick={handleFetchSubIssues}
{subIssueHelpers.preview_loader.includes(parentIssueId) ? ( >
<Loader width={14} strokeWidth={2} className="animate-spin" /> <div className="flex flex-shrink-0 items-center justify-center">
) : subIssueHelpers.issue_visibility.includes(parentIssueId) ? ( {subIssueHelpers.preview_loader.includes(parentIssueId) ? (
<ChevronDown width={16} strokeWidth={2} /> <Loader strokeWidth={2} className="h-3 w-3 animate-spin" />
) : ( ) : (
<ChevronRight width={14} strokeWidth={2} /> <ChevronRight
)} className={cn("h-3 w-3 transition-all", {
"rotate-90": subIssueHelpers.issue_visibility.includes(parentIssueId),
})}
strokeWidth={2}
/>
)}
</div>
<div>Sub-issues</div>
</button>
<div className="flex items-center gap-2 text-custom-text-300">
<CircularProgressIndicator
size={16}
percentage={
subIssuesDistribution?.completed?.length && subIssues.length
? (subIssuesDistribution?.completed?.length / subIssues.length) * 100
: 0
}
strokeWidth={3}
/>
<span>
{subIssuesDistribution?.completed?.length ?? 0}/{subIssues.length} Done
</span>
</div> </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> </div>
{!disabled && ( {!disabled && (
<div className="ml-auto flex flex-shrink-0 select-none items-center gap-2"> <CustomMenu
<div label={
className="cursor-pointer rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80" <>
<Plus className="h-3 w-3" />
Add sub-issue
</>
}
buttonClassName="whitespace-nowrap"
placement="bottom-end"
noBorder
noChevron
>
<CustomMenu.MenuItem
onClick={() => { onClick={() => {
setTrackElement("Issue detail add sub-issue"); setTrackElement("Issue detail nested sub-issue");
handleIssueCrudState("create", parentIssueId, null); handleIssueCrudState("create", parentIssueId, null);
toggleCreateIssueModal(true);
}} }}
> >
Add sub-issue <div className="flex items-center gap-2">
</div> <Pencil className="h-3 w-3" />
<div <span>Create new</span>
className="cursor-pointer rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80" </div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => { onClick={() => {
setTrackElement("Issue detail add sub-issue"); setTrackElement("Issue detail nested sub-issue");
handleIssueCrudState("existing", parentIssueId, null); handleIssueCrudState("existing", parentIssueId, null);
toggleSubIssuesModal(true);
}} }}
> >
Add an existing issue <div className="flex items-center gap-2">
</div> <LayersIcon className="h-3 w-3" />
</div> <span>Add existing</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)} )}
</div> </div>
@ -379,62 +409,74 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
) : ( ) : (
!disabled && ( !disabled && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="py-2 text-xs italic text-custom-text-300">No Sub-Issues yet</div> <div className="text-xs italic text-custom-text-300">No sub-issues yet</div>
<div> <CustomMenu
<CustomMenu label={
label={ <>
<> <Plus className="h-3 w-3" />
<Plus className="h-3 w-3" /> Add sub-issue
Add sub-issue </>
</> }
} buttonClassName="whitespace-nowrap"
buttonClassName="whitespace-nowrap" placement="bottom-end"
placement="bottom-end" noBorder
noBorder noChevron
noChevron >
<CustomMenu.MenuItem
onClick={() => {
setTrackElement("Issue detail nested sub-issue");
handleIssueCrudState("create", parentIssueId, null);
toggleCreateIssueModal(true);
}}
> >
<CustomMenu.MenuItem <div className="flex items-center gap-2">
onClick={() => { <Pencil className="h-3 w-3" />
setTrackElement("Issue detail nested sub-issue"); <span>Create new</span>
handleIssueCrudState("create", parentIssueId, null); </div>
}} </CustomMenu.MenuItem>
> <CustomMenu.MenuItem
Create new onClick={() => {
</CustomMenu.MenuItem> setTrackElement("Issue detail nested sub-issue");
<CustomMenu.MenuItem handleIssueCrudState("existing", parentIssueId, null);
onClick={() => { toggleSubIssuesModal(true);
setTrackElement("Issue detail nested sub-issue"); }}
handleIssueCrudState("existing", parentIssueId, null); >
}} <div className="flex items-center gap-2">
> <LayersIcon className="h-3 w-3" />
Add an existing issue <span>Add existing</span>
</CustomMenu.MenuItem> </div>
</CustomMenu> </CustomMenu.MenuItem>
</div> </CustomMenu>
</div> </div>
) )
)} )}
{/* issue create, add from existing , update and delete modals */} {/* issue create, add from existing , update and delete modals */}
{issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && ( {issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && isCreateIssueModalOpen && (
<CreateUpdateIssueModal <CreateUpdateIssueModal
isOpen={issueCrudState?.create?.toggle} isOpen={issueCrudState?.create?.toggle}
data={{ data={{
parent_id: issueCrudState?.create?.parentIssueId, parent_id: issueCrudState?.create?.parentIssueId,
}} }}
onClose={() => handleIssueCrudState("create", null, null)} onClose={() => {
handleIssueCrudState("create", null, null);
toggleCreateIssueModal(false);
}}
onSubmit={async (_issue: TIssue) => { onSubmit={async (_issue: TIssue) => {
await subIssueOperations.addSubIssue(workspaceSlug, projectId, parentIssueId, [_issue.id]); await subIssueOperations.addSubIssue(workspaceSlug, projectId, parentIssueId, [_issue.id]);
}} }}
/> />
)} )}
{issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && ( {issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && isSubIssuesModalOpen && (
<ExistingIssuesListModal <ExistingIssuesListModal
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
isOpen={issueCrudState?.existing?.toggle} isOpen={issueCrudState?.existing?.toggle}
handleClose={() => handleIssueCrudState("existing", null, null)} handleClose={() => {
handleIssueCrudState("existing", null, null);
toggleSubIssuesModal(false);
}}
searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }} searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }}
handleOnSubmit={(_issue) => handleOnSubmit={(_issue) =>
subIssueOperations.addSubIssue( subIssueOperations.addSubIssue(

View File

@ -44,20 +44,24 @@ export interface IIssueDetail
IIssueCommentReactionStoreActions { IIssueCommentReactionStoreActions {
// observables // observables
peekIssue: TPeekIssue | undefined; peekIssue: TPeekIssue | undefined;
isCreateIssueModalOpen: boolean;
isIssueLinkModalOpen: boolean; isIssueLinkModalOpen: boolean;
isParentIssueModalOpen: boolean; isParentIssueModalOpen: boolean;
isDeleteIssueModalOpen: boolean; isDeleteIssueModalOpen: boolean;
isArchiveIssueModalOpen: boolean; isArchiveIssueModalOpen: boolean;
isRelationModalOpen: TIssueRelationTypes | null; isRelationModalOpen: TIssueRelationTypes | null;
isSubIssuesModalOpen: boolean;
// computed // computed
isAnyModalOpen: boolean; isAnyModalOpen: boolean;
// actions // actions
setPeekIssue: (peekIssue: TPeekIssue | undefined) => void; setPeekIssue: (peekIssue: TPeekIssue | undefined) => void;
toggleCreateIssueModal: (value: boolean) => void;
toggleIssueLinkModal: (value: boolean) => void; toggleIssueLinkModal: (value: boolean) => void;
toggleParentIssueModal: (value: boolean) => void; toggleParentIssueModal: (value: boolean) => void;
toggleDeleteIssueModal: (value: boolean) => void; toggleDeleteIssueModal: (value: boolean) => void;
toggleArchiveIssueModal: (value: boolean) => void; toggleArchiveIssueModal: (value: boolean) => void;
toggleRelationModal: (value: TIssueRelationTypes | null) => void; toggleRelationModal: (value: TIssueRelationTypes | null) => void;
toggleSubIssuesModal: (value: boolean) => void;
// store // store
rootIssueStore: IIssueRootStore; rootIssueStore: IIssueRootStore;
issue: IIssueStore; issue: IIssueStore;
@ -75,11 +79,13 @@ export interface IIssueDetail
export class IssueDetail implements IIssueDetail { export class IssueDetail implements IIssueDetail {
// observables // observables
peekIssue: TPeekIssue | undefined = undefined; peekIssue: TPeekIssue | undefined = undefined;
isCreateIssueModalOpen: boolean = false;
isIssueLinkModalOpen: boolean = false; isIssueLinkModalOpen: boolean = false;
isParentIssueModalOpen: boolean = false; isParentIssueModalOpen: boolean = false;
isDeleteIssueModalOpen: boolean = false; isDeleteIssueModalOpen: boolean = false;
isArchiveIssueModalOpen: boolean = false; isArchiveIssueModalOpen: boolean = false;
isRelationModalOpen: TIssueRelationTypes | null = null; isRelationModalOpen: TIssueRelationTypes | null = null;
isSubIssuesModalOpen: boolean = false;
// store // store
rootIssueStore: IIssueRootStore; rootIssueStore: IIssueRootStore;
issue: IIssueStore; issue: IIssueStore;
@ -97,20 +103,24 @@ export class IssueDetail implements IIssueDetail {
makeObservable(this, { makeObservable(this, {
// observables // observables
peekIssue: observable, peekIssue: observable,
isCreateIssueModalOpen: observable,
isIssueLinkModalOpen: observable.ref, isIssueLinkModalOpen: observable.ref,
isParentIssueModalOpen: observable.ref, isParentIssueModalOpen: observable.ref,
isDeleteIssueModalOpen: observable.ref, isDeleteIssueModalOpen: observable.ref,
isArchiveIssueModalOpen: observable.ref, isArchiveIssueModalOpen: observable.ref,
isRelationModalOpen: observable.ref, isRelationModalOpen: observable.ref,
isSubIssuesModalOpen: observable.ref,
// computed // computed
isAnyModalOpen: computed, isAnyModalOpen: computed,
// action // action
setPeekIssue: action, setPeekIssue: action,
toggleCreateIssueModal: action,
toggleIssueLinkModal: action, toggleIssueLinkModal: action,
toggleParentIssueModal: action, toggleParentIssueModal: action,
toggleDeleteIssueModal: action, toggleDeleteIssueModal: action,
toggleArchiveIssueModal: action, toggleArchiveIssueModal: action,
toggleRelationModal: action, toggleRelationModal: action,
toggleSubIssuesModal: action,
}); });
// store // store
@ -130,21 +140,25 @@ export class IssueDetail implements IIssueDetail {
// computed // computed
get isAnyModalOpen() { get isAnyModalOpen() {
return ( return (
this.isCreateIssueModalOpen ||
this.isIssueLinkModalOpen || this.isIssueLinkModalOpen ||
this.isParentIssueModalOpen || this.isParentIssueModalOpen ||
this.isDeleteIssueModalOpen || this.isDeleteIssueModalOpen ||
this.isArchiveIssueModalOpen || this.isArchiveIssueModalOpen ||
Boolean(this.isRelationModalOpen) Boolean(this.isRelationModalOpen) ||
this.isSubIssuesModalOpen
); );
} }
// actions // actions
setPeekIssue = (peekIssue: TPeekIssue | undefined) => (this.peekIssue = peekIssue); setPeekIssue = (peekIssue: TPeekIssue | undefined) => (this.peekIssue = peekIssue);
toggleCreateIssueModal = (value: boolean) => (this.isCreateIssueModalOpen = value);
toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value); toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value);
toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value); toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value);
toggleDeleteIssueModal = (value: boolean) => (this.isDeleteIssueModalOpen = value); toggleDeleteIssueModal = (value: boolean) => (this.isDeleteIssueModalOpen = value);
toggleArchiveIssueModal = (value: boolean) => (this.isArchiveIssueModalOpen = value); toggleArchiveIssueModal = (value: boolean) => (this.isArchiveIssueModalOpen = value);
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value); toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
toggleSubIssuesModal = (value: boolean) => (this.isSubIssuesModalOpen = value);
// issue // issue
fetchIssue = async ( fetchIssue = async (