[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:
Anmol Singh Bhatia 2024-07-11 20:12:09 +05:30 committed by GitHub
parent 24973c1386
commit dd3b0f6a3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 322 additions and 337 deletions

View file

@ -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

View file

@ -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"

View file

@ -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}

View file

@ -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";

View file

@ -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
/>
</>
);
});

View file

@ -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>
);
});

View file

@ -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}

View file

@ -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} />}
/>
);
});

View file

@ -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>
);
});

View file

@ -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}

View file

@ -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} />}
/>
);
});

View file

@ -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} />
</>
);
};

View file

@ -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>
);
});

View file

@ -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}

View file

@ -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} />}
/>
);
});

View file

@ -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