mirror of
https://github.com/makeplane/plane.git
synced 2026-02-09 07:38:52 -06:00
[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:
committed by
GitHub
parent
4d484577b5
commit
15b0a448ee
8
packages/types/src/issues/issue.d.ts
vendored
8
packages/types/src/issues/issue.d.ts
vendored
@@ -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";
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user