[WEB-1925] dev: issue detail widget enhancement (#5101)

* chore: collapsible button border color updated

* chore: TIssueDetailWidget type added

* chore: issue link modal onClose updated

* chore: issue detail widgets collapse state added to store

* chore: issue detail widget interaction added

* chore: issue detail widget interaction added
This commit is contained in:
Anmol Singh Bhatia
2024-07-11 14:34:56 +05:30
committed by GitHub
parent 4d484577b5
commit 15b0a448ee
15 changed files with 135 additions and 46 deletions

View File

@@ -84,7 +84,7 @@ export type TIssuesResponse = {
total_pages: number;
extra_stats: null;
results: TIssueResponseResults;
}
};
export type TBulkIssueProperties = Pick<
TIssue,
@@ -100,3 +100,9 @@ export type TBulkOperationsPayload = {
issue_ids: string[];
properties: Partial<TBulkIssueProperties>;
};
export type TIssueDetailWidget =
| "sub-issues"
| "relations"
| "links"
| "attachments";

View File

@@ -13,7 +13,7 @@ type Props = {
export const CollapsibleButton: FC<Props> = (props) => {
const { isOpen, title, hideChevron = false, indicatorElement, actionItemElement } = props;
return (
<div className="flex items-center justify-between gap-3 h-12 px-2.5 py-3 border-b border-custom-border-100">
<div className="flex items-center justify-between gap-3 h-12 px-2.5 py-3 border-b border-custom-border-200">
<div className="flex items-center gap-3.5">
<div className="flex items-center gap-3">
{!hideChevron && (

View File

@@ -8,7 +8,7 @@ import { MAX_FILE_SIZE } from "@/constants/common";
// helper
import { generateFileName } from "@/helpers/attachment.helper";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance, useIssueDetail } from "@/hooks/store";
import { useAttachmentOperations } from "./helper";
@@ -22,11 +22,16 @@ type Props = {
export const IssueAttachmentActionButton: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, customButton, disabled = false } = props;
// helper
// state
const [isLoading, setIsLoading] = useState(false);
// store hooks
const { config } = useInstance();
const { setLastWidgetAction } = useIssueDetail();
// operations
const handleAttachmentOperations = useAttachmentOperations(workspaceSlug, projectId, issueId);
// handlers
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const currentFile: File = acceptedFiles[0];
@@ -45,7 +50,10 @@ export const IssueAttachmentActionButton: FC<Props> = observer((props) => {
})
);
setIsLoading(true);
handleAttachmentOperations.create(formData).finally(() => setIsLoading(false));
handleAttachmentOperations.create(formData).finally(() => {
setLastWidgetAction("attachments");
setIsLoading(false);
});
},
[handleAttachmentOperations, workspaceSlug]
);

View File

@@ -1,11 +1,14 @@
"use client";
import React, { FC, useState } from "react";
import React, { FC } from "react";
import { observer } from "mobx-react";
import { Collapsible } from "@plane/ui";
// components
import {
IssueAttachmentsCollapsibleContent,
IssueAttachmentsCollapsibleTitle,
} from "@/components/issues/issue-detail-widgets";
// hooks
import { useIssueDetail } from "@/hooks/store";
type Props = {
workspaceSlug: string;
@@ -14,17 +17,21 @@ type Props = {
disabled?: boolean;
};
export const AttachmentsCollapsible: FC<Props> = (props) => {
export const AttachmentsCollapsible: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props;
// state
const [isOpen, setIsOpen] = useState<boolean>(false);
// store hooks
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
// derived values
const isCollapsibleOpen = activeIssueDetailWidgets.includes("attachments");
return (
<Collapsible
isOpen={isOpen}
onToggle={() => setIsOpen((prev) => !prev)}
isOpen={isCollapsibleOpen}
onToggle={() => toggleActiveIssueDetailWidget("attachments")}
title={
<IssueAttachmentsCollapsibleTitle
isOpen={isOpen}
isOpen={isCollapsibleOpen}
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
@@ -40,4 +47,4 @@ export const AttachmentsCollapsible: FC<Props> = (props) => {
/>
</Collapsible>
);
};
});

View File

@@ -23,7 +23,7 @@ export const IssueLinksActionButton: FC<Props> = observer((props) => {
const [isIssueLinkModal, setIsIssueLinkModal] = useState(false);
// store hooks
const { toggleIssueLinkModal: toggleIssueLinkModalStore } = useIssueDetail();
const { toggleIssueLinkModal: toggleIssueLinkModalStore, setLastWidgetAction } = useIssueDetail();
// helper
const handleLinkOperations = useLinkOperations(workspaceSlug, projectId, issueId);
@@ -43,11 +43,16 @@ export const IssueLinksActionButton: FC<Props> = observer((props) => {
toggleIssueLinkModal(true);
};
const handleOnClose = () => {
toggleIssueLinkModal(false);
setLastWidgetAction("links");
};
return (
<>
<IssueLinkCreateUpdateModal
isModalOpen={isIssueLinkModal}
handleModal={toggleIssueLinkModal}
handleOnClose={handleOnClose}
linkOperations={handleLinkOperations}
/>
<button type="button" onClick={handleOnClick} disabled={disabled}>

View File

@@ -1,8 +1,11 @@
"use client";
import React, { FC, useState } from "react";
import React, { FC } from "react";
import { observer } from "mobx-react";
import { Collapsible } from "@plane/ui";
// components
import { IssueLinksCollapsibleContent, IssueLinksCollapsibleTitle } from "@/components/issues/issue-detail-widgets";
// hooks
import { useIssueDetail } from "@/hooks/store";
type Props = {
workspaceSlug: string;
@@ -11,17 +14,21 @@ type Props = {
disabled?: boolean;
};
export const LinksCollapsible: FC<Props> = (props) => {
export const LinksCollapsible: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props;
// state
const [isOpen, setIsOpen] = useState<boolean>(false);
// store hooks
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
// derived values
const isCollapsibleOpen = activeIssueDetailWidgets.includes("links");
return (
<Collapsible
isOpen={isOpen}
onToggle={() => setIsOpen((prev) => !prev)}
isOpen={isCollapsibleOpen}
onToggle={() => toggleActiveIssueDetailWidget("links")}
title={
<IssueLinksCollapsibleTitle
isOpen={isOpen}
isOpen={isCollapsibleOpen}
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
@@ -37,4 +44,4 @@ export const LinksCollapsible: FC<Props> = (props) => {
/>
</Collapsible>
);
};
});

View File

@@ -23,7 +23,8 @@ export const RelationActionButton: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, customButton, issueId, disabled = false } = props;
// state
const [relationKey, setRelationKey] = useState<TIssueRelationTypes | null>(null);
const { createRelation, isRelationModalOpen, toggleRelationModal } = useIssueDetail();
// store hooks
const { createRelation, isRelationModalOpen, toggleRelationModal, setLastWidgetAction } = useIssueDetail();
// handlers
const handleOnClick = (relationKey: TIssueRelationTypes) => {
@@ -57,6 +58,7 @@ export const RelationActionButton: FC<Props> = observer((props) => {
const handleOnClose = () => {
setRelationKey(null);
toggleRelationModal(null, null);
setLastWidgetAction("relations");
};
// button element

View File

@@ -1,8 +1,11 @@
"use client";
import React, { FC, useState } from "react";
import React, { FC } from "react";
import { observer } from "mobx-react";
import { Collapsible } from "@plane/ui";
// components
import { RelationsCollapsibleContent, RelationsCollapsibleTitle } from "@/components/issues/issue-detail-widgets";
// hooks
import { useIssueDetail } from "@/hooks/store";
type Props = {
workspaceSlug: string;
@@ -11,17 +14,21 @@ type Props = {
disabled?: boolean;
};
export const RelationsCollapsible: FC<Props> = (props) => {
export const RelationsCollapsible: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props;
// state
const [isOpen, setIsOpen] = useState<boolean>(false);
// store hooks
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
// derived values
const isCollapsibleOpen = activeIssueDetailWidgets.includes("relations");
return (
<Collapsible
isOpen={isOpen}
onToggle={() => setIsOpen((prev) => !prev)}
isOpen={isCollapsibleOpen}
onToggle={() => toggleActiveIssueDetailWidget("relations")}
title={
<RelationsCollapsibleTitle
isOpen={isOpen}
isOpen={isCollapsibleOpen}
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
@@ -37,4 +44,4 @@ export const RelationsCollapsible: FC<Props> = (props) => {
/>
</Collapsible>
);
};
});

View File

@@ -47,6 +47,7 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
toggleCreateIssueModal,
isSubIssuesModalOpen,
toggleSubIssuesModal,
setLastWidgetAction,
} = useIssueDetail();
const { setTrackElement } = useEventTracker();
@@ -88,6 +89,7 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
const handleExistingIssuesModalClose = () => {
handleIssueCrudState("existing", null, null);
setLastWidgetAction("sub-issues");
toggleSubIssuesModal(null);
};
@@ -102,6 +104,7 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
const handleCreateUpdateModalClose = () => {
handleIssueCrudState("create", null, null);
toggleCreateIssueModal(false);
setLastWidgetAction("sub-issues");
};
const handleCreateUpdateModalOnSubmit = async (_issue: TIssue) => {

View File

@@ -1,8 +1,11 @@
"use client";
import React, { FC, useState } from "react";
import React, { FC } from "react";
import { observer } from "mobx-react";
import { Collapsible } from "@plane/ui";
// components
import { SubIssuesCollapsibleContent, SubIssuesCollapsibleTitle } from "@/components/issues/issue-detail-widgets";
// hooks
import { useIssueDetail } from "@/hooks/store";
type Props = {
workspaceSlug: string;
@@ -11,17 +14,22 @@ type Props = {
disabled?: boolean;
};
export const SubIssuesCollapsible: FC<Props> = (props) => {
export const SubIssuesCollapsible: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props;
// state
const [isOpen, setIsOpen] = useState<boolean>(false);
// store hooks
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
// derived state
const isCollapsibleOpen = activeIssueDetailWidgets.includes("sub-issues");
return (
<Collapsible
isOpen={isOpen}
onToggle={() => setIsOpen((prev) => !prev)}
isOpen={isCollapsibleOpen}
onToggle={() => toggleActiveIssueDetailWidget("sub-issues")}
title={
<SubIssuesCollapsibleTitle
isOpen={isOpen}
isOpen={isCollapsibleOpen}
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssueId={issueId}
@@ -37,4 +45,4 @@ export const SubIssuesCollapsible: FC<Props> = (props) => {
/>
</Collapsible>
);
};
});

View File

@@ -17,7 +17,7 @@ export type TIssueLinkCreateFormFieldOptions = TIssueLinkEditableFields & {
export type TIssueLinkCreateEditModal = {
isModalOpen: boolean;
handleModal: (modalToggle: boolean) => void;
handleOnClose?: () => void;
linkOperations: TLinkOperationsModal;
preloadedData?: TIssueLinkCreateFormFieldOptions | null;
};
@@ -29,7 +29,7 @@ const defaultValues: TIssueLinkCreateFormFieldOptions = {
export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props) => {
// props
const { isModalOpen, handleModal, linkOperations, preloadedData } = props;
const { isModalOpen, handleOnClose, linkOperations, preloadedData } = props;
// react hook form
const {
@@ -42,7 +42,7 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
});
const onClose = () => {
handleModal(false);
if (handleOnClose) handleOnClose();
const timeout = setTimeout(() => {
reset(preloadedData ? preloadedData : defaultValues);
clearTimeout(timeout);

View File

@@ -42,11 +42,15 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
const createdByDetails = getUserDetails(linkDetail.created_by_id);
const handleOnClose = () => {
toggleIssueLinkModal(false);
};
return (
<div key={linkId}>
<IssueLinkCreateUpdateModal
isModalOpen={isIssueLinkModalOpen}
handleModal={toggleIssueLinkModal}
handleOnClose={handleOnClose}
linkOperations={linkOperations}
preloadedData={linkDetail}
/>

View File

@@ -37,11 +37,14 @@ export const IssueLinkItem: FC<TIssueLinkItem> = (props) => {
const linkDetail = getLinkById(linkId);
if (!linkDetail) return <></>;
const handleOnClose = () => {
toggleIssueLinkModal(false);
};
return (
<>
<IssueLinkCreateUpdateModal
isModalOpen={isIssueLinkModalOpen}
handleModal={toggleIssueLinkModal}
handleOnClose={handleOnClose}
linkOperations={linkOperations}
preloadedData={linkDetail}
/>

View File

@@ -100,11 +100,15 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
[workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, toggleIssueLinkModal]
);
const handleOnClose = () => {
toggleIssueLinkModal(false);
};
return (
<>
<IssueLinkCreateUpdateModal
isModalOpen={isIssueLinkModal}
handleModal={toggleIssueLinkModal}
handleOnClose={handleOnClose}
linkOperations={handleLinkOperations}
/>

View File

@@ -8,6 +8,7 @@ import {
TIssueLink,
TIssueReaction,
TIssueRelationTypes,
TIssueDetailWidget,
} from "@plane/types";
import { IIssueRootStore } from "../root.store";
import { IIssueActivityStore, IssueActivityStore, IIssueActivityStoreActions, TActivityLoader } from "./activity.store";
@@ -50,6 +51,8 @@ export interface IIssueDetail
IIssueCommentReactionStoreActions {
// observables
peekIssue: TPeekIssue | undefined;
activeIssueDetailWidgets: TIssueDetailWidget[];
lastWidgetAction: TIssueDetailWidget | null;
isCreateIssueModalOpen: boolean;
isIssueLinkModalOpen: boolean;
isParentIssueModalOpen: string | null;
@@ -72,6 +75,9 @@ export interface IIssueDetail
toggleRelationModal: (issueId: string | null, relationType: TIssueRelationTypes | null) => void;
toggleSubIssuesModal: (value: string | null) => void;
toggleDeleteAttachmentModal: (attachmentId: string | null) => void;
setActiveIssueDetailWidgets: (state: TIssueDetailWidget[]) => void;
setLastWidgetAction: (action: TIssueDetailWidget) => void;
toggleActiveIssueDetailWidget: (state: TIssueDetailWidget) => void;
// store
rootIssueStore: IIssueRootStore;
issue: IIssueStore;
@@ -89,6 +95,8 @@ export interface IIssueDetail
export class IssueDetail implements IIssueDetail {
// observables
peekIssue: TPeekIssue | undefined = undefined;
activeIssueDetailWidgets: TIssueDetailWidget[] = ["sub-issues"];
lastWidgetAction: TIssueDetailWidget | null = null;
isCreateIssueModalOpen: boolean = false;
isIssueLinkModalOpen: boolean = false;
isParentIssueModalOpen: string | null = null;
@@ -122,6 +130,8 @@ export class IssueDetail implements IIssueDetail {
isRelationModalOpen: observable.ref,
isSubIssuesModalOpen: observable.ref,
attachmentDeleteModalId: observable.ref,
activeIssueDetailWidgets: observable.ref,
lastWidgetAction: observable.ref,
// computed
isAnyModalOpen: computed,
// action
@@ -134,6 +144,9 @@ export class IssueDetail implements IIssueDetail {
toggleRelationModal: action,
toggleSubIssuesModal: action,
toggleDeleteAttachmentModal: action,
setActiveIssueDetailWidgets: action,
setLastWidgetAction: action,
toggleActiveIssueDetailWidget: action,
});
// store
@@ -178,6 +191,18 @@ export class IssueDetail implements IIssueDetail {
(this.isRelationModalOpen = { issueId, relationType });
toggleSubIssuesModal = (issueId: string | null) => (this.isSubIssuesModalOpen = issueId);
toggleDeleteAttachmentModal = (attachmentId: string | null) => (this.attachmentDeleteModalId = attachmentId);
setActiveIssueDetailWidgets = (state: TIssueDetailWidget[]) => {
this.activeIssueDetailWidgets = state;
if (this.lastWidgetAction) this.lastWidgetAction = null;
};
setLastWidgetAction = (action: TIssueDetailWidget) => {
this.activeIssueDetailWidgets = [action];
};
toggleActiveIssueDetailWidget = (state: TIssueDetailWidget) => {
if (this.activeIssueDetailWidgets && this.activeIssueDetailWidgets.includes(state))
this.activeIssueDetailWidgets = this.activeIssueDetailWidgets.filter((s) => s !== state);
else this.activeIssueDetailWidgets = [state, ...this.activeIssueDetailWidgets];
};
// issue
fetchIssue = async (