[WEB-1921] fix: issue widgets modal and code refactor (#5106)
* fix: celery fix * chore: issue relationkey and issueCrudOperation state added to issueDetail store * chore: moved issue detail widget modal to root * chore: code refactor * chore: default open widget updated
This commit is contained in:
parent
24973c1386
commit
dd3b0f6a3f
16 changed files with 322 additions and 337 deletions
|
|
@ -276,8 +276,6 @@ CELERY_IMPORTS = (
|
|||
"plane.bgtasks.api_logs_task",
|
||||
# management tasks
|
||||
"plane.bgtasks.dummy_data_task",
|
||||
# backfill tasks
|
||||
"plane.db.backfills.backfill_0070_page_versions",
|
||||
)
|
||||
|
||||
# Sentry Settings
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ export const IssueDetailWidgetActionButtons: FC<Props> = (props) => {
|
|||
return (
|
||||
<div className="flex items-center flex-wrap gap-2">
|
||||
<SubIssuesActionButton
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
customButton={
|
||||
|
|
@ -34,8 +32,6 @@ export const IssueDetailWidgetActionButtons: FC<Props> = (props) => {
|
|||
}
|
||||
/>
|
||||
<RelationActionButton
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
customButton={
|
||||
<IssueDetailWidgetButton
|
||||
|
|
@ -45,9 +41,6 @@ export const IssueDetailWidgetActionButtons: FC<Props> = (props) => {
|
|||
}
|
||||
/>
|
||||
<IssueLinksActionButton
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
customButton={
|
||||
<IssueDetailWidgetButton
|
||||
title="Add Links"
|
||||
|
|
|
|||
|
|
@ -20,15 +20,15 @@ type Props = {
|
|||
export const AttachmentsCollapsible: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
// store hooks
|
||||
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
|
||||
const { openWidgets, toggleOpenWidget } = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const isCollapsibleOpen = activeIssueDetailWidgets.includes("attachments");
|
||||
const isCollapsibleOpen = openWidgets.includes("attachments");
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
isOpen={isCollapsibleOpen}
|
||||
onToggle={() => toggleActiveIssueDetailWidget("attachments")}
|
||||
onToggle={() => toggleOpenWidget("attachments")}
|
||||
title={
|
||||
<IssueAttachmentsCollapsibleTitle
|
||||
isOpen={isCollapsibleOpen}
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@ export * from "./sub-issues";
|
|||
export * from "./widget-button";
|
||||
export * from "./issue-detail-widget-collapsibles";
|
||||
export * from "./action-buttons";
|
||||
export * from "./issue-detail-widget-modals";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "@/components/core";
|
||||
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
|
||||
import { IssueLinkCreateUpdateModal } from "../issue-detail/links/create-update-link-modal";
|
||||
// helpers
|
||||
import { useLinkOperations } from "./links/helper";
|
||||
import { useSubIssueOperations } from "./sub-issues/helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const IssueDetailWidgetModals: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId } = props;
|
||||
// store hooks
|
||||
const {
|
||||
isIssueLinkModalOpen,
|
||||
toggleIssueLinkModal: toggleIssueLinkModalStore,
|
||||
isCreateIssueModalOpen,
|
||||
toggleCreateIssueModal,
|
||||
isSubIssuesModalOpen,
|
||||
toggleSubIssuesModal,
|
||||
relationKey,
|
||||
isRelationModalOpen,
|
||||
setRelationKey,
|
||||
setLastWidgetAction,
|
||||
toggleRelationModal,
|
||||
createRelation,
|
||||
issueCrudOperationState,
|
||||
setIssueCrudOperationState,
|
||||
} = useIssueDetail();
|
||||
|
||||
// helper hooks
|
||||
const subIssueOperations = useSubIssueOperations();
|
||||
const handleLinkOperations = useLinkOperations(workspaceSlug, projectId, issueId);
|
||||
|
||||
// handlers
|
||||
const handleIssueCrudState = (
|
||||
key: "create" | "existing",
|
||||
_parentIssueId: string | null,
|
||||
issue: TIssue | null = null
|
||||
) => {
|
||||
setIssueCrudOperationState({
|
||||
...issueCrudOperationState,
|
||||
[key]: {
|
||||
toggle: !issueCrudOperationState[key].toggle,
|
||||
parentIssueId: _parentIssueId,
|
||||
issue: issue,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleExistingIssuesModalClose = () => {
|
||||
handleIssueCrudState("existing", null, null);
|
||||
setLastWidgetAction("sub-issues");
|
||||
toggleSubIssuesModal(null);
|
||||
};
|
||||
|
||||
const handleExistingIssuesModalOnSubmit = async (_issue: ISearchIssueResponse[]) =>
|
||||
subIssueOperations.addSubIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
_issue.map((issue) => issue.id)
|
||||
);
|
||||
|
||||
const handleCreateUpdateModalClose = () => {
|
||||
handleIssueCrudState("create", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
setLastWidgetAction("sub-issues");
|
||||
};
|
||||
|
||||
const handleCreateUpdateModalOnSubmit = async (_issue: TIssue) => {
|
||||
if (_issue.parent_id) {
|
||||
await subIssueOperations.addSubIssue(workspaceSlug, projectId, issueId, [_issue.id]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIssueLinkModalOnClose = () => {
|
||||
toggleIssueLinkModalStore(false);
|
||||
setLastWidgetAction("links");
|
||||
};
|
||||
|
||||
const handleRelationOnClose = () => {
|
||||
setRelationKey(null);
|
||||
toggleRelationModal(null, null);
|
||||
setLastWidgetAction("relations");
|
||||
};
|
||||
|
||||
const handleExistingIssueModalOnSubmit = async (data: ISearchIssueResponse[]) => {
|
||||
if (!relationKey) return;
|
||||
if (data.length === 0) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Please select at least one issue.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await createRelation(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
relationKey,
|
||||
data.map((i) => i.id)
|
||||
);
|
||||
|
||||
toggleRelationModal(null, null);
|
||||
};
|
||||
|
||||
// helpers
|
||||
const createUpdateModalData = { parent_id: issueCrudOperationState?.create?.parentIssueId };
|
||||
|
||||
const existingIssuesModalSearchParams = {
|
||||
sub_issue: true,
|
||||
issue_id: issueCrudOperationState?.existing?.parentIssueId,
|
||||
};
|
||||
|
||||
// render conditions
|
||||
const shouldRenderExistingIssuesModal =
|
||||
issueCrudOperationState?.existing?.toggle &&
|
||||
issueCrudOperationState?.existing?.parentIssueId &&
|
||||
isSubIssuesModalOpen;
|
||||
|
||||
const shouldRenderCreateUpdateModal =
|
||||
issueCrudOperationState?.create?.toggle && issueCrudOperationState?.create?.parentIssueId && isCreateIssueModalOpen;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssueLinkCreateUpdateModal
|
||||
isModalOpen={isIssueLinkModalOpen}
|
||||
handleOnClose={handleIssueLinkModalOnClose}
|
||||
linkOperations={handleLinkOperations}
|
||||
/>
|
||||
|
||||
{shouldRenderCreateUpdateModal && (
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={issueCrudOperationState?.create?.toggle}
|
||||
data={createUpdateModalData}
|
||||
onClose={handleCreateUpdateModalClose}
|
||||
onSubmit={handleCreateUpdateModalOnSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldRenderExistingIssuesModal && (
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isOpen={issueCrudOperationState?.existing?.toggle}
|
||||
handleClose={handleExistingIssuesModalClose}
|
||||
searchParams={existingIssuesModalSearchParams}
|
||||
handleOnSubmit={handleExistingIssuesModalOnSubmit}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isOpen={isRelationModalOpen?.issueId === issueId && isRelationModalOpen?.relationType === relationKey}
|
||||
handleClose={handleRelationOnClose}
|
||||
searchParams={{ issue_relation: true, issue_id: issueId }}
|
||||
handleOnSubmit={handleExistingIssueModalOnSubmit}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,63 +1,30 @@
|
|||
"use client";
|
||||
import React, { FC, useCallback, useState } from "react";
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Plus } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// components
|
||||
import { IssueLinkCreateUpdateModal } from "../../issue-detail/links/create-update-link-modal";
|
||||
// helper
|
||||
import { useLinkOperations } from "./helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
customButton?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueLinksActionButton: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, customButton, disabled = false } = props;
|
||||
// state
|
||||
const [isIssueLinkModal, setIsIssueLinkModal] = useState(false);
|
||||
|
||||
const { customButton, disabled = false } = props;
|
||||
// store hooks
|
||||
const { toggleIssueLinkModal: toggleIssueLinkModalStore, setLastWidgetAction } = useIssueDetail();
|
||||
|
||||
// helper
|
||||
const handleLinkOperations = useLinkOperations(workspaceSlug, projectId, issueId);
|
||||
|
||||
// handler
|
||||
const toggleIssueLinkModal = useCallback(
|
||||
(modalToggle: boolean) => {
|
||||
toggleIssueLinkModalStore(modalToggle);
|
||||
setIsIssueLinkModal(modalToggle);
|
||||
},
|
||||
[toggleIssueLinkModalStore]
|
||||
);
|
||||
const { toggleIssueLinkModal } = useIssueDetail();
|
||||
|
||||
// handlers
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleIssueLinkModal(true);
|
||||
};
|
||||
|
||||
const handleOnClose = () => {
|
||||
toggleIssueLinkModal(false);
|
||||
setLastWidgetAction("links");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssueLinkCreateUpdateModal
|
||||
isModalOpen={isIssueLinkModal}
|
||||
handleOnClose={handleOnClose}
|
||||
linkOperations={handleLinkOperations}
|
||||
/>
|
||||
<button type="button" onClick={handleOnClick} disabled={disabled}>
|
||||
{customButton ? customButton : <Plus className="h-4 w-4" />}
|
||||
</button>
|
||||
</>
|
||||
<button type="button" onClick={handleOnClick} disabled={disabled}>
|
||||
{customButton ? customButton : <Plus className="h-4 w-4" />}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,24 +17,16 @@ type Props = {
|
|||
export const LinksCollapsible: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
// store hooks
|
||||
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
|
||||
const { openWidgets, toggleOpenWidget } = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const isCollapsibleOpen = activeIssueDetailWidgets.includes("links");
|
||||
const isCollapsibleOpen = openWidgets.includes("links");
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
isOpen={isCollapsibleOpen}
|
||||
onToggle={() => toggleActiveIssueDetailWidget("links")}
|
||||
title={
|
||||
<IssueLinksCollapsibleTitle
|
||||
isOpen={isCollapsibleOpen}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
onToggle={() => toggleOpenWidget("links")}
|
||||
title={<IssueLinksCollapsibleTitle isOpen={isCollapsibleOpen} issueId={issueId} disabled={disabled} />}
|
||||
>
|
||||
<IssueLinksCollapsibleContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,12 @@ import { useIssueDetail } from "@/hooks/store";
|
|||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const IssueLinksCollapsibleTitle: FC<Props> = observer((props) => {
|
||||
const { isOpen, workspaceSlug, projectId, issueId, disabled } = props;
|
||||
const { isOpen, issueId, disabled } = props;
|
||||
// store hooks
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
|
|
@ -42,14 +40,7 @@ export const IssueLinksCollapsibleTitle: FC<Props> = observer((props) => {
|
|||
isOpen={isOpen}
|
||||
title="Links"
|
||||
indicatorElement={indicatorElement}
|
||||
actionItemElement={
|
||||
<IssueLinksActionButton
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
actionItemElement={<IssueLinksActionButton disabled={disabled} />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,30 +1,24 @@
|
|||
"use client";
|
||||
import React, { FC, useState } from "react";
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { ISearchIssueResponse, TIssueRelationTypes } from "@plane/types";
|
||||
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "@/components/core";
|
||||
import { TIssueRelationTypes } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// helper
|
||||
import { ISSUE_RELATION_OPTIONS } from "./helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
customButton?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const RelationActionButton: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, customButton, issueId, disabled = false } = props;
|
||||
// state
|
||||
const [relationKey, setRelationKey] = useState<TIssueRelationTypes | null>(null);
|
||||
const { customButton, issueId, disabled = false } = props;
|
||||
// store hooks
|
||||
const { createRelation, isRelationModalOpen, toggleRelationModal, setLastWidgetAction } = useIssueDetail();
|
||||
const { toggleRelationModal, setRelationKey } = useIssueDetail();
|
||||
|
||||
// handlers
|
||||
const handleOnClick = (relationKey: TIssueRelationTypes) => {
|
||||
|
|
@ -32,67 +26,26 @@ export const RelationActionButton: FC<Props> = observer((props) => {
|
|||
toggleRelationModal(issueId, relationKey);
|
||||
};
|
||||
|
||||
// submit handler
|
||||
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||
if (!relationKey) return;
|
||||
if (data.length === 0) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Please select at least one issue.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await createRelation(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
relationKey,
|
||||
data.map((i) => i.id)
|
||||
);
|
||||
|
||||
toggleRelationModal(null, null);
|
||||
};
|
||||
|
||||
const handleOnClose = () => {
|
||||
setRelationKey(null);
|
||||
toggleRelationModal(null, null);
|
||||
setLastWidgetAction("relations");
|
||||
};
|
||||
|
||||
// button element
|
||||
const customButtonElement = customButton ? <>{customButton}</> : <Plus className="h-4 w-4" />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomMenu customButton={customButtonElement} placement="bottom-start" disabled={disabled} closeOnSelect>
|
||||
{ISSUE_RELATION_OPTIONS.map((item, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleOnClick(item.key as TIssueRelationTypes);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.icon(12)}
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isOpen={isRelationModalOpen?.issueId === issueId && isRelationModalOpen?.relationType === relationKey}
|
||||
handleClose={handleOnClose}
|
||||
searchParams={{ issue_relation: true, issue_id: issueId }}
|
||||
handleOnSubmit={onSubmit}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
</>
|
||||
<CustomMenu customButton={customButtonElement} placement="bottom-start" disabled={disabled} closeOnSelect>
|
||||
{ISSUE_RELATION_OPTIONS.map((item, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleOnClick(item.key as TIssueRelationTypes);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.icon(12)}
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,24 +17,16 @@ type Props = {
|
|||
export const RelationsCollapsible: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
// store hooks
|
||||
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
|
||||
const { openWidgets, toggleOpenWidget } = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const isCollapsibleOpen = activeIssueDetailWidgets.includes("relations");
|
||||
const isCollapsibleOpen = openWidgets.includes("relations");
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
isOpen={isCollapsibleOpen}
|
||||
onToggle={() => toggleActiveIssueDetailWidget("relations")}
|
||||
title={
|
||||
<RelationsCollapsibleTitle
|
||||
isOpen={isCollapsibleOpen}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
onToggle={() => toggleOpenWidget("relations")}
|
||||
title={<RelationsCollapsibleTitle isOpen={isCollapsibleOpen} issueId={issueId} disabled={disabled} />}
|
||||
>
|
||||
<RelationsCollapsibleContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,12 @@ import { useIssueDetail } from "@/hooks/store";
|
|||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
|
||||
const { isOpen, workspaceSlug, projectId, issueId, disabled } = props;
|
||||
const { isOpen, issueId, disabled } = props;
|
||||
// store hook
|
||||
const {
|
||||
relation: { getRelationsByIssueId },
|
||||
|
|
@ -41,14 +39,7 @@ export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
|
|||
isOpen={isOpen}
|
||||
title="Relations"
|
||||
indicatorElement={indicatorElement}
|
||||
actionItemElement={
|
||||
<RelationActionButton
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
actionItemElement={<RelationActionButton issueId={issueId} disabled={disabled} />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React, { FC } from "react";
|
|||
import {
|
||||
IssueDetailWidgetActionButtons,
|
||||
IssueDetailWidgetCollapsibles,
|
||||
IssueDetailWidgetModals,
|
||||
} from "@/components/issues/issue-detail-widgets";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -16,19 +17,22 @@ type Props = {
|
|||
export const IssueDetailWidgets: FC<Props> = (props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled } = props;
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<IssueDetailWidgetActionButtons
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<IssueDetailWidgetCollapsibles
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="flex flex-col gap-5">
|
||||
<IssueDetailWidgetActionButtons
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<IssueDetailWidgetCollapsibles
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<IssueDetailWidgetModals workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,58 +1,34 @@
|
|||
"use client";
|
||||
import React, { FC, useState } from "react";
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { LayersIcon, Plus } from "lucide-react";
|
||||
import { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "@/components/core";
|
||||
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal";
|
||||
// hooks
|
||||
import { useEventTracker, useIssueDetail } from "@/hooks/store";
|
||||
// helper
|
||||
import { useSubIssueOperations } from "./helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
customButton?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type TIssueCrudState = { toggle: boolean; parentIssueId: string | undefined; issue: TIssue | undefined };
|
||||
|
||||
export const SubIssuesActionButton: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, customButton, disabled = false } = props;
|
||||
// state
|
||||
const [issueCrudState, setIssueCrudState] = useState<{
|
||||
create: TIssueCrudState;
|
||||
existing: TIssueCrudState;
|
||||
}>({
|
||||
create: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
existing: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
});
|
||||
const { issueId, customButton, disabled = false } = props;
|
||||
// store hooks
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
isCreateIssueModalOpen,
|
||||
toggleCreateIssueModal,
|
||||
isSubIssuesModalOpen,
|
||||
toggleSubIssuesModal,
|
||||
setLastWidgetAction,
|
||||
setIssueCrudOperationState,
|
||||
issueCrudOperationState,
|
||||
} = useIssueDetail();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
||||
// helper
|
||||
const subIssueOperations = useSubIssueOperations();
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return <></>;
|
||||
|
||||
// handlers
|
||||
const handleIssueCrudState = (
|
||||
|
|
@ -60,21 +36,16 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
|
|||
_parentIssueId: string | null,
|
||||
issue: TIssue | null = null
|
||||
) => {
|
||||
setIssueCrudState({
|
||||
...issueCrudState,
|
||||
setIssueCrudOperationState({
|
||||
...issueCrudOperationState,
|
||||
[key]: {
|
||||
toggle: !issueCrudState[key].toggle,
|
||||
toggle: !issueCrudOperationState[key].toggle,
|
||||
parentIssueId: _parentIssueId,
|
||||
issue: issue,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return <></>;
|
||||
|
||||
const handleCreateNew = () => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("create", issueId, null);
|
||||
|
|
@ -87,32 +58,6 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
|
|||
toggleSubIssuesModal(issue.id);
|
||||
};
|
||||
|
||||
const handleExistingIssuesModalClose = () => {
|
||||
handleIssueCrudState("existing", null, null);
|
||||
setLastWidgetAction("sub-issues");
|
||||
toggleSubIssuesModal(null);
|
||||
};
|
||||
|
||||
const handleExistingIssuesModalOnSubmit = async (_issue: ISearchIssueResponse[]) =>
|
||||
subIssueOperations.addSubIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
_issue.map((issue) => issue.id)
|
||||
);
|
||||
|
||||
const handleCreateUpdateModalClose = () => {
|
||||
handleIssueCrudState("create", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
setLastWidgetAction("sub-issues");
|
||||
};
|
||||
|
||||
const handleCreateUpdateModalOnSubmit = async (_issue: TIssue) => {
|
||||
if (_issue.parent_id) {
|
||||
await subIssueOperations.addSubIssue(workspaceSlug, projectId, issueId, [_issue.id]);
|
||||
}
|
||||
};
|
||||
|
||||
// options
|
||||
const optionItems = [
|
||||
{
|
||||
|
|
@ -127,59 +72,26 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
|
|||
},
|
||||
];
|
||||
|
||||
// create update modal
|
||||
const shouldRenderCreateUpdateModal =
|
||||
issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && isCreateIssueModalOpen;
|
||||
|
||||
const createUpdateModalData = { parent_id: issueCrudState?.create?.parentIssueId };
|
||||
|
||||
// existing issues modal
|
||||
const shouldRenderExistingIssuesModal =
|
||||
issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && isSubIssuesModalOpen;
|
||||
|
||||
const existingIssuesModalSearchParams = { sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId };
|
||||
|
||||
// button element
|
||||
const customButtonElement = customButton ? <>{customButton}</> : <Plus className="h-4 w-4" />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomMenu customButton={customButtonElement} placement="bottom-start" disabled={disabled} closeOnSelect>
|
||||
{optionItems.map((item, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.onClick();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
|
||||
{shouldRenderCreateUpdateModal && (
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={issueCrudState?.create?.toggle}
|
||||
data={createUpdateModalData}
|
||||
onClose={handleCreateUpdateModalClose}
|
||||
onSubmit={handleCreateUpdateModalOnSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldRenderExistingIssuesModal && (
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isOpen={issueCrudState?.existing?.toggle}
|
||||
handleClose={handleExistingIssuesModalClose}
|
||||
searchParams={existingIssuesModalSearchParams}
|
||||
handleOnSubmit={handleExistingIssuesModalOnSubmit}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<CustomMenu customButton={customButtonElement} placement="bottom-start" disabled={disabled} closeOnSelect>
|
||||
{optionItems.map((item, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.onClick();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,24 +18,16 @@ export const SubIssuesCollapsible: FC<Props> = observer((props) => {
|
|||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
|
||||
// store hooks
|
||||
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
|
||||
const { openWidgets, toggleOpenWidget } = useIssueDetail();
|
||||
|
||||
// derived state
|
||||
const isCollapsibleOpen = activeIssueDetailWidgets.includes("sub-issues");
|
||||
const isCollapsibleOpen = openWidgets.includes("sub-issues");
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
isOpen={isCollapsibleOpen}
|
||||
onToggle={() => toggleActiveIssueDetailWidget("sub-issues")}
|
||||
title={
|
||||
<SubIssuesCollapsibleTitle
|
||||
isOpen={isCollapsibleOpen}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={issueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
onToggle={() => toggleOpenWidget("sub-issues")}
|
||||
title={<SubIssuesCollapsibleTitle isOpen={isCollapsibleOpen} parentIssueId={issueId} disabled={disabled} />}
|
||||
>
|
||||
<SubIssuesCollapsibleContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,12 @@ import { useIssueDetail } from "@/hooks/store";
|
|||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
parentIssueId: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
|
||||
const { isOpen, workspaceSlug, projectId, parentIssueId, disabled } = props;
|
||||
const { isOpen, parentIssueId, disabled } = props;
|
||||
// store hooks
|
||||
const {
|
||||
subIssues: { subIssuesByIssueId, stateDistributionByIssueId },
|
||||
|
|
@ -52,14 +50,7 @@ export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
|
|||
isOpen={isOpen}
|
||||
title="Sub-issues"
|
||||
indicatorElement={indicatorElement}
|
||||
actionItemElement={
|
||||
<SubIssuesActionButton
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={parentIssueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
actionItemElement={<SubIssuesActionButton issueId={parentIssueId} disabled={disabled} />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,6 +38,13 @@ export type TIssueRelationModal = {
|
|||
relationType: TIssueRelationTypes | null;
|
||||
};
|
||||
|
||||
export type TIssueCrudState = { toggle: boolean; parentIssueId: string | undefined; issue: TIssue | undefined };
|
||||
|
||||
export type TIssueCrudOperationState = {
|
||||
create: TIssueCrudState;
|
||||
existing: TIssueCrudState;
|
||||
};
|
||||
|
||||
export interface IIssueDetail
|
||||
extends IIssueStoreActions,
|
||||
IIssueReactionStoreActions,
|
||||
|
|
@ -51,7 +58,9 @@ export interface IIssueDetail
|
|||
IIssueCommentReactionStoreActions {
|
||||
// observables
|
||||
peekIssue: TPeekIssue | undefined;
|
||||
activeIssueDetailWidgets: TIssueDetailWidget[];
|
||||
relationKey: TIssueRelationTypes | null;
|
||||
issueCrudOperationState: TIssueCrudOperationState;
|
||||
openWidgets: TIssueDetailWidget[];
|
||||
lastWidgetAction: TIssueDetailWidget | null;
|
||||
isCreateIssueModalOpen: boolean;
|
||||
isIssueLinkModalOpen: boolean;
|
||||
|
|
@ -75,9 +84,11 @@ 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;
|
||||
setOpenWidgets: (state: TIssueDetailWidget[]) => void;
|
||||
setLastWidgetAction: (action: TIssueDetailWidget) => void;
|
||||
toggleActiveIssueDetailWidget: (state: TIssueDetailWidget) => void;
|
||||
toggleOpenWidget: (state: TIssueDetailWidget) => void;
|
||||
setRelationKey: (relationKey: TIssueRelationTypes | null) => void;
|
||||
setIssueCrudOperationState: (state: TIssueCrudOperationState) => void;
|
||||
// store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
issue: IIssueStore;
|
||||
|
|
@ -95,7 +106,20 @@ export interface IIssueDetail
|
|||
export class IssueDetail implements IIssueDetail {
|
||||
// observables
|
||||
peekIssue: TPeekIssue | undefined = undefined;
|
||||
activeIssueDetailWidgets: TIssueDetailWidget[] = ["sub-issues"];
|
||||
relationKey: TIssueRelationTypes | null = null;
|
||||
issueCrudOperationState: TIssueCrudOperationState = {
|
||||
create: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
existing: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
};
|
||||
openWidgets: TIssueDetailWidget[] = ["sub-issues", "links", "attachments"];
|
||||
lastWidgetAction: TIssueDetailWidget | null = null;
|
||||
isCreateIssueModalOpen: boolean = false;
|
||||
isIssueLinkModalOpen: boolean = false;
|
||||
|
|
@ -122,6 +146,8 @@ export class IssueDetail implements IIssueDetail {
|
|||
makeObservable(this, {
|
||||
// observables
|
||||
peekIssue: observable,
|
||||
relationKey: observable,
|
||||
issueCrudOperationState: observable,
|
||||
isCreateIssueModalOpen: observable,
|
||||
isIssueLinkModalOpen: observable.ref,
|
||||
isParentIssueModalOpen: observable.ref,
|
||||
|
|
@ -130,7 +156,7 @@ export class IssueDetail implements IIssueDetail {
|
|||
isRelationModalOpen: observable.ref,
|
||||
isSubIssuesModalOpen: observable.ref,
|
||||
attachmentDeleteModalId: observable.ref,
|
||||
activeIssueDetailWidgets: observable.ref,
|
||||
openWidgets: observable.ref,
|
||||
lastWidgetAction: observable.ref,
|
||||
// computed
|
||||
isAnyModalOpen: computed,
|
||||
|
|
@ -144,9 +170,11 @@ export class IssueDetail implements IIssueDetail {
|
|||
toggleRelationModal: action,
|
||||
toggleSubIssuesModal: action,
|
||||
toggleDeleteAttachmentModal: action,
|
||||
setActiveIssueDetailWidgets: action,
|
||||
setOpenWidgets: action,
|
||||
setLastWidgetAction: action,
|
||||
toggleActiveIssueDetailWidget: action,
|
||||
toggleOpenWidget: action,
|
||||
setRelationKey: action,
|
||||
setIssueCrudOperationState: action,
|
||||
});
|
||||
|
||||
// store
|
||||
|
|
@ -181,6 +209,8 @@ export class IssueDetail implements IIssueDetail {
|
|||
getIsIssuePeeked = (issueId: string) => this.peekIssue?.issueId === issueId;
|
||||
|
||||
// actions
|
||||
setRelationKey = (relationKey: TIssueRelationTypes | null) => (this.relationKey = relationKey);
|
||||
setIssueCrudOperationState = (state: TIssueCrudOperationState) => (this.issueCrudOperationState = state);
|
||||
setPeekIssue = (peekIssue: TPeekIssue | undefined) => (this.peekIssue = peekIssue);
|
||||
toggleCreateIssueModal = (value: boolean) => (this.isCreateIssueModalOpen = value);
|
||||
toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value);
|
||||
|
|
@ -191,17 +221,17 @@ 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;
|
||||
setOpenWidgets = (state: TIssueDetailWidget[]) => {
|
||||
this.openWidgets = state;
|
||||
if (this.lastWidgetAction) this.lastWidgetAction = null;
|
||||
};
|
||||
setLastWidgetAction = (action: TIssueDetailWidget) => {
|
||||
this.activeIssueDetailWidgets = [action];
|
||||
this.openWidgets = [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];
|
||||
toggleOpenWidget = (state: TIssueDetailWidget) => {
|
||||
if (this.openWidgets && this.openWidgets.includes(state))
|
||||
this.openWidgets = this.openWidgets.filter((s) => s !== state);
|
||||
else this.openWidgets = [state, ...this.openWidgets];
|
||||
};
|
||||
|
||||
// issue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue