mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
1 Commits
tests-gith
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c376aaf0a |
17
packages/types/src/issues/issue_sub_issues.d.ts
vendored
17
packages/types/src/issues/issue_sub_issues.d.ts
vendored
@@ -20,3 +20,20 @@ export type TIssueSubIssuesStateDistributionMap = {
|
||||
export type TIssueSubIssuesIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
|
||||
export type TSubIssueOperations = {
|
||||
copyLink: (path: string) => void;
|
||||
fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise<void>;
|
||||
addSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => Promise<void>;
|
||||
updateSubIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue?: Partial<TIssue>,
|
||||
fromModal?: boolean
|
||||
) => Promise<void>;
|
||||
removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ export const IssueDetailWidgetModals: FC<Props> = observer((props) => {
|
||||
} = useIssueDetail();
|
||||
|
||||
// helper hooks
|
||||
const subIssueOperations = useSubIssueOperations();
|
||||
const subIssueOperations = useSubIssueOperations(issueServiceType);
|
||||
const handleLinkOperations = useLinkOperations(workspaceSlug, projectId, issueId);
|
||||
|
||||
// handlers
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
"use client";
|
||||
import { useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueServiceType, ISSUE_DELETED, ISSUE_UPDATED } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
// helper
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
// hooks
|
||||
import { useEventTracker, useIssueDetail } from "@/hooks/store";
|
||||
|
||||
export type TRelationIssueOperations = {
|
||||
copyText: (text: string) => void;
|
||||
copyLink: (path: string) => void;
|
||||
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
};
|
||||
@@ -29,9 +28,8 @@ export const useRelationOperations = (
|
||||
|
||||
const issueOperations: TRelationIssueOperations = useMemo(
|
||||
() => ({
|
||||
copyText: (text: string) => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}${text}`).then(() => {
|
||||
copyLink: (path) => {
|
||||
copyUrlToClipboard(path).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("common.link_copied"),
|
||||
@@ -39,7 +37,7 @@ export const useRelationOperations = (
|
||||
});
|
||||
});
|
||||
},
|
||||
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||
update: async (workspaceSlug, projectId, issueId, data) => {
|
||||
try {
|
||||
await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
captureIssueEvent({
|
||||
@@ -56,7 +54,7 @@ export const useRelationOperations = (
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: t("entity.update.success", { entity: entityName }),
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { state: "FAILED", element: "Issue detail page" },
|
||||
@@ -73,7 +71,7 @@ export const useRelationOperations = (
|
||||
});
|
||||
}
|
||||
},
|
||||
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
remove: async (workspaceSlug, projectId, issueId) => {
|
||||
try {
|
||||
return removeIssue(workspaceSlug, projectId, issueId).then(() => {
|
||||
captureIssueEvent({
|
||||
@@ -82,7 +80,7 @@ export const useRelationOperations = (
|
||||
path: pathname,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_DELETED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||
@@ -91,7 +89,7 @@ export const useRelationOperations = (
|
||||
}
|
||||
},
|
||||
}),
|
||||
[pathname, removeIssue, updateIssue]
|
||||
[captureIssueEvent, entityName, pathname, removeIssue, t, updateIssue]
|
||||
);
|
||||
|
||||
return issueOperations;
|
||||
|
||||
@@ -6,11 +6,11 @@ import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
// components
|
||||
import { DeleteIssueModal } from "@/components/issues/delete-issue-modal";
|
||||
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal";
|
||||
import { IssueList } from "@/components/issues/sub-issues/issues-list";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// helper
|
||||
// local imports
|
||||
import { useSubIssueOperations } from "./helper";
|
||||
import { SubIssuesListRoot } from "./issues-list/root";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@@ -53,8 +53,9 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
|
||||
},
|
||||
});
|
||||
// store hooks
|
||||
const { toggleCreateIssueModal, toggleDeleteIssueModal } = useIssueDetail();
|
||||
const {
|
||||
toggleCreateIssueModal,
|
||||
toggleDeleteIssueModal,
|
||||
subIssues: { subIssueHelpersByIssueId, setSubIssueHelpers },
|
||||
} = useIssueDetail(issueServiceType);
|
||||
|
||||
@@ -63,20 +64,19 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
|
||||
const subIssueHelpers = subIssueHelpersByIssueId(`${parentIssueId}_root`);
|
||||
|
||||
// handler
|
||||
const handleIssueCrudState = (
|
||||
key: "create" | "existing" | "update" | "delete",
|
||||
_parentIssueId: string | null,
|
||||
issue: TIssue | null = null
|
||||
) => {
|
||||
setIssueCrudState({
|
||||
...issueCrudState,
|
||||
[key]: {
|
||||
toggle: !issueCrudState[key].toggle,
|
||||
parentIssueId: _parentIssueId,
|
||||
issue: issue,
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleIssueCrudState = useCallback(
|
||||
(key: "create" | "existing" | "update" | "delete", _parentIssueId: string | null, issue: TIssue | null = null) => {
|
||||
setIssueCrudState({
|
||||
...issueCrudState,
|
||||
[key]: {
|
||||
toggle: !issueCrudState[key].toggle,
|
||||
parentIssueId: _parentIssueId,
|
||||
issue,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueCrudState]
|
||||
);
|
||||
|
||||
const handleFetchSubIssues = useCallback(async () => {
|
||||
if (!subIssueHelpers.issue_visibility.includes(parentIssueId)) {
|
||||
@@ -116,7 +116,7 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
|
||||
return (
|
||||
<>
|
||||
{subIssueHelpers.issue_visibility.includes(parentIssueId) && (
|
||||
<IssueList
|
||||
<SubIssuesListRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={parentIssueId}
|
||||
|
||||
@@ -1,46 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
import { TIssueServiceType, TSubIssueOperations } from "@plane/types";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// helper
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
// hooks
|
||||
import { useEventTracker, useIssueDetail, useProjectState } from "@/hooks/store";
|
||||
// plane-web
|
||||
// plane web helpers
|
||||
import { updateEpicAnalytics } from "@/plane-web/helpers/epic-analytics";
|
||||
// type
|
||||
import { TSubIssueOperations } from "../../sub-issues";
|
||||
|
||||
export type TRelationIssueOperations = {
|
||||
copyText: (text: string) => void;
|
||||
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useSubIssueOperations = (
|
||||
issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES
|
||||
): TSubIssueOperations => {
|
||||
export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSubIssueOperations => {
|
||||
// router
|
||||
const { epicId: epicIdParam } = useParams();
|
||||
const pathname = usePathname();
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
subIssues: { setSubIssueHelpers },
|
||||
fetchSubIssues,
|
||||
createSubIssues,
|
||||
updateSubIssue,
|
||||
deleteSubIssue,
|
||||
} = useIssueDetail();
|
||||
removeSubIssue,
|
||||
} = useIssueDetail(issueServiceType);
|
||||
const { getStateById } = useProjectState();
|
||||
const { peekIssue: epicPeekIssue } = useIssueDetail(EIssueServiceType.EPICS);
|
||||
// const { updateEpicAnalytics } = useIssueTypes();
|
||||
const { updateAnalytics } = updateEpicAnalytics();
|
||||
const { fetchSubIssues } = useIssueDetail();
|
||||
const { removeSubIssue } = useIssueDetail(issueServiceType);
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
|
||||
// derived values
|
||||
@@ -48,9 +40,8 @@ export const useSubIssueOperations = (
|
||||
|
||||
const subIssueOperations: TSubIssueOperations = useMemo(
|
||||
() => ({
|
||||
copyText: (text: string) => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${text}`).then(() => {
|
||||
copyLink: (path) => {
|
||||
copyUrlToClipboard(`/${path}`).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("common.link_copied"),
|
||||
@@ -63,10 +54,10 @@ export const useSubIssueOperations = (
|
||||
});
|
||||
});
|
||||
},
|
||||
fetchSubIssues: async (workspaceSlug: string, projectId: string, parentIssueId: string) => {
|
||||
fetchSubIssues: async (workspaceSlug, projectId, parentIssueId) => {
|
||||
try {
|
||||
await fetchSubIssues(workspaceSlug, projectId, parentIssueId);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("toast.error"),
|
||||
@@ -79,7 +70,7 @@ export const useSubIssueOperations = (
|
||||
});
|
||||
}
|
||||
},
|
||||
addSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => {
|
||||
addSubIssue: async (workspaceSlug, projectId, parentIssueId, issueIds) => {
|
||||
try {
|
||||
await createSubIssues(workspaceSlug, projectId, parentIssueId, issueIds);
|
||||
setToast({
|
||||
@@ -92,11 +83,10 @@ export const useSubIssueOperations = (
|
||||
: t("issue.label", { count: 2 }),
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("toast.error"),
|
||||
// message: `Error adding ${issueServiceType === EIssueServiceType.ISSUES ? "sub-issues" : "issues"}`,
|
||||
message: t("entity.add.failed", {
|
||||
entity:
|
||||
issueServiceType === EIssueServiceType.ISSUES
|
||||
@@ -107,13 +97,13 @@ export const useSubIssueOperations = (
|
||||
}
|
||||
},
|
||||
updateSubIssue: async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue: Partial<TIssue> = {},
|
||||
fromModal: boolean = false
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
parentIssueId,
|
||||
issueId,
|
||||
issueData,
|
||||
oldIssue = {},
|
||||
fromModal = false
|
||||
) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
@@ -157,7 +147,7 @@ export const useSubIssueOperations = (
|
||||
message: t("sub_work_item.update.success"),
|
||||
});
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue updated",
|
||||
payload: { ...oldIssue, ...issueData, state: "FAILED", element: "Issue detail page" },
|
||||
@@ -174,7 +164,7 @@ export const useSubIssueOperations = (
|
||||
});
|
||||
}
|
||||
},
|
||||
removeSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
|
||||
removeSubIssue: async (workspaceSlug, projectId, parentIssueId, issueId) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
@@ -203,7 +193,7 @@ export const useSubIssueOperations = (
|
||||
path: pathname,
|
||||
});
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue removed",
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||
@@ -220,7 +210,7 @@ export const useSubIssueOperations = (
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
|
||||
deleteSubIssue: async (workspaceSlug, projectId, parentIssueId, issueId) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
return deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId).then(() => {
|
||||
@@ -231,7 +221,7 @@ export const useSubIssueOperations = (
|
||||
});
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue removed",
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||
@@ -245,7 +235,22 @@ export const useSubIssueOperations = (
|
||||
}
|
||||
},
|
||||
}),
|
||||
[fetchSubIssues, createSubIssues, updateSubIssue, removeSubIssue, deleteSubIssue, setSubIssueHelpers]
|
||||
[
|
||||
captureIssueEvent,
|
||||
createSubIssues,
|
||||
deleteSubIssue,
|
||||
epicId,
|
||||
fetchSubIssues,
|
||||
getIssueById,
|
||||
getStateById,
|
||||
issueServiceType,
|
||||
pathname,
|
||||
removeSubIssue,
|
||||
setSubIssueHelpers,
|
||||
t,
|
||||
updateAnalytics,
|
||||
updateSubIssue,
|
||||
]
|
||||
);
|
||||
|
||||
return subIssueOperations;
|
||||
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
// ui
|
||||
import { TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types";
|
||||
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
@@ -17,15 +16,11 @@ import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-red
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// local components
|
||||
import { IssueList } from "./issues-list";
|
||||
import { IssueProperty } from "./properties";
|
||||
// ui
|
||||
// types
|
||||
import { TSubIssueOperations } from "./root";
|
||||
// import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
||||
// local imports
|
||||
import { SubIssuesListItemProperties } from "./properties";
|
||||
import { SubIssuesListRoot } from "./root";
|
||||
|
||||
export interface ISubIssues {
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
parentIssueId: string;
|
||||
@@ -40,9 +35,9 @@ export interface ISubIssues {
|
||||
subIssueOperations: TSubIssueOperations;
|
||||
issueId: string;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
}
|
||||
};
|
||||
|
||||
export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
||||
export const SubIssuesListItem: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
@@ -171,7 +166,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<IssueProperty
|
||||
<SubIssuesListItemProperties
|
||||
workspaceSlug={workspaceSlug}
|
||||
parentIssueId={parentIssueId}
|
||||
issueId={issueId}
|
||||
@@ -203,7 +198,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
subIssueOperations.copyText(workItemLink);
|
||||
subIssueOperations.copyLink(workItemLink);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -256,7 +251,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
||||
issue.project_id &&
|
||||
subIssueCount > 0 &&
|
||||
!isCurrentIssueRoot && (
|
||||
<IssueList
|
||||
<SubIssuesListRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
parentIssueId={issue.id}
|
||||
@@ -1,22 +1,20 @@
|
||||
import React from "react";
|
||||
import { TIssueServiceType } from "@plane/types";
|
||||
// hooks
|
||||
import { PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns";
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// plane imports
|
||||
import { TIssueServiceType, TSubIssueOperations } from "@plane/types";
|
||||
// components
|
||||
// types
|
||||
import { TSubIssueOperations } from "./root";
|
||||
import { PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
|
||||
export interface IIssueProperty {
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
parentIssueId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
subIssueOperations: TSubIssueOperations;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
}
|
||||
};
|
||||
|
||||
export const IssueProperty: React.FC<IIssueProperty> = (props) => {
|
||||
export const SubIssuesListItemProperties: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug, parentIssueId, issueId, disabled, subIssueOperations, issueServiceType } = props;
|
||||
// hooks
|
||||
const {
|
||||
@@ -0,0 +1,64 @@
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// local imports
|
||||
import { SubIssuesListItem } from "./list-item";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
parentIssueId: string;
|
||||
rootIssueId: string;
|
||||
spacingLeft: number;
|
||||
disabled: boolean;
|
||||
handleIssueCrudState: (
|
||||
key: "create" | "existing" | "update" | "delete",
|
||||
issueId: string,
|
||||
issue?: TIssue | null
|
||||
) => void;
|
||||
subIssueOperations: TSubIssueOperations;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
};
|
||||
|
||||
export const SubIssuesListRoot: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
parentIssueId,
|
||||
rootIssueId,
|
||||
spacingLeft = 10,
|
||||
disabled,
|
||||
handleIssueCrudState,
|
||||
subIssueOperations,
|
||||
issueServiceType = EIssueServiceType.ISSUES,
|
||||
} = props;
|
||||
// store hooks
|
||||
const {
|
||||
subIssues: { subIssuesByIssueId },
|
||||
} = useIssueDetail(issueServiceType);
|
||||
// derived values
|
||||
const subIssueIds = subIssuesByIssueId(parentIssueId);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{subIssueIds?.map((issueId) => (
|
||||
<SubIssuesListItem
|
||||
key={issueId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={parentIssueId}
|
||||
rootIssueId={rootIssueId}
|
||||
issueId={issueId}
|
||||
spacingLeft={spacingLeft}
|
||||
disabled={disabled}
|
||||
handleIssueCrudState={handleIssueCrudState}
|
||||
subIssueOperations={subIssueOperations}
|
||||
issueServiceType={issueServiceType}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -21,7 +21,7 @@ export const SubIssuesCollapsible: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false, issueServiceType } = props;
|
||||
// store hooks
|
||||
const { openWidgets, toggleOpenWidget } = useIssueDetail(issueServiceType);
|
||||
// derived state
|
||||
// derived values
|
||||
const isCollapsibleOpen = openWidgets.includes("sub-issues");
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssueServiceType } from "@plane/types";
|
||||
@@ -19,13 +21,13 @@ type Props = {
|
||||
|
||||
export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
|
||||
const { isOpen, parentIssueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const {
|
||||
subIssues: { subIssuesByIssueId, stateDistributionByIssueId },
|
||||
} = useIssueDetail(issueServiceType);
|
||||
|
||||
// derived data
|
||||
// derived values
|
||||
const subIssuesDistribution = stateDistributionByIssueId(parentIssueId);
|
||||
const subIssues = subIssuesByIssueId(parentIssueId);
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
const handleCopyIssueLink = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
issueOperations.copyText(workItemLink);
|
||||
issueOperations.copyLink(workItemLink);
|
||||
};
|
||||
|
||||
const handleRemoveRelation = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./root";
|
||||
@@ -1,69 +0,0 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// components
|
||||
import { IssueListItem } from "./issue-list-item";
|
||||
// types
|
||||
import { TSubIssueOperations } from "./root";
|
||||
|
||||
export interface IIssueList {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
parentIssueId: string;
|
||||
rootIssueId: string;
|
||||
spacingLeft: number;
|
||||
disabled: boolean;
|
||||
handleIssueCrudState: (
|
||||
key: "create" | "existing" | "update" | "delete",
|
||||
issueId: string,
|
||||
issue?: TIssue | null
|
||||
) => void;
|
||||
subIssueOperations: TSubIssueOperations;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
}
|
||||
|
||||
export const IssueList: FC<IIssueList> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
parentIssueId,
|
||||
rootIssueId,
|
||||
spacingLeft = 10,
|
||||
disabled,
|
||||
handleIssueCrudState,
|
||||
subIssueOperations,
|
||||
issueServiceType = EIssueServiceType.ISSUES,
|
||||
} = props;
|
||||
// hooks
|
||||
const {
|
||||
subIssues: { subIssuesByIssueId },
|
||||
} = useIssueDetail(issueServiceType);
|
||||
|
||||
const subIssueIds = subIssuesByIssueId(parentIssueId);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{subIssueIds &&
|
||||
subIssueIds.length > 0 &&
|
||||
subIssueIds.map((issueId) => (
|
||||
<Fragment key={issueId}>
|
||||
<IssueListItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={parentIssueId}
|
||||
rootIssueId={rootIssueId}
|
||||
issueId={issueId}
|
||||
spacingLeft={spacingLeft}
|
||||
disabled={disabled}
|
||||
handleIssueCrudState={handleIssueCrudState}
|
||||
subIssueOperations={subIssueOperations}
|
||||
issueServiceType={issueServiceType}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
export interface IProgressBar {
|
||||
total: number;
|
||||
done: number;
|
||||
}
|
||||
|
||||
export const ProgressBar = ({ total = 0, done = 0 }: IProgressBar) => {
|
||||
const calPercentage = (doneValue: number, totalValue: number): string => {
|
||||
if (doneValue === 0 || totalValue === 0) return (0).toFixed(0);
|
||||
return ((100 * doneValue) / totalValue).toFixed(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="w-full">
|
||||
<div className="w-full overflow-hidden rounded-full bg-custom-background-80 shadow">
|
||||
<div
|
||||
className="h-[6px] rounded-full bg-green-500 transition-all"
|
||||
style={{ width: `${calPercentage(done, total)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-xs font-medium">{calPercentage(done, total)}% Done</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,546 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// icons
|
||||
import { Plus, ChevronRight, Loader, Pencil } from "lucide-react";
|
||||
// types
|
||||
import { IUser, TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { CircularProgressIndicator, CustomMenu, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "@/components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useIssueDetail } from "@/hooks/store";
|
||||
// local components
|
||||
import useURLHash from "@/hooks/use-url-hash";
|
||||
import { IssueList } from "./issues-list";
|
||||
|
||||
export interface ISubIssuesRoot {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
parentIssueId: string;
|
||||
currentUser: IUser;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export type TSubIssueOperations = {
|
||||
copyText: (text: string) => void;
|
||||
fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise<void>;
|
||||
addSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => Promise<void>;
|
||||
updateSubIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue?: Partial<TIssue>,
|
||||
fromModal?: boolean
|
||||
) => Promise<void>;
|
||||
removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, parentIssueId, disabled = false } = props;
|
||||
// router
|
||||
const pathname = usePathname();
|
||||
const hashValue = useURLHash();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
subIssues: { subIssuesByIssueId, stateDistributionByIssueId, subIssueHelpersByIssueId, setSubIssueHelpers },
|
||||
fetchSubIssues,
|
||||
createSubIssues,
|
||||
updateSubIssue,
|
||||
removeSubIssue,
|
||||
deleteSubIssue,
|
||||
isCreateIssueModalOpen,
|
||||
toggleCreateIssueModal,
|
||||
isSubIssuesModalOpen,
|
||||
toggleSubIssuesModal,
|
||||
toggleDeleteIssueModal,
|
||||
} = useIssueDetail();
|
||||
const { setTrackElement, captureIssueEvent } = useEventTracker();
|
||||
// state
|
||||
|
||||
type TIssueCrudState = { toggle: boolean; parentIssueId: string | undefined; issue: TIssue | undefined };
|
||||
const [issueCrudState, setIssueCrudState] = useState<{
|
||||
create: TIssueCrudState;
|
||||
existing: TIssueCrudState;
|
||||
update: TIssueCrudState;
|
||||
delete: TIssueCrudState;
|
||||
}>({
|
||||
create: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
existing: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
update: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
delete: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const scrollToSubIssuesView = useCallback(() => {
|
||||
if (hashValue === "sub-issues") {
|
||||
setTimeout(() => {
|
||||
const subIssueDiv = document.getElementById(`sub-issues`);
|
||||
if (subIssueDiv)
|
||||
subIssueDiv.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}, [hashValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hashValue) {
|
||||
scrollToSubIssuesView();
|
||||
}
|
||||
}, [hashValue, scrollToSubIssuesView]);
|
||||
|
||||
const handleIssueCrudState = (
|
||||
key: "create" | "existing" | "update" | "delete",
|
||||
_parentIssueId: string | null,
|
||||
issue: TIssue | null = null
|
||||
) => {
|
||||
setIssueCrudState({
|
||||
...issueCrudState,
|
||||
[key]: {
|
||||
toggle: !issueCrudState[key].toggle,
|
||||
parentIssueId: _parentIssueId,
|
||||
issue: issue,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const subIssueOperations: TSubIssueOperations = useMemo(
|
||||
() => ({
|
||||
copyText: (text: string) => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}${text}`).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link Copied!",
|
||||
message: "Work item link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
},
|
||||
fetchSubIssues: async (workspaceSlug: string, projectId: string, parentIssueId: string) => {
|
||||
try {
|
||||
await fetchSubIssues(workspaceSlug, projectId, parentIssueId);
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error fetching sub-work items",
|
||||
});
|
||||
}
|
||||
},
|
||||
addSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => {
|
||||
try {
|
||||
await createSubIssues(workspaceSlug, projectId, parentIssueId, issueIds);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Sub-work items added successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error adding sub-work item",
|
||||
});
|
||||
}
|
||||
},
|
||||
updateSubIssue: async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue: Partial<TIssue> = {},
|
||||
fromModal: boolean = false
|
||||
) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
await updateSubIssue(workspaceSlug, projectId, parentIssueId, issueId, issueData, oldIssue, fromModal);
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue updated",
|
||||
payload: { ...oldIssue, ...issueData, state: "SUCCESS", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: Object.keys(issueData).join(","),
|
||||
change_details: Object.values(issueData).join(","),
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Sub-work item updated successfully",
|
||||
});
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
} catch (error) {
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue updated",
|
||||
payload: { ...oldIssue, ...issueData, state: "FAILED", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: Object.keys(issueData).join(","),
|
||||
change_details: Object.values(issueData).join(","),
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error updating sub-work item",
|
||||
});
|
||||
}
|
||||
},
|
||||
removeSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Sub-work item removed successfully",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue removed",
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: "parent_id",
|
||||
change_details: parentIssueId,
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
} catch (error) {
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue removed",
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: "parent_id",
|
||||
change_details: parentIssueId,
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error removing sub-work item",
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
await deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue deleted",
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||
path: pathname,
|
||||
});
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
} catch (error) {
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue removed",
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||
path: pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error deleting work item",
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
[fetchSubIssues, createSubIssues, updateSubIssue, removeSubIssue, deleteSubIssue, setSubIssueHelpers]
|
||||
);
|
||||
|
||||
const issue = getIssueById(parentIssueId);
|
||||
const subIssuesDistribution = stateDistributionByIssueId(parentIssueId);
|
||||
const subIssues = subIssuesByIssueId(parentIssueId);
|
||||
const subIssueHelpers = subIssueHelpersByIssueId(`${parentIssueId}_root`);
|
||||
|
||||
const handleFetchSubIssues = useCallback(async () => {
|
||||
if (!subIssueHelpers.issue_visibility.includes(parentIssueId)) {
|
||||
setSubIssueHelpers(`${parentIssueId}_root`, "preview_loader", parentIssueId);
|
||||
await subIssueOperations.fetchSubIssues(workspaceSlug, projectId, parentIssueId);
|
||||
setSubIssueHelpers(`${parentIssueId}_root`, "preview_loader", parentIssueId);
|
||||
}
|
||||
setSubIssueHelpers(`${parentIssueId}_root`, "issue_visibility", parentIssueId);
|
||||
}, [
|
||||
parentIssueId,
|
||||
projectId,
|
||||
setSubIssueHelpers,
|
||||
subIssueHelpers.issue_visibility,
|
||||
subIssueOperations,
|
||||
workspaceSlug,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
handleFetchSubIssues();
|
||||
|
||||
return () => {
|
||||
handleFetchSubIssues();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [parentIssueId]);
|
||||
|
||||
if (!issue) return <></>;
|
||||
return (
|
||||
<div id="sub-issues" className="h-full w-full space-y-2">
|
||||
{!subIssues ? (
|
||||
<div className="py-3 text-center text-sm font-medium text-custom-text-300">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
{subIssues && subIssues?.length > 0 ? (
|
||||
<>
|
||||
<div className="relative flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded py-1 px-2 transition-all hover:bg-custom-background-80 font-medium"
|
||||
onClick={handleFetchSubIssues}
|
||||
>
|
||||
<div className="flex flex-shrink-0 items-center justify-center">
|
||||
{subIssueHelpers.preview_loader.includes(parentIssueId) ? (
|
||||
<Loader strokeWidth={2} className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<ChevronRight
|
||||
className={cn("h-3 w-3 transition-all", {
|
||||
"rotate-90": subIssueHelpers.issue_visibility.includes(parentIssueId),
|
||||
})}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>Sub-work items</div>
|
||||
</button>
|
||||
<div className="flex items-center gap-2 text-custom-text-300">
|
||||
<CircularProgressIndicator
|
||||
size={16}
|
||||
percentage={
|
||||
subIssuesDistribution?.completed?.length && subIssues.length
|
||||
? (subIssuesDistribution?.completed?.length / subIssues.length) * 100
|
||||
: 0
|
||||
}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<span>
|
||||
{subIssuesDistribution?.completed?.length ?? 0}/{subIssues.length} Done
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!disabled && (
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add sub-work item
|
||||
</>
|
||||
}
|
||||
buttonClassName="whitespace-nowrap"
|
||||
placement="bottom-end"
|
||||
noBorder
|
||||
noChevron
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("create", parentIssueId, null);
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Create new</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
toggleSubIssuesModal(issue.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayersIcon className="h-3 w-3" />
|
||||
<span>Add existing</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{subIssueHelpers.issue_visibility.includes(parentIssueId) && (
|
||||
<IssueList
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={parentIssueId}
|
||||
rootIssueId={parentIssueId}
|
||||
spacingLeft={10}
|
||||
disabled={!disabled}
|
||||
handleIssueCrudState={handleIssueCrudState}
|
||||
subIssueOperations={subIssueOperations}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
!disabled && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs italic text-custom-text-300">No sub-work items yet</div>
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add sub-work item
|
||||
</>
|
||||
}
|
||||
buttonClassName="whitespace-nowrap"
|
||||
placement="bottom-end"
|
||||
noBorder
|
||||
noChevron
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("create", parentIssueId, null);
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Create new</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
toggleSubIssuesModal(issue.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayersIcon className="h-3 w-3" />
|
||||
<span>Add existing</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* issue create, add from existing , update and delete modals */}
|
||||
{issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && isCreateIssueModalOpen && (
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={issueCrudState?.create?.toggle}
|
||||
data={{
|
||||
parent_id: issueCrudState?.create?.parentIssueId,
|
||||
}}
|
||||
onClose={() => {
|
||||
handleIssueCrudState("create", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
}}
|
||||
onSubmit={async (_issue: TIssue) => {
|
||||
if (_issue.parent_id) {
|
||||
await subIssueOperations.addSubIssue(workspaceSlug, projectId, _issue.parent_id, [_issue.id]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && isSubIssuesModalOpen && (
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isOpen={issueCrudState?.existing?.toggle}
|
||||
handleClose={() => {
|
||||
handleIssueCrudState("existing", null, null);
|
||||
toggleSubIssuesModal(null);
|
||||
}}
|
||||
searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }}
|
||||
handleOnSubmit={(_issue) =>
|
||||
subIssueOperations.addSubIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
parentIssueId,
|
||||
_issue.map((issue) => issue.id)
|
||||
)
|
||||
}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
)}
|
||||
|
||||
{issueCrudState?.update?.toggle && issueCrudState?.update?.issue && (
|
||||
<>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={issueCrudState?.update?.toggle}
|
||||
onClose={() => {
|
||||
handleIssueCrudState("update", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
}}
|
||||
data={issueCrudState?.update?.issue ?? undefined}
|
||||
onSubmit={async (_issue: TIssue) => {
|
||||
await subIssueOperations.updateSubIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
parentIssueId,
|
||||
_issue.id,
|
||||
_issue,
|
||||
issueCrudState?.update?.issue,
|
||||
true
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{issueCrudState?.delete?.toggle &&
|
||||
issueCrudState?.delete?.issue &&
|
||||
issueCrudState.delete.parentIssueId &&
|
||||
issueCrudState.delete.issue.id && (
|
||||
<DeleteIssueModal
|
||||
isOpen={issueCrudState?.delete?.toggle}
|
||||
handleClose={() => {
|
||||
handleIssueCrudState("delete", null, null);
|
||||
toggleDeleteIssueModal(null);
|
||||
}}
|
||||
data={issueCrudState?.delete?.issue as TIssue}
|
||||
onSubmit={async () =>
|
||||
await subIssueOperations.deleteSubIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueCrudState?.delete?.parentIssueId as string,
|
||||
issueCrudState?.delete?.issue?.id as string
|
||||
)
|
||||
}
|
||||
isSubIssue
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user