diff --git a/web/components/issues/attachment/attachments-list.tsx b/web/components/issues/attachment/attachments-list.tsx index 0f834c1a49..aed1f7922a 100644 --- a/web/components/issues/attachment/attachments-list.tsx +++ b/web/components/issues/attachment/attachments-list.tsx @@ -21,23 +21,21 @@ export const IssueAttachmentsList: FC = observer((props) const { attachment: { getAttachmentsByIssueId }, } = useIssueDetail(); - + // derived values const issueAttachments = getAttachmentsByIssueId(issueId); if (!issueAttachments) return <>; return ( <> - {issueAttachments && - issueAttachments.length > 0 && - issueAttachments.map((attachmentId) => ( - - ))} + {issueAttachments?.map((attachmentId) => ( + + ))} ); }); diff --git a/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx index 096f9e778f..85f0ea0e97 100644 --- a/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx +++ b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx @@ -69,7 +69,7 @@ export const IssueAttachmentDeleteModal: FC = (props) => {
- Delete Attachment + Delete attachment

@@ -94,7 +94,7 @@ export const IssueAttachmentDeleteModal: FC = (props) => { }} disabled={loader} > - {loader ? "Deleting..." : "Delete"} + {loader ? "Deleting" : "Delete"}

diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx index 3cf6b162a9..715e9f8404 100644 --- a/web/components/issues/attachment/root.tsx +++ b/web/components/issues/attachment/root.tsx @@ -101,7 +101,7 @@ export const IssueAttachmentRoot: FC = (props) => { return (

Attachments

-
+
= observer((pr handleRestoreIssue, isSubmitting, } = props; - // router - const router = useRouter(); // store hooks const { currentUser } = useUser(); const { @@ -101,10 +99,6 @@ export const IssuePeekOverviewHeader: FC = observer((pr }); }); }; - const redirectToIssueDetail = () => { - router.push({ pathname: `/${issueLink}` }); - removeRoutePeekId(); - }; // auth const isArchivingAllowed = !isArchived && !disabled; const isInArchivableGroup = @@ -122,9 +116,9 @@ export const IssuePeekOverviewHeader: FC = observer((pr - + {currentMode && (
= (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 ( +
+
Attachments
+
+ + +
+
+ ); +}; diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 59b1c16090..2abcec2ff1 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -55,7 +55,7 @@ export const PeekOverviewIssueDetails: FC = observer( : undefined; return ( - <> +
{projectDetails?.identifier}-{issue?.sequence_id} @@ -89,6 +89,6 @@ export const PeekOverviewIssueDetails: FC = observer( currentUser={currentUser} /> )} - +
); }); diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 8ae021b867..ba00755f41 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -61,7 +61,7 @@ export const PeekOverviewProperties: FC = observer((pro maxDate?.setDate(maxDate.getDate()); return ( -
+
Properties
{/* TODO: render properties using a common component */}
diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index 47890c95c2..4109f3feb7 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -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 = observer((props) => { // ref const issuePeekOverviewRef = useRef(null); // store hooks + const { currentUser } = useUser(); const { setPeekIssue, isAnyModalOpen, @@ -147,7 +150,7 @@ export const IssueView: FC = observer((props) => { issue && ( <> {["side-peek", "modal"].includes(peekMode) ? ( -
+
= observer((props) => { setIsSubmitting={(value) => setIsSubmitting(value)} /> + {currentUser && ( + + )} + + + = observer((props) => {
) : ( -
+
-
+
= observer((props) => { setIsSubmitting={(value) => setIsSubmitting(value)} /> + {currentUser && ( + + )} + + +
diff --git a/web/components/issues/sub-issues/issue-list-item.tsx b/web/components/issues/sub-issues/issue-list-item.tsx index 170bf622f9..781a91a3de 100644 --- a/web/components/issues/sub-issues/issue-list-item.tsx +++ b/web/components/issues/sub-issues/issue-list-item.tsx @@ -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 = observer((props) => { setSubIssueHelpers(parentIssueId, "issue_visibility", issueId); }} > - {subIssueHelpers.issue_visibility.includes(issue.id) ? ( - - ) : ( - - )} +
)} diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index ed46a40f55..c3655286e8 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -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 = observer((props) => { updateSubIssue, removeSubIssue, deleteSubIssue, + isCreateIssueModalOpen, + toggleCreateIssueModal, + isSubIssuesModalOpen, + toggleSubIssuesModal, } = useIssueDetail(); const { setTrackElement, captureIssueEvent } = useEventTracker(); // state @@ -310,55 +314,81 @@ export const SubIssuesRoot: FC = observer((props) => { <> {subIssues && subIssues?.length > 0 ? ( <> -
-
-
- {subIssueHelpers.preview_loader.includes(parentIssueId) ? ( - - ) : subIssueHelpers.issue_visibility.includes(parentIssueId) ? ( - - ) : ( - - )} +
+
+ +
+ + + {subIssuesDistribution?.completed?.length ?? 0}/{subIssues.length} Done +
-
Sub-issues
-
({subIssues?.length || 0})
-
- -
-
{!disabled && ( -
-
+ + Add sub-issue + + } + buttonClassName="whitespace-nowrap" + placement="bottom-end" + noBorder + noChevron + > + { - setTrackElement("Issue detail add sub-issue"); + setTrackElement("Issue detail nested sub-issue"); handleIssueCrudState("create", parentIssueId, null); + toggleCreateIssueModal(true); }} > - Add sub-issue -
-
+ + Create new +
+ + { - setTrackElement("Issue detail add sub-issue"); + setTrackElement("Issue detail nested sub-issue"); handleIssueCrudState("existing", parentIssueId, null); + toggleSubIssuesModal(true); }} > - Add an existing issue -
-
+
+ + Add existing +
+ + )}
@@ -379,62 +409,74 @@ export const SubIssuesRoot: FC = observer((props) => { ) : ( !disabled && (
-
No Sub-Issues yet
-
- - - Add sub-issue - - } - buttonClassName="whitespace-nowrap" - placement="bottom-end" - noBorder - noChevron +
No sub-issues yet
+ + + Add sub-issue + + } + buttonClassName="whitespace-nowrap" + placement="bottom-end" + noBorder + noChevron + > + { + setTrackElement("Issue detail nested sub-issue"); + handleIssueCrudState("create", parentIssueId, null); + toggleCreateIssueModal(true); + }} > - { - setTrackElement("Issue detail nested sub-issue"); - handleIssueCrudState("create", parentIssueId, null); - }} - > - Create new - - { - setTrackElement("Issue detail nested sub-issue"); - handleIssueCrudState("existing", parentIssueId, null); - }} - > - Add an existing issue - - -
+
+ + Create new +
+ + { + setTrackElement("Issue detail nested sub-issue"); + handleIssueCrudState("existing", parentIssueId, null); + toggleSubIssuesModal(true); + }} + > +
+ + Add existing +
+
+
) )} {/* issue create, add from existing , update and delete modals */} - {issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && ( + {issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && isCreateIssueModalOpen && ( 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 && ( handleIssueCrudState("existing", null, null)} + handleClose={() => { + handleIssueCrudState("existing", null, null); + toggleSubIssuesModal(false); + }} searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }} handleOnSubmit={(_issue) => subIssueOperations.addSubIssue( diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index be77efcd1e..c58e1af426 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -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 (