diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index 341b8c0ac1..c2b40a0a79 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -358,7 +358,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( else { // if the issue is being converted, handle the conversion if (isConversionOperation) handleConvert(workspaceSlug.toString(), data); - response = await handleUpdateIssue(payload); + response = await handleUpdateIssue(payload, !isConversionOperation); } } catch (error) { console.error(error); diff --git a/web/core/store/issue/issue-details/root.store.ts b/web/core/store/issue/issue-details/root.store.ts index 5a7ef44c27..a8c2022162 100644 --- a/web/core/store/issue/issue-details/root.store.ts +++ b/web/core/store/issue/issue-details/root.store.ts @@ -82,6 +82,7 @@ export interface IIssueDetail attachmentDeleteModalId: string | null; isWorkItemToEpicModalOpen: string | null; isEpicToWorkItemModalOpen: string | null; + isConversionWarningModalOpen: string | null; // computed isAnyModalOpen: boolean; // helper actions @@ -104,6 +105,7 @@ export interface IIssueDetail setIssueCrudOperationState: (state: TIssueCrudOperationState) => void; toggleWorkItemToEpicModal: (value: string | null) => void; toggleEpicToWorkItemModal: (value: string | null) => void; + toggleConversionWarningModal: (value: string | null) => void; // store rootIssueStore: IIssueRootStore; issue: IIssueStore; @@ -147,6 +149,7 @@ export class IssueDetail implements IIssueDetail { attachmentDeleteModalId: string | null = null; isWorkItemToEpicModalOpen: string | null = null; isEpicToWorkItemModalOpen: string | null = null; + isConversionWarningModalOpen: string | null = null; // service type serviceType: TIssueServiceType; // store @@ -181,6 +184,7 @@ export class IssueDetail implements IIssueDetail { lastWidgetAction: observable.ref, isWorkItemToEpicModalOpen: observable.ref, isEpicToWorkItemModalOpen: observable.ref, + isConversionWarningModalOpen: observable.ref, // computed isAnyModalOpen: computed, // action @@ -201,6 +205,7 @@ export class IssueDetail implements IIssueDetail { toggleOpenWidget: action, setRelationKey: action, setIssueCrudOperationState: action, + toggleConversionWarningModal: action, }); // store @@ -230,7 +235,8 @@ export class IssueDetail implements IIssueDetail { !!this.isSubIssuesModalOpen || !!this.attachmentDeleteModalId || !!this.isWorkItemToEpicModalOpen || - !!this.isEpicToWorkItemModalOpen + !!this.isEpicToWorkItemModalOpen || + !!this.isConversionWarningModalOpen ); } @@ -265,6 +271,7 @@ export class IssueDetail implements IIssueDetail { setIssueLinkData = (issueLinkData: TIssueLink | null) => (this.issueLinkData = issueLinkData); toggleWorkItemToEpicModal = (value: string | null) => (this.isWorkItemToEpicModalOpen = value); toggleEpicToWorkItemModal = (value: string | null) => (this.isEpicToWorkItemModalOpen = value); + toggleConversionWarningModal = (value: string | null) => (this.isConversionWarningModalOpen = value); // issue fetchIssue = async ( workspaceSlug: string, diff --git a/web/ee/components/epics/conversions/root.tsx b/web/ee/components/epics/conversions/root.tsx index 25df5766b3..07cdcf408d 100644 --- a/web/ee/components/epics/conversions/root.tsx +++ b/web/ee/components/epics/conversions/root.tsx @@ -4,6 +4,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; // plane imports import { EIssueServiceType, EIssuesStoreType, EWorkItemConversionType } from "@plane/constants"; +import { AlertModalCore, EModalPosition, EModalWidth } from "@plane/ui"; // components import { CreateUpdateIssueModal } from "@/components/issues"; // hooks @@ -33,6 +34,8 @@ export const ConvertWorkItemAction: FC = observer((props) => { issue: { getIssueById }, isWorkItemToEpicModalOpen, toggleWorkItemToEpicModal, + isConversionWarningModalOpen, + toggleConversionWarningModal, setPeekIssue, } = useIssueDetail(); const { getProjectFeatures } = useProjectAdvanced(); @@ -53,16 +56,40 @@ export const ConvertWorkItemAction: FC = observer((props) => { setPeekIssue(undefined); setEpicPeek(undefined); }; - const handleClick = () => { + const handleConvert = () => { if (conversionType === EWorkItemConversionType.WORK_ITEM) toggleEpicToWorkItemModal(workItemId); else toggleWorkItemToEpicModal(workItemId); }; + const handleOnClick = () => { + if (conversionType === EWorkItemConversionType.WORK_ITEM) handleConvert(); + else toggleConversionWarningModal(workItemId); + }; + if (!isEpicsEnabled) return null; return ( <> - + + + {isConversionWarningModalOpen === workItemId && conversionType === EWorkItemConversionType.EPIC && ( + toggleConversionWarningModal(null)} + handleSubmit={handleConvert} + isSubmitting={false} + title="Convert this work item to epic?" + content="This will detach any associated cycles, modules, and parent items connected to it." + primaryButtonText={{ + default: "Continue", + loading: "Loading", + }} + variant="primary" + secondaryButtonText="Cancel" + width={EModalWidth.LG} + position={EModalPosition.TOP} + /> + )} {issue && isEpicToWorkItemModalOpen === workItemId && conversionType === EWorkItemConversionType.WORK_ITEM && ( = observer((props) => { fetchIssueDetails={false} primaryButtonText={{ default: "Convert to work item", - loading: "Converting...", + loading: "Converting", }} withDraftIssueWrapper={false} isConversionOperation @@ -98,7 +125,7 @@ export const ConvertWorkItemAction: FC = observer((props) => { isConversionOperation primaryButtonText={{ default: "Convert to epic", - loading: "Converting...", + loading: "Converting", }} /> )} diff --git a/web/ee/components/epics/epic-modal/provider.tsx b/web/ee/components/epics/epic-modal/provider.tsx index 61706f98ed..196cdbba0e 100644 --- a/web/ee/components/epics/epic-modal/provider.tsx +++ b/web/ee/components/epics/epic-modal/provider.tsx @@ -14,10 +14,13 @@ import { TCreateUpdatePropertyValuesProps, TPropertyValuesValidationProps, } from "@/components/issues"; +// plane web components +import { ConversionToastActionItems } from "@/plane-web/components/issues"; // plane web hooks import { useIssuePropertiesActivity, useIssueTypes } from "@/plane-web/hooks/store"; // plane web services import { IssuePropertyValuesService } from "@/plane-web/services/issue-types"; +// local components type TEpicModalProviderProps = { children: React.ReactNode; @@ -145,6 +148,7 @@ export const EpicModalProvider = observer((props: TEpicModalProviderProps) => { type: TOAST_TYPE.SUCCESS, title: t("success"), message: "Work item converted to epic successfully", + actionItems: , }); }) .catch((error) => { diff --git a/web/ee/components/issues/conversion-toast-action-items.tsx b/web/ee/components/issues/conversion-toast-action-items.tsx new file mode 100644 index 0000000000..5b57ef1874 --- /dev/null +++ b/web/ee/components/issues/conversion-toast-action-items.tsx @@ -0,0 +1,82 @@ +"use client"; +import React, { FC, useState } from "react"; +import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { copyUrlToClipboard } from "@plane/utils"; +// helpers +import { generateWorkItemLink } from "@/helpers/issue.helper"; +// hooks +import { useIssueDetail, useProject } from "@/hooks/store"; + +type TConversionToastActionItems = { + workspaceSlug: string; + workItemId: string | undefined; +}; + +export const ConversionToastActionItems: FC = observer((props) => { + const { workspaceSlug, workItemId } = props; + // state + const [copied, setCopied] = useState(false); + // store hooks + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { getProjectIdentifierById } = useProject(); + const { t } = useTranslation(); + + // derived values + const workItem = workItemId ? getIssueById(workItemId) : undefined; + const projectIdentifier = getProjectIdentifierById(workItem?.project_id); + + if (!workItem) return null; + + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: workItem?.project_id, + issueId: workItem?.id, + projectIdentifier, + sequenceId: workItem?.sequence_id, + isEpic: workItem?.is_epic, + }); + + const copyToClipboard = async (e: React.MouseEvent) => { + try { + await copyUrlToClipboard(workItemLink); + setCopied(true); + setTimeout(() => setCopied(false), 3000); + } catch (error) { + setCopied(false); + } + e.preventDefault(); + e.stopPropagation(); + }; + + return ( +
+ + {`${t("common.view")} ${workItem?.is_epic ? t("common.epic") : t("work_item")}`} + + + {copied ? ( + <> + {t("common.copied")}! + + ) : ( + <> + + + )} +
+ ); +}); diff --git a/web/ee/components/issues/index.ts b/web/ee/components/issues/index.ts index 6ec8545c50..94aae6b1e6 100644 --- a/web/ee/components/issues/index.ts +++ b/web/ee/components/issues/index.ts @@ -8,3 +8,4 @@ export * from "./header"; export * from "./issue-layouts"; export * from "./header"; export * from "./issue-detail-widgets"; +export * from "./conversion-toast-action-items"; diff --git a/web/ee/components/issues/issue-modal/provider.tsx b/web/ee/components/issues/issue-modal/provider.tsx index fa5ab50712..a9f834d8d2 100644 --- a/web/ee/components/issues/issue-modal/provider.tsx +++ b/web/ee/components/issues/issue-modal/provider.tsx @@ -27,6 +27,7 @@ import { useLabel, useMember, useModule, useProjectState } from "@/hooks/store"; import { useIssuePropertiesActivity, useIssueTypes, useWorkItemTemplates } from "@/plane-web/hooks/store"; // plane web services import { DraftIssuePropertyValuesService, IssuePropertyValuesService } from "@/plane-web/services/issue-types"; +import { ConversionToastActionItems } from "../conversion-toast-action-items"; const issuePropertyValuesService = new IssuePropertyValuesService(); const draftIssuePropertyValuesService = new DraftIssuePropertyValuesService(); @@ -328,6 +329,7 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) => type: TOAST_TYPE.SUCCESS, title: t("success"), message: "Epic converted to work item successfully", + actionItems: , }); }) .catch((error) => {