[WEB-3560] feat: epic to work item and vice versa conversion (#2977)
* feat: conversion of epic to work item and vice versa * chore: delete related cycles and modules * chore: delete related cycles, modules and parent * feat: activitites * fix: None for requested_data and current_instance * chore: convertWorkItem method, services and types added * chore: conversion component added * chore: epic and work item modal updated * chore: modal state and mutation changes * chore: conversion action button peek view implementation * chore: conversion action button detail page implementation * chore: mutation * chore: epic enable check added * chore: toast message updated * chore: code refactor * chore: code refactor * chore: remove prefetch_related and select_related * fix: work item type conversion and additional properties * chore: code refactor * chore: code refactoring * chore: code refactor * chore: feature flag added * chore: icon updated --------- Co-authored-by: Anmol Singh Bhatia <[email protected]>
This commit is contained in:
co-authored by
Anmol Singh Bhatia
parent
51d7e0608c
commit
e55aa6d394
@@ -1741,6 +1741,54 @@ def delete_customer_activity(
|
||||
)
|
||||
|
||||
|
||||
def convert_epic_to_work_item_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="converted the epic to work item",
|
||||
field="epic",
|
||||
verb="converted",
|
||||
actor_id=actor_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def convert_work_item_to_epic_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="converted the work item to epic",
|
||||
field="work_item",
|
||||
verb="converted",
|
||||
actor_id=actor_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Receive message from room group
|
||||
@shared_task
|
||||
def issue_activity(
|
||||
@@ -1811,6 +1859,8 @@ def issue_activity(
|
||||
"issue_draft.activity.deleted": delete_draft_issue_activity,
|
||||
"intake.activity.created": create_intake_activity,
|
||||
"epic.activity.created": create_epic_activity,
|
||||
"work_item.activity.converted": convert_epic_to_work_item_activity,
|
||||
"epic.activity.converted": convert_work_item_to_epic_activity,
|
||||
"customer.activity.created": create_customer_activity,
|
||||
"customer.activity.deleted": delete_customer_activity,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ from plane.ee.views.app import (
|
||||
BulkSubscribeIssuesEndpoint,
|
||||
IssueWorkLogsEndpoint,
|
||||
IssueTotalWorkLogEndpoint,
|
||||
IssueConvertEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -39,4 +40,11 @@ urlpatterns = [
|
||||
IssueTotalWorkLogEndpoint.as_view(),
|
||||
name="issue-work-logs",
|
||||
),
|
||||
# Issue Convertion Endpoint
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/conversion/<uuid:entity_id>/",
|
||||
IssueConvertEndpoint.as_view(),
|
||||
name="issue-conversion",
|
||||
),
|
||||
# End Issue Convertion Endpoint
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ from plane.ee.views.app.issue import (
|
||||
BulkSubscribeIssuesEndpoint,
|
||||
IssueWorkLogsEndpoint,
|
||||
IssueTotalWorkLogEndpoint,
|
||||
IssueConvertEndpoint,
|
||||
)
|
||||
|
||||
from plane.ee.views.app.intake import ProjectInTakePublishViewSet
|
||||
|
||||
@@ -4,3 +4,4 @@ from .bulk_operations import (
|
||||
BulkArchiveIssuesEndpoint,
|
||||
BulkSubscribeIssuesEndpoint,
|
||||
)
|
||||
from .convert import IssueConvertEndpoint
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Module imports
|
||||
from plane.ee.views.base import BaseAPIView
|
||||
from plane.db.models import Issue, IssueType
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.utils import timezone
|
||||
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
|
||||
|
||||
class IssueConvertEndpoint(BaseAPIView):
|
||||
def post(self, request, slug, project_id: str, entity_id: str):
|
||||
conversion_type = request.data.get("conversion_type")
|
||||
|
||||
issue = Issue.objects.get(id=entity_id)
|
||||
|
||||
if conversion_type == "epic":
|
||||
issue_type = IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
is_epic=True,
|
||||
level=1,
|
||||
is_active=True,
|
||||
).first()
|
||||
|
||||
if issue_type is None:
|
||||
return Response(
|
||||
{"error": "Epic is not enabled for this project"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
# Set the issue type id to the issue type id of the epic
|
||||
issue.type_id = issue_type.id
|
||||
|
||||
# Delete the issue cycle and issue module of the issue
|
||||
issue.issue_cycle.all().delete()
|
||||
issue.issue_module.all().delete()
|
||||
|
||||
# Set the parent id to None if it exists
|
||||
if issue.parent_id is not None:
|
||||
issue.parent_id = None
|
||||
|
||||
issue.save()
|
||||
|
||||
# Create the issue activity
|
||||
issue_activity.delay(
|
||||
type="epic.activity.converted",
|
||||
issue_id=issue.id,
|
||||
project_id=issue.project_id,
|
||||
actor_id=request.user.id,
|
||||
requested_data=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
current_instance=None,
|
||||
)
|
||||
|
||||
elif conversion_type == "work_item":
|
||||
issue.type_id = None
|
||||
issue.save()
|
||||
|
||||
# Create the issue activity
|
||||
issue_activity.delay(
|
||||
type="work_item.activity.converted",
|
||||
issue_id=issue.id,
|
||||
project_id=issue.project_id,
|
||||
actor_id=request.user.id,
|
||||
requested_data=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
current_instance=None,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Converted Successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
@@ -8,6 +8,7 @@ export enum E_FEATURE_FLAGS {
|
||||
ESTIMATE_WITH_TIME = "ESTIMATE_WITH_TIME",
|
||||
ISSUE_TYPES = "ISSUE_TYPES",
|
||||
EPICS = "EPICS",
|
||||
WORK_ITEM_CONVERSION = "WORK_ITEM_CONVERSION",
|
||||
OIDC_SAML_AUTH = "OIDC_SAML_AUTH",
|
||||
PAGE_ISSUE_EMBEDS = "PAGE_ISSUE_EMBEDS",
|
||||
PAGE_PUBLISH = "PAGE_PUBLISH",
|
||||
|
||||
@@ -375,3 +375,8 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
},
|
||||
...ADDITIONAL_SPREADSHEET_PROPERTY_DETAILS,
|
||||
};
|
||||
|
||||
export enum EWorkItemConversionType {
|
||||
WORK_ITEM = "work_item",
|
||||
EPIC = "epic",
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
// types
|
||||
import { EIssuePropertyType, EWorkItemTypeEntity } from "@plane/constants";
|
||||
import { EIssuePropertyType, EWorkItemTypeEntity, EWorkItemConversionType } from "@plane/constants";
|
||||
import {
|
||||
TLoader,
|
||||
TIssuePropertyPayload,
|
||||
@@ -114,4 +114,11 @@ export interface IIssueTypesStore {
|
||||
) => Promise<void | undefined>;
|
||||
createType: (typeData: Partial<TIssueType>) => Promise<TIssueType | undefined>;
|
||||
deleteType: (typeId: string) => Promise<void>;
|
||||
// convert actions
|
||||
convertWorkItem: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
convertTo: EWorkItemConversionType
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Briefcase, Sidebar } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIssueServiceType, EIssuesStoreType, EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
|
||||
import {
|
||||
EIssueServiceType,
|
||||
EIssuesStoreType,
|
||||
EUserPermissionsLevel,
|
||||
EUserProjectRoles,
|
||||
EWorkItemConversionType,
|
||||
} from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { Breadcrumbs, EpicIcon, Header, Logo } from "@plane/ui";
|
||||
// components
|
||||
@@ -16,7 +22,8 @@ import { useAppTheme, useIssueDetail, useProject, useUserPermissions } from "@/h
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
// plane-web components
|
||||
import { ProjectEpicQuickActions } from "@/plane-web/components/epics";
|
||||
import { ConvertWorkItemAction, ProjectEpicQuickActions } from "@/plane-web/components/epics";
|
||||
import { WithFeatureFlagHOC } from "@/plane-web/components/feature-flags";
|
||||
|
||||
export const EpicItemDetailsHeader = observer(() => {
|
||||
// router
|
||||
@@ -40,7 +47,9 @@ export const EpicItemDetailsHeader = observer(() => {
|
||||
|
||||
const isEditingAllowed = allowPermissions(
|
||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug?.toString(),
|
||||
projectId?.toString()
|
||||
);
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -112,6 +121,13 @@ export const EpicItemDetailsHeader = observer(() => {
|
||||
<Header.RightItem>
|
||||
{issueDetails && (
|
||||
<div ref={parentRef} className="flex items-center gap-2">
|
||||
<WithFeatureFlagHOC workspaceSlug={workspaceSlug?.toString()} flag="WORK_ITEM_CONVERSION" fallback={<></>}>
|
||||
<ConvertWorkItemAction
|
||||
workItemId={issueDetails?.id}
|
||||
conversionType={EWorkItemConversionType.WORK_ITEM}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</WithFeatureFlagHOC>
|
||||
<ProjectEpicQuickActions
|
||||
parentRef={parentRef}
|
||||
issue={issueDetails}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
EIssuesStoreType,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
EWorkItemConversionType,
|
||||
} from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
@@ -32,6 +33,8 @@ import {
|
||||
} from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { ConvertWorkItemAction } from "@/plane-web/components/epics";
|
||||
import { WithFeatureFlagHOC } from "@/plane-web/components/feature-flags";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@@ -204,6 +207,13 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
|
||||
{currentUser && !issue?.archived_at && (
|
||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
)}
|
||||
<WithFeatureFlagHOC workspaceSlug={workspaceSlug?.toString()} flag="WORK_ITEM_CONVERSION" fallback={<></>}>
|
||||
<ConvertWorkItemAction
|
||||
workItemId={issue?.id}
|
||||
conversionType={EWorkItemConversionType.EPIC}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</WithFeatureFlagHOC>
|
||||
<div className="flex flex-wrap items-center gap-2.5 text-custom-text-300">
|
||||
<Tooltip tooltipContent={t("common.actions.copy_link")} isMobile={isMobile}>
|
||||
<button
|
||||
|
||||
@@ -39,6 +39,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
modalTitle,
|
||||
primaryButtonText,
|
||||
isProjectSelectionDisabled = false,
|
||||
isConversionOperation = false,
|
||||
} = props;
|
||||
const issueStoreType = useIssueStoreType();
|
||||
|
||||
@@ -72,7 +73,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
removeSubIssue,
|
||||
} = useIssueDetail();
|
||||
const { removeSubIssue: removeEpicSubIssue } = useIssueDetail(EIssueServiceType.EPICS);
|
||||
const { handleCreateUpdatePropertyValues } = useIssueModal();
|
||||
const { handleCreateUpdatePropertyValues, handleConvert } = useIssueModal();
|
||||
const { getProjectByIdentifier } = useProject();
|
||||
const { getIssueTypeById } = useIssueTypes();
|
||||
// pathname
|
||||
@@ -270,7 +271,10 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => {
|
||||
const handleUpdateIssue = async (
|
||||
payload: Partial<TIssue>,
|
||||
showToast: boolean = true
|
||||
): Promise<TIssue | undefined> => {
|
||||
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
||||
|
||||
try {
|
||||
@@ -311,11 +315,13 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
isDraft: isDraft,
|
||||
});
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("success"),
|
||||
message: t("issue_updated_successfully"),
|
||||
});
|
||||
if (showToast) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("success"),
|
||||
message: t("issue_updated_successfully"),
|
||||
});
|
||||
}
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...payload, issueId: data.id, state: "SUCCESS" },
|
||||
@@ -324,11 +330,13 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
message: error?.error ?? t("issue_could_not_be_updated"),
|
||||
});
|
||||
if (showToast) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
message: error?.error ?? t("issue_could_not_be_updated"),
|
||||
});
|
||||
}
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...payload, state: "FAILED" },
|
||||
@@ -347,7 +355,11 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
try {
|
||||
if (beforeFormSubmit) await beforeFormSubmit();
|
||||
if (!data?.id) response = await handleCreateIssue(payload, is_draft_issue);
|
||||
else response = await handleUpdateIssue(payload);
|
||||
else {
|
||||
// if the issue is being converted, handle the conversion
|
||||
if (isConversionOperation) handleConvert(workspaceSlug.toString(), data);
|
||||
response = await handleUpdateIssue(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
@@ -375,7 +387,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
},
|
||||
onAssetUpload: handleUpdateUploadedAssetIds,
|
||||
onClose: handleClose,
|
||||
onSubmit: (payload) => handleFormSubmit(payload, isDraft),
|
||||
onSubmit: (payload, is_draft_issue = false) => handleFormSubmit(payload, is_draft_issue),
|
||||
projectId: activeProjectId,
|
||||
isCreateMoreToggleEnabled: createMore,
|
||||
onCreateMoreToggleChange: handleCreateMoreToggleChange,
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface IssuesModalProps {
|
||||
};
|
||||
isProjectSelectionDisabled?: boolean;
|
||||
templateId?: string;
|
||||
isConversionOperation?: boolean;
|
||||
}
|
||||
|
||||
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { ArchiveRestoreIcon, Link2, MoveDiagonal, MoveRight, Trash2 } from "lucide-react";
|
||||
// plane imports
|
||||
import { ARCHIVABLE_STATE_GROUPS } from "@plane/constants";
|
||||
import { ARCHIVABLE_STATE_GROUPS, EWorkItemConversionType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TNameDescriptionLoader } from "@plane/types";
|
||||
import {
|
||||
@@ -28,6 +28,8 @@ import { generateWorkItemLink } from "@/helpers/issue.helper";
|
||||
import { useIssueDetail, useProject, useProjectState, useUser } from "@/hooks/store";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { ConvertWorkItemAction } from "@/plane-web/components/epics";
|
||||
import { WithFeatureFlagHOC } from "@/plane-web/components/feature-flags";
|
||||
export type TPeekModes = "side-peek" | "modal" | "full-screen";
|
||||
|
||||
const PEEK_OPTIONS: { key: TPeekModes; icon: any; i18n_title: string }[] = [
|
||||
@@ -175,6 +177,9 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
||||
{currentUser && !isArchived && (
|
||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
)}
|
||||
<WithFeatureFlagHOC workspaceSlug={workspaceSlug?.toString()} flag="WORK_ITEM_CONVERSION" fallback={<></>}>
|
||||
<ConvertWorkItemAction workItemId={issueId} conversionType={EWorkItemConversionType.EPIC} />
|
||||
</WithFeatureFlagHOC>
|
||||
<Tooltip tooltipContent={t("common.actions.copy_link")} isMobile={isMobile}>
|
||||
<button type="button" onClick={handleCopyText}>
|
||||
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
|
||||
|
||||
@@ -94,6 +94,8 @@ export interface IBaseIssuesStore {
|
||||
addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
removeCycleFromIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
|
||||
addIssueToList: (issueId: string) => void;
|
||||
removeIssueFromList: (issueId: string) => void;
|
||||
addIssuesToModule: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
||||
@@ -80,6 +80,8 @@ export interface IIssueDetail
|
||||
isRelationModalOpen: TIssueRelationModal | null;
|
||||
isSubIssuesModalOpen: string | null;
|
||||
attachmentDeleteModalId: string | null;
|
||||
isWorkItemToEpicModalOpen: string | null;
|
||||
isEpicToWorkItemModalOpen: string | null;
|
||||
// computed
|
||||
isAnyModalOpen: boolean;
|
||||
// helper actions
|
||||
@@ -100,6 +102,8 @@ export interface IIssueDetail
|
||||
toggleOpenWidget: (state: TIssueDetailWidget) => void;
|
||||
setRelationKey: (relationKey: TIssueRelationTypes | null) => void;
|
||||
setIssueCrudOperationState: (state: TIssueCrudOperationState) => void;
|
||||
toggleWorkItemToEpicModal: (value: string | null) => void;
|
||||
toggleEpicToWorkItemModal: (value: string | null) => void;
|
||||
// store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
issue: IIssueStore;
|
||||
@@ -141,6 +145,8 @@ export class IssueDetail implements IIssueDetail {
|
||||
isRelationModalOpen: TIssueRelationModal | null = null;
|
||||
isSubIssuesModalOpen: string | null = null;
|
||||
attachmentDeleteModalId: string | null = null;
|
||||
isWorkItemToEpicModalOpen: string | null = null;
|
||||
isEpicToWorkItemModalOpen: string | null = null;
|
||||
// service type
|
||||
serviceType: TIssueServiceType;
|
||||
// store
|
||||
@@ -173,6 +179,8 @@ export class IssueDetail implements IIssueDetail {
|
||||
attachmentDeleteModalId: observable.ref,
|
||||
openWidgets: observable.ref,
|
||||
lastWidgetAction: observable.ref,
|
||||
isWorkItemToEpicModalOpen: observable.ref,
|
||||
isEpicToWorkItemModalOpen: observable.ref,
|
||||
// computed
|
||||
isAnyModalOpen: computed,
|
||||
// action
|
||||
@@ -186,6 +194,8 @@ export class IssueDetail implements IIssueDetail {
|
||||
toggleRelationModal: action,
|
||||
toggleSubIssuesModal: action,
|
||||
toggleDeleteAttachmentModal: action,
|
||||
toggleWorkItemToEpicModal: action,
|
||||
toggleEpicToWorkItemModal: action,
|
||||
setOpenWidgets: action,
|
||||
setLastWidgetAction: action,
|
||||
toggleOpenWidget: action,
|
||||
@@ -218,7 +228,9 @@ export class IssueDetail implements IIssueDetail {
|
||||
!!this.isArchiveIssueModalOpen ||
|
||||
!!this.isRelationModalOpen?.issueId ||
|
||||
!!this.isSubIssuesModalOpen ||
|
||||
!!this.attachmentDeleteModalId
|
||||
!!this.attachmentDeleteModalId ||
|
||||
!!this.isWorkItemToEpicModalOpen ||
|
||||
!!this.isEpicToWorkItemModalOpen
|
||||
);
|
||||
}
|
||||
|
||||
@@ -251,7 +263,8 @@ export class IssueDetail implements IIssueDetail {
|
||||
else this.openWidgets = [state, ...this.openWidgets];
|
||||
};
|
||||
setIssueLinkData = (issueLinkData: TIssueLink | null) => (this.issueLinkData = issueLinkData);
|
||||
|
||||
toggleWorkItemToEpicModal = (value: string | null) => (this.isWorkItemToEpicModalOpen = value);
|
||||
toggleEpicToWorkItemModal = (value: string | null) => (this.isEpicToWorkItemModalOpen = value);
|
||||
// issue
|
||||
fetchIssue = async (
|
||||
workspaceSlug: string,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
import React, { FC } from "react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
// plane imports
|
||||
import { EWorkItemConversionType } from "@plane/constants";
|
||||
import { EpicIcon, LayersIcon, Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
interface ConvertWorkItemIconProps {
|
||||
handleOnClick: () => void;
|
||||
conversionType: EWorkItemConversionType;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ConvertWorkItemIcon: FC<ConvertWorkItemIconProps> = (props) => {
|
||||
const { handleOnClick, conversionType, disabled = false } = props;
|
||||
// derived values
|
||||
const IconComponent = conversionType === EWorkItemConversionType.WORK_ITEM ? LayersIcon : EpicIcon;
|
||||
const tooltipContent =
|
||||
conversionType === EWorkItemConversionType.WORK_ITEM ? "Convert to Work item" : "Convert to Epic";
|
||||
const commonIconClasses = "size-4 text-current";
|
||||
|
||||
return (
|
||||
<Tooltip tooltipContent={tooltipContent}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-[1px]",
|
||||
"text-custom-text-300 hover:text-custom-text-100",
|
||||
"hover:cursor-pointer",
|
||||
disabled && "cursor-not-allowed text-custom-text-400"
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
disabled={disabled}
|
||||
aria-label={tooltipContent}
|
||||
>
|
||||
<ArrowRight className={commonIconClasses} />
|
||||
<IconComponent className={commonIconClasses} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./icon";
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EIssueServiceType, EIssuesStoreType, EWorkItemConversionType } from "@plane/constants";
|
||||
// components
|
||||
import { CreateUpdateIssueModal } from "@/components/issues";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// local imports
|
||||
import { useIssueTypes } from "@/plane-web/hooks/store";
|
||||
import { useProjectAdvanced } from "@/plane-web/hooks/store/projects/use-projects";
|
||||
import { CreateUpdateEpicModal } from "../epic-modal";
|
||||
import { ConvertWorkItemIcon } from "./icon";
|
||||
|
||||
type Props = {
|
||||
workItemId: string;
|
||||
conversionType: EWorkItemConversionType;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const ConvertWorkItemAction: FC<Props> = observer((props) => {
|
||||
const { workItemId, conversionType, disabled = false } = props;
|
||||
// hooks
|
||||
const {
|
||||
issue: { getIssueById: getEpicById },
|
||||
isEpicToWorkItemModalOpen,
|
||||
toggleEpicToWorkItemModal,
|
||||
setPeekIssue: setEpicPeek,
|
||||
} = useIssueDetail(EIssueServiceType.EPICS);
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
isWorkItemToEpicModalOpen,
|
||||
toggleWorkItemToEpicModal,
|
||||
setPeekIssue,
|
||||
} = useIssueDetail();
|
||||
const { getProjectFeatures } = useProjectAdvanced();
|
||||
const { getProjectDefaultIssueType, getProjectEpicId } = useIssueTypes();
|
||||
|
||||
// derived values
|
||||
const issue =
|
||||
conversionType === EWorkItemConversionType.WORK_ITEM ? getEpicById(workItemId) : getIssueById(workItemId);
|
||||
const projectId = issue?.project_id;
|
||||
const projectFeatures = projectId ? getProjectFeatures(projectId) : undefined;
|
||||
const isEpicsEnabled = projectFeatures?.is_epic_enabled;
|
||||
const defaultIssueType = projectId ? getProjectDefaultIssueType(projectId) : undefined;
|
||||
const defaultEpicId = projectId ? getProjectEpicId(projectId) : undefined;
|
||||
|
||||
const handleClose = () => {
|
||||
if (conversionType === EWorkItemConversionType.WORK_ITEM) toggleEpicToWorkItemModal(null);
|
||||
else toggleWorkItemToEpicModal(null);
|
||||
setPeekIssue(undefined);
|
||||
setEpicPeek(undefined);
|
||||
};
|
||||
const handleClick = () => {
|
||||
if (conversionType === EWorkItemConversionType.WORK_ITEM) toggleEpicToWorkItemModal(workItemId);
|
||||
else toggleWorkItemToEpicModal(workItemId);
|
||||
};
|
||||
|
||||
if (!isEpicsEnabled) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConvertWorkItemIcon handleOnClick={handleClick} disabled={disabled} conversionType={conversionType} />
|
||||
|
||||
{issue && isEpicToWorkItemModalOpen === workItemId && conversionType === EWorkItemConversionType.WORK_ITEM && (
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={!!isEpicToWorkItemModalOpen}
|
||||
onClose={handleClose}
|
||||
storeType={EIssuesStoreType.PROJECT}
|
||||
data={{
|
||||
...issue,
|
||||
is_epic: false,
|
||||
type_id: defaultIssueType?.id,
|
||||
}}
|
||||
isProjectSelectionDisabled
|
||||
fetchIssueDetails={false}
|
||||
primaryButtonText={{
|
||||
default: "Convert to work item",
|
||||
loading: "Converting...",
|
||||
}}
|
||||
withDraftIssueWrapper={false}
|
||||
isConversionOperation
|
||||
/>
|
||||
)}
|
||||
{issue && isWorkItemToEpicModalOpen === workItemId && conversionType === EWorkItemConversionType.EPIC && (
|
||||
<CreateUpdateEpicModal
|
||||
isOpen={!!isWorkItemToEpicModalOpen}
|
||||
onClose={handleClose}
|
||||
data={{
|
||||
...issue,
|
||||
is_epic: true,
|
||||
type_id: defaultEpicId,
|
||||
}}
|
||||
isProjectSelectionDisabled
|
||||
fetchIssueDetails={false}
|
||||
isConversionOperation
|
||||
primaryButtonText={{
|
||||
default: "Convert to epic",
|
||||
loading: "Converting...",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -29,6 +29,7 @@ export const CreateUpdateEpicModalBase: React.FC<IssuesModalProps> = observer((p
|
||||
fetchIssueDetails = true,
|
||||
primaryButtonText,
|
||||
isProjectSelectionDisabled = false,
|
||||
isConversionOperation = false,
|
||||
} = props;
|
||||
// ref
|
||||
const issueTitleRef = useRef<HTMLInputElement>(null);
|
||||
@@ -41,7 +42,7 @@ export const CreateUpdateEpicModalBase: React.FC<IssuesModalProps> = observer((p
|
||||
const { workspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const { projectsWithCreatePermissions } = useUser();
|
||||
const { fetchIssue } = useIssueDetail(EIssueServiceType.EPICS);
|
||||
const { handleCreateUpdatePropertyValues } = useIssueModal();
|
||||
const { handleCreateUpdatePropertyValues, handleConvert } = useIssueModal();
|
||||
// pathname
|
||||
const pathname = usePathname();
|
||||
// current store details
|
||||
@@ -164,7 +165,10 @@ export const CreateUpdateEpicModalBase: React.FC<IssuesModalProps> = observer((p
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => {
|
||||
const handleUpdateIssue = async (
|
||||
payload: Partial<TIssue>,
|
||||
showToast: boolean = true
|
||||
): Promise<TIssue | undefined> => {
|
||||
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
||||
|
||||
try {
|
||||
@@ -178,11 +182,14 @@ export const CreateUpdateEpicModalBase: React.FC<IssuesModalProps> = observer((p
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
});
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Epic updated successfully.",
|
||||
});
|
||||
if (showToast) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Epic updated successfully.",
|
||||
});
|
||||
}
|
||||
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...payload, issueId: data.id, state: "SUCCESS" },
|
||||
@@ -191,11 +198,13 @@ export const CreateUpdateEpicModalBase: React.FC<IssuesModalProps> = observer((p
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Epic could not be updated. Please try again.",
|
||||
});
|
||||
if (showToast) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Epic could not be updated. Please try again.",
|
||||
});
|
||||
}
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...payload, state: "FAILED" },
|
||||
@@ -214,7 +223,10 @@ export const CreateUpdateEpicModalBase: React.FC<IssuesModalProps> = observer((p
|
||||
try {
|
||||
if (beforeFormSubmit) await beforeFormSubmit();
|
||||
if (!data?.id) response = await handleCreateIssue(payload);
|
||||
else response = await handleUpdateIssue(payload);
|
||||
else {
|
||||
if (isConversionOperation) handleConvert(workspaceSlug.toString(), data);
|
||||
response = await handleUpdateIssue(payload, !isConversionOperation);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface EpicModalProps {
|
||||
loading: string;
|
||||
};
|
||||
isProjectSelectionDisabled?: boolean;
|
||||
isConversionOperation?: boolean;
|
||||
}
|
||||
|
||||
export const CreateUpdateEpicModal: React.FC<EpicModalProps> = observer(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { mutate } from "swr";
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { EIssueServiceType, EWorkItemConversionType } from "@plane/constants";
|
||||
// plane imports
|
||||
import { ISearchIssueResponse, TIssuePropertyValueErrors, TIssuePropertyValues } from "@plane/types";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ISearchIssueResponse, TIssue, TIssuePropertyValueErrors, TIssuePropertyValues } from "@plane/types";
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import { getPropertiesDefaultValues } from "@plane/utils";
|
||||
// components
|
||||
@@ -33,8 +34,10 @@ export const EpicModalProvider = observer((props: TEpicModalProviderProps) => {
|
||||
const [issuePropertyValues, setIssuePropertyValues] = useState<TIssuePropertyValues>({});
|
||||
const [issuePropertyValueErrors, setIssuePropertyValueErrors] = useState<TIssuePropertyValueErrors>({});
|
||||
// plane web hooks
|
||||
const { isEpicEnabledForProject, getIssueTypeById, getIssueTypeProperties, getProjectEpicId } = useIssueTypes();
|
||||
const { isEpicEnabledForProject, getIssueTypeById, getIssueTypeProperties, getProjectEpicId, convertWorkItem } =
|
||||
useIssueTypes();
|
||||
const { fetchPropertyActivities } = useIssuePropertiesActivity();
|
||||
const { t } = useTranslation();
|
||||
// helpers
|
||||
const getIssueTypeIdOnProjectChange = (projectId: string) => {
|
||||
// get active issue types for the project
|
||||
@@ -131,6 +134,31 @@ export const EpicModalProvider = observer((props: TEpicModalProviderProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to handle work item conversion
|
||||
*/
|
||||
const handleConvert = async (workspaceSlug: string, data: Partial<TIssue> | undefined) => {
|
||||
if (data?.id && data?.project_id) {
|
||||
await convertWorkItem(workspaceSlug.toString(), data.project_id, data?.id, EWorkItemConversionType.EPIC)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("success"),
|
||||
message: "Work item converted to epic successfully",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
message: "Work item could not be converted to epic. Please try again.",
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IssueModalContext.Provider
|
||||
value={{
|
||||
@@ -150,6 +178,7 @@ export const EpicModalProvider = observer((props: TEpicModalProviderProps) => {
|
||||
handleCreateUpdatePropertyValues,
|
||||
handleProjectEntitiesFetch: () => Promise.resolve(),
|
||||
handleTemplateChange: () => Promise.resolve(),
|
||||
handleConvert,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./epic-modal";
|
||||
export * from "./epic-quick-action";
|
||||
export * from "./peek-overview";
|
||||
export * from "./settings";
|
||||
export * from "./conversions";
|
||||
|
||||
@@ -5,7 +5,13 @@ import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Link2, MoveDiagonal, MoveRight, Sidebar } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIssueServiceType, EIssuesStoreType, EUserProjectRoles, EUserPermissionsLevel } from "@plane/constants";
|
||||
import {
|
||||
EIssueServiceType,
|
||||
EIssuesStoreType,
|
||||
EUserProjectRoles,
|
||||
EUserPermissionsLevel,
|
||||
EWorkItemConversionType,
|
||||
} from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
import {
|
||||
CenterPanelIcon,
|
||||
@@ -29,6 +35,8 @@ import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { WithFeatureFlagHOC } from "../../feature-flags";
|
||||
import { ConvertWorkItemAction } from "../conversions";
|
||||
import { ProjectEpicQuickActions } from "../epic-quick-action";
|
||||
export type TPeekModes = "side-peek" | "modal" | "full-screen";
|
||||
|
||||
@@ -192,6 +200,9 @@ export const EpicPeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pro
|
||||
<div className="flex items-center gap-x-4">
|
||||
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
|
||||
<div className="flex items-center gap-4">
|
||||
<WithFeatureFlagHOC workspaceSlug={workspaceSlug?.toString()} flag="WORK_ITEM_CONVERSION" fallback={<></>}>
|
||||
<ConvertWorkItemAction workItemId={issueId} conversionType={EWorkItemConversionType.WORK_ITEM} />
|
||||
</WithFeatureFlagHOC>
|
||||
<Tooltip tooltipContent="Copy link" isMobile={isMobile}>
|
||||
<button type="button" onClick={handleCopyText}>
|
||||
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
|
||||
|
||||
@@ -2,8 +2,9 @@ import React, { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { mutate } from "swr";
|
||||
// plane imports
|
||||
import { DEFAULT_WORK_ITEM_FORM_VALUES, EWorkItemTypeEntity } from "@plane/constants";
|
||||
import { ISearchIssueResponse, TIssuePropertyValueErrors, TIssuePropertyValues } from "@plane/types";
|
||||
import { DEFAULT_WORK_ITEM_FORM_VALUES, EWorkItemConversionType, EWorkItemTypeEntity } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ISearchIssueResponse, TIssue, TIssuePropertyValueErrors, TIssuePropertyValues } from "@plane/types";
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import {
|
||||
extractAndSanitizeCustomPropertyValuesFormData,
|
||||
@@ -39,6 +40,7 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) =>
|
||||
const [issuePropertyValues, setIssuePropertyValues] = useState<TIssuePropertyValues>({});
|
||||
const [issuePropertyValueErrors, setIssuePropertyValueErrors] = useState<TIssuePropertyValueErrors>({});
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { getProjectStateIds, getAvailableWorkItemCreationStateIds, fetchProjectStates } = useProjectState();
|
||||
const { getProjectLabelIds, fetchProjectLabels } = useLabel();
|
||||
const { getModulesFetchStatusByProjectId, getProjectModuleIds, fetchModules } = useModule();
|
||||
@@ -49,6 +51,7 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) =>
|
||||
const {
|
||||
isWorkItemTypeEnabledForProject,
|
||||
getIssueTypeById,
|
||||
convertWorkItem,
|
||||
getIssueTypeProperties,
|
||||
getProjectIssueTypes,
|
||||
getProjectDefaultIssueType,
|
||||
@@ -314,6 +317,31 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) =>
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Used to handle work item conversion
|
||||
*/
|
||||
const handleConvert = async (workspaceSlug: string, data: Partial<TIssue> | undefined) => {
|
||||
if (data?.id && data?.project_id) {
|
||||
await convertWorkItem(workspaceSlug.toString(), data.project_id, data?.id, EWorkItemConversionType.WORK_ITEM)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("success"),
|
||||
message: "Epic converted to work item successfully",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
message: "Epic could not be converted to work item. Please try again.",
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IssueModalContext.Provider
|
||||
value={{
|
||||
@@ -333,6 +361,7 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) =>
|
||||
handleCreateUpdatePropertyValues,
|
||||
handleProjectEntitiesFetch,
|
||||
handleTemplateChange,
|
||||
handleConvert,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// plane imports
|
||||
import { EWorkItemConversionType } from "@plane/constants";
|
||||
import { TEpicAnalytics, TEpicStats } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
@@ -40,6 +41,20 @@ export class EpicService extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async convertWorkItemType(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
entityId: string,
|
||||
entityType: EWorkItemConversionType
|
||||
): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/conversion/${entityId}/`, {
|
||||
conversion_type: entityType,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
export const epicService = new EpicService();
|
||||
|
||||
@@ -44,7 +44,6 @@ export class CommandPaletteStore extends BaseCommandPaletteStore implements ICom
|
||||
createUpdateTeamspaceViewModal: observable,
|
||||
createUpdateInitiativeModal: observable,
|
||||
createUpdateCustomerModal: observable,
|
||||
|
||||
// computed
|
||||
isAnyModalOpen: computed,
|
||||
// actions
|
||||
|
||||
@@ -2,7 +2,7 @@ import set from "lodash/set";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import { EWorkItemTypeEntity } from "@plane/constants";
|
||||
import { EWorkItemConversionType, EWorkItemTypeEntity } from "@plane/constants";
|
||||
import {
|
||||
TLoader,
|
||||
TIssueType,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
issuePropertyOptionService,
|
||||
issuePropertyService,
|
||||
issueTypeService,
|
||||
epicService,
|
||||
} from "@/plane-web/services/issue-types";
|
||||
// plane web stores
|
||||
import { IssueType } from "@/plane-web/store/issue-types";
|
||||
@@ -67,6 +68,7 @@ export class IssueTypes implements IIssueTypesStore {
|
||||
fetchAllEpicPropertiesAndOptions: action,
|
||||
createType: action,
|
||||
deleteType: action,
|
||||
convertWorkItem: action,
|
||||
});
|
||||
// root store
|
||||
this.rootStore = store;
|
||||
@@ -705,4 +707,39 @@ export class IssueTypes implements IIssueTypesStore {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Convert a work item type to another work item type
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param workItemId
|
||||
* @param convertTo
|
||||
*/
|
||||
convertWorkItem = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
workItemId: string,
|
||||
convertTo: EWorkItemConversionType
|
||||
) => {
|
||||
if (!workspaceSlug || !projectId || !workItemId || !convertTo) return;
|
||||
try {
|
||||
await epicService.convertWorkItemType(workspaceSlug, projectId, workItemId, convertTo);
|
||||
runInAction(() => {
|
||||
if (convertTo === EWorkItemConversionType.WORK_ITEM) {
|
||||
this.rootStore.issue.projectEpics.removeIssueFromList(workItemId);
|
||||
this.rootStore.issue.projectIssues.addIssueToList(workItemId);
|
||||
// update is_epic to false
|
||||
this.rootStore.issue.issues.updateIssue(workItemId, { is_epic: false });
|
||||
} else if (convertTo === EWorkItemConversionType.EPIC) {
|
||||
this.rootStore.issue.projectIssues.removeIssueFromList(workItemId);
|
||||
this.rootStore.issue.projectEpics.addIssueToList(workItemId);
|
||||
// update is_epic to true
|
||||
this.rootStore.issue.issues.updateIssue(workItemId, { is_epic: true });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.loader = "loaded";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user