forked from github/plane
[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:
parent
92a077dce1
commit
13bbb9cde4
@ -21,23 +21,21 @@ export const IssueAttachmentsList: FC<TIssueAttachmentsList> = observer((props)
|
||||
const {
|
||||
attachment: { getAttachmentsByIssueId },
|
||||
} = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const issueAttachments = getAttachmentsByIssueId(issueId);
|
||||
|
||||
if (!issueAttachments) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueAttachments &&
|
||||
issueAttachments.length > 0 &&
|
||||
issueAttachments.map((attachmentId) => (
|
||||
<IssueAttachmentsDetail
|
||||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
handleAttachmentOperations={handleAttachmentOperations}
|
||||
/>
|
||||
))}
|
||||
{issueAttachments?.map((attachmentId) => (
|
||||
<IssueAttachmentsDetail
|
||||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
handleAttachmentOperations={handleAttachmentOperations}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -69,7 +69,7 @@ export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
||||
</div>
|
||||
<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">
|
||||
Delete Attachment
|
||||
Delete attachment
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
@ -94,7 +94,7 @@ export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
||||
}}
|
||||
disabled={loader}
|
||||
>
|
||||
{loader ? "Deleting..." : "Delete"}
|
||||
{loader ? "Deleting" : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
@ -101,7 +101,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
||||
return (
|
||||
<div className="relative py-3 space-y-3">
|
||||
<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
|
||||
workspaceSlug={workspaceSlug}
|
||||
disabled={disabled}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react";
|
||||
// ui
|
||||
import {
|
||||
@ -74,8 +74,6 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
||||
handleRestoreIssue,
|
||||
isSubmitting,
|
||||
} = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const {
|
||||
@ -101,10 +99,6 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
||||
});
|
||||
});
|
||||
};
|
||||
const redirectToIssueDetail = () => {
|
||||
router.push({ pathname: `/${issueLink}` });
|
||||
removeRoutePeekId();
|
||||
};
|
||||
// auth
|
||||
const isArchivingAllowed = !isArchived && !disabled;
|
||||
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" />
|
||||
</button>
|
||||
|
||||
<button onClick={redirectToIssueDetail}>
|
||||
<Link href={`/${issueLink}`} onClick={() => removeRoutePeekId()}>
|
||||
<MoveDiagonal className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
</Link>
|
||||
{currentMode && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<CustomSelect
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from "./header";
|
||||
export * from "./issue-attachments";
|
||||
export * from "./issue-detail";
|
||||
export * from "./properties";
|
||||
export * from "./root";
|
||||
export * from "./view";
|
||||
export * from "./header";
|
||||
|
111
web/components/issues/peek-overview/issue-attachments.tsx
Normal file
111
web/components/issues/peek-overview/issue-attachments.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -55,7 +55,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<span className="text-base font-medium text-custom-text-400">
|
||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||
</span>
|
||||
@ -89,6 +89,6 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -61,7 +61,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
return (
|
||||
<div className="mt-1">
|
||||
<div>
|
||||
<h6 className="text-sm font-medium">Properties</h6>
|
||||
{/* TODO: render properties using a common component */}
|
||||
<div className={`w-full space-y-2 mt-3 ${disabled ? "opacity-60" : ""}`}>
|
||||
|
@ -11,13 +11,15 @@ import {
|
||||
PeekOverviewProperties,
|
||||
TIssueOperations,
|
||||
ArchiveIssueModal,
|
||||
PeekOverviewIssueAttachments,
|
||||
} from "components/issues";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { useIssueDetail, useUser } from "hooks/store";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// store hooks
|
||||
import { IssueActivity } from "../issue-detail/issue-activity";
|
||||
import { SubIssuesRoot } from "../sub-issues";
|
||||
|
||||
interface IIssueView {
|
||||
workspaceSlug: string;
|
||||
@ -37,6 +39,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
// ref
|
||||
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const {
|
||||
setPeekIssue,
|
||||
isAnyModalOpen,
|
||||
@ -147,7 +150,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
issue && (
|
||||
<>
|
||||
{["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
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
@ -158,6 +161,23 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
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
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
@ -169,9 +189,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
</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>
|
||||
<div className="space-y-3">
|
||||
<PeekOverviewIssueDetails
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
@ -182,6 +202,23 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
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} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
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
|
||||
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
||||
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||
@ -11,6 +11,7 @@ import { IssueProperty } from "./properties";
|
||||
// ui
|
||||
// types
|
||||
import { TSubIssueOperations } from "./root";
|
||||
import { cn } from "helpers/common.helper";
|
||||
// import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
||||
|
||||
export interface ISubIssues {
|
||||
@ -90,11 +91,12 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
||||
setSubIssueHelpers(parentIssueId, "issue_visibility", issueId);
|
||||
}}
|
||||
>
|
||||
{subIssueHelpers.issue_visibility.includes(issue.id) ? (
|
||||
<ChevronDown width={14} strokeWidth={2} />
|
||||
) : (
|
||||
<ChevronRight width={14} strokeWidth={2} />
|
||||
)}
|
||||
<ChevronRight
|
||||
className={cn("h-3 w-3 transition-all", {
|
||||
"rotate-90": subIssueHelpers.issue_visibility.includes(issue.id),
|
||||
})}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react";
|
||||
import { Plus, ChevronRight, Loader, Pencil } from "lucide-react";
|
||||
// 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 { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
@ -11,7 +11,7 @@ import { useEventTracker, useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IUser, TIssue } from "@plane/types";
|
||||
import { IssueList } from "./issues-list";
|
||||
import { ProgressBar } from "./progressbar";
|
||||
import { cn } from "helpers/common.helper";
|
||||
// ui
|
||||
// helpers
|
||||
// types
|
||||
@ -53,6 +53,10 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
updateSubIssue,
|
||||
removeSubIssue,
|
||||
deleteSubIssue,
|
||||
isCreateIssueModalOpen,
|
||||
toggleCreateIssueModal,
|
||||
isSubIssuesModalOpen,
|
||||
toggleSubIssuesModal,
|
||||
} = useIssueDetail();
|
||||
const { setTrackElement, captureIssueEvent } = useEventTracker();
|
||||
// state
|
||||
@ -310,55 +314,81 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
<>
|
||||
{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={handleFetchSubIssues}
|
||||
>
|
||||
<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 className="relative flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded py-1 px-2 transition-all hover:bg-custom-background-80 font-medium"
|
||||
onClick={handleFetchSubIssues}
|
||||
>
|
||||
<div className="flex flex-shrink-0 items-center justify-center">
|
||||
{subIssueHelpers.preview_loader.includes(parentIssueId) ? (
|
||||
<Loader strokeWidth={2} className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<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>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"
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add sub-issue
|
||||
</>
|
||||
}
|
||||
buttonClassName="whitespace-nowrap"
|
||||
placement="bottom-end"
|
||||
noBorder
|
||||
noChevron
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail add sub-issue");
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("create", parentIssueId, null);
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
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"
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Create new</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail add sub-issue");
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
toggleSubIssuesModal(true);
|
||||
}}
|
||||
>
|
||||
Add an existing issue
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayersIcon className="h-3 w-3" />
|
||||
<span>Add existing</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -379,62 +409,74 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
) : (
|
||||
!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
|
||||
<div className="text-xs italic text-custom-text-300">No sub-issues yet</div>
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add sub-issue
|
||||
</>
|
||||
}
|
||||
buttonClassName="whitespace-nowrap"
|
||||
placement="bottom-end"
|
||||
noBorder
|
||||
noChevron
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("create", parentIssueId, null);
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("create", parentIssueId, null);
|
||||
}}
|
||||
>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
}}
|
||||
>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Create new</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
toggleSubIssuesModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayersIcon className="h-3 w-3" />
|
||||
<span>Add existing</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* issue create, add from existing , update and delete modals */}
|
||||
{issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && (
|
||||
{issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && isCreateIssueModalOpen && (
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={issueCrudState?.create?.toggle}
|
||||
data={{
|
||||
parent_id: issueCrudState?.create?.parentIssueId,
|
||||
}}
|
||||
onClose={() => handleIssueCrudState("create", null, null)}
|
||||
onClose={() => {
|
||||
handleIssueCrudState("create", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
}}
|
||||
onSubmit={async (_issue: TIssue) => {
|
||||
await subIssueOperations.addSubIssue(workspaceSlug, projectId, parentIssueId, [_issue.id]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && (
|
||||
{issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && isSubIssuesModalOpen && (
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
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 }}
|
||||
handleOnSubmit={(_issue) =>
|
||||
subIssueOperations.addSubIssue(
|
||||
|
@ -44,20 +44,24 @@ export interface IIssueDetail
|
||||
IIssueCommentReactionStoreActions {
|
||||
// observables
|
||||
peekIssue: TPeekIssue | undefined;
|
||||
isCreateIssueModalOpen: boolean;
|
||||
isIssueLinkModalOpen: boolean;
|
||||
isParentIssueModalOpen: boolean;
|
||||
isDeleteIssueModalOpen: boolean;
|
||||
isArchiveIssueModalOpen: boolean;
|
||||
isRelationModalOpen: TIssueRelationTypes | null;
|
||||
isSubIssuesModalOpen: boolean;
|
||||
// computed
|
||||
isAnyModalOpen: boolean;
|
||||
// actions
|
||||
setPeekIssue: (peekIssue: TPeekIssue | undefined) => void;
|
||||
toggleCreateIssueModal: (value: boolean) => void;
|
||||
toggleIssueLinkModal: (value: boolean) => void;
|
||||
toggleParentIssueModal: (value: boolean) => void;
|
||||
toggleDeleteIssueModal: (value: boolean) => void;
|
||||
toggleArchiveIssueModal: (value: boolean) => void;
|
||||
toggleRelationModal: (value: TIssueRelationTypes | null) => void;
|
||||
toggleSubIssuesModal: (value: boolean) => void;
|
||||
// store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
issue: IIssueStore;
|
||||
@ -75,11 +79,13 @@ export interface IIssueDetail
|
||||
export class IssueDetail implements IIssueDetail {
|
||||
// observables
|
||||
peekIssue: TPeekIssue | undefined = undefined;
|
||||
isCreateIssueModalOpen: boolean = false;
|
||||
isIssueLinkModalOpen: boolean = false;
|
||||
isParentIssueModalOpen: boolean = false;
|
||||
isDeleteIssueModalOpen: boolean = false;
|
||||
isArchiveIssueModalOpen: boolean = false;
|
||||
isRelationModalOpen: TIssueRelationTypes | null = null;
|
||||
isSubIssuesModalOpen: boolean = false;
|
||||
// store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
issue: IIssueStore;
|
||||
@ -97,20 +103,24 @@ export class IssueDetail implements IIssueDetail {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
peekIssue: observable,
|
||||
isCreateIssueModalOpen: observable,
|
||||
isIssueLinkModalOpen: observable.ref,
|
||||
isParentIssueModalOpen: observable.ref,
|
||||
isDeleteIssueModalOpen: observable.ref,
|
||||
isArchiveIssueModalOpen: observable.ref,
|
||||
isRelationModalOpen: observable.ref,
|
||||
isSubIssuesModalOpen: observable.ref,
|
||||
// computed
|
||||
isAnyModalOpen: computed,
|
||||
// action
|
||||
setPeekIssue: action,
|
||||
toggleCreateIssueModal: action,
|
||||
toggleIssueLinkModal: action,
|
||||
toggleParentIssueModal: action,
|
||||
toggleDeleteIssueModal: action,
|
||||
toggleArchiveIssueModal: action,
|
||||
toggleRelationModal: action,
|
||||
toggleSubIssuesModal: action,
|
||||
});
|
||||
|
||||
// store
|
||||
@ -130,21 +140,25 @@ export class IssueDetail implements IIssueDetail {
|
||||
// computed
|
||||
get isAnyModalOpen() {
|
||||
return (
|
||||
this.isCreateIssueModalOpen ||
|
||||
this.isIssueLinkModalOpen ||
|
||||
this.isParentIssueModalOpen ||
|
||||
this.isDeleteIssueModalOpen ||
|
||||
this.isArchiveIssueModalOpen ||
|
||||
Boolean(this.isRelationModalOpen)
|
||||
Boolean(this.isRelationModalOpen) ||
|
||||
this.isSubIssuesModalOpen
|
||||
);
|
||||
}
|
||||
|
||||
// actions
|
||||
setPeekIssue = (peekIssue: TPeekIssue | undefined) => (this.peekIssue = peekIssue);
|
||||
toggleCreateIssueModal = (value: boolean) => (this.isCreateIssueModalOpen = value);
|
||||
toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value);
|
||||
toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value);
|
||||
toggleDeleteIssueModal = (value: boolean) => (this.isDeleteIssueModalOpen = value);
|
||||
toggleArchiveIssueModal = (value: boolean) => (this.isArchiveIssueModalOpen = value);
|
||||
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
|
||||
toggleSubIssuesModal = (value: boolean) => (this.isSubIssuesModalOpen = value);
|
||||
|
||||
// issue
|
||||
fetchIssue = async (
|
||||
|
Loading…
Reference in New Issue
Block a user