From fedcdf0c84335a14a8092dbf9d0129e93457090f Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 24 Dec 2024 20:52:03 +0530 Subject: [PATCH] [WEB-2879] chore sub issue analytics improvements (#6275) * chore: epics type added to package * chore: epic analytics helper function added * chore: sub issue analytics mutation improvement --- packages/types/src/epics.d.ts | 16 +++++ packages/types/src/index.d.ts | 1 + web/ce/helpers/epic-analytics.ts | 15 +++++ .../sub-issues/helper.tsx | 64 ++++++++++++++++--- .../issues/sub-issues/issue-list-item.tsx | 10 +-- .../issues/sub-issues/properties.tsx | 4 +- .../store/issue/helpers/base-issues.store.ts | 11 +++- web/ee/helpers/epic-analytics.ts | 1 + 8 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 packages/types/src/epics.d.ts create mode 100644 web/ce/helpers/epic-analytics.ts create mode 100644 web/ee/helpers/epic-analytics.ts diff --git a/packages/types/src/epics.d.ts b/packages/types/src/epics.d.ts new file mode 100644 index 000000000..1ba50e2f2 --- /dev/null +++ b/packages/types/src/epics.d.ts @@ -0,0 +1,16 @@ +export type TEpicAnalyticsGroup = + | "backlog_issues" + | "unstarted_issues" + | "started_issues" + | "completed_issues" + | "cancelled_issues" + | "overdue_issues"; + +export type TEpicAnalytics = { + backlog_issues: number; + unstarted_issues: number; + started_issues: number; + completed_issues: number; + cancelled_issues: number; + overdue_issues: number; +}; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 43cc3084a..af1e3ff48 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -36,3 +36,4 @@ export * from "./workspace-draft-issues/base"; export * from "./command-palette"; export * from "./timezone"; export * from "./activity"; +export * from "./epics"; diff --git a/web/ce/helpers/epic-analytics.ts b/web/ce/helpers/epic-analytics.ts new file mode 100644 index 000000000..43e6ffef0 --- /dev/null +++ b/web/ce/helpers/epic-analytics.ts @@ -0,0 +1,15 @@ +import { TEpicAnalyticsGroup } from "@plane/types"; + +export const updateEpicAnalytics = () => { + const updateAnalytics = ( + workspaceSlug: string, + projectId: string, + epicId: string, + data: { + incrementStateGroupCount?: TEpicAnalyticsGroup; + decrementStateGroupCount?: TEpicAnalyticsGroup; + } + ) => {}; + + return { updateAnalytics }; +}; diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx index cc8abd82f..b67c1a7ee 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx @@ -1,13 +1,15 @@ "use client"; import { useMemo } from "react"; -import { usePathname } from "next/navigation"; +import { useParams, usePathname } from "next/navigation"; import { EIssueServiceType } from "@plane/constants"; import { TIssue, TIssueServiceType } from "@plane/types"; import { TOAST_TYPE, setToast } from "@plane/ui"; // helper import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks -import { useEventTracker, useIssueDetail } from "@/hooks/store"; +import { useEventTracker, useIssueDetail, useProjectState } from "@/hooks/store"; +// plane-web +import { updateEpicAnalytics } from "@/plane-web/helpers/epic-analytics"; // type import { TSubIssueOperations } from "../../sub-issues"; @@ -20,16 +22,26 @@ export type TRelationIssueOperations = { export const useSubIssueOperations = ( issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES ): TSubIssueOperations => { + // router + const { epicId: epicIdParam } = useParams(); + const pathname = usePathname(); + // store hooks const { + issue: { getIssueById }, subIssues: { setSubIssueHelpers }, - fetchSubIssues, createSubIssues, updateSubIssue, deleteSubIssue, } = useIssueDetail(); - const { removeSubIssue } = useIssueDetail(issueServiceType); + const { getStateById } = useProjectState(); + const { peekIssue: epicPeekIssue } = useIssueDetail(EIssueServiceType.EPICS); + // const { updateEpicAnalytics } = useIssueTypes(); + const { updateAnalytics } = updateEpicAnalytics(); + const { fetchSubIssues, removeSubIssue } = useIssueDetail(issueServiceType); const { captureIssueEvent } = useEventTracker(); - const pathname = usePathname(); + + // derived values + const epicId = epicIdParam || epicPeekIssue?.issueId; const subIssueOperations: TSubIssueOperations = useMemo( () => ({ @@ -39,7 +51,7 @@ export const useSubIssueOperations = ( setToast({ type: TOAST_TYPE.SUCCESS, title: "Link Copied!", - message: "Issue link copied to clipboard.", + message: `${issueServiceType === EIssueServiceType.ISSUES ? "Issue" : "Epic"} link copied to clipboard`, }); }); }, @@ -50,7 +62,7 @@ export const useSubIssueOperations = ( setToast({ type: TOAST_TYPE.ERROR, title: "Error!", - message: "Error fetching sub-issues", + message: `Error fetching ${issueServiceType === EIssueServiceType.ISSUES ? "sub-issues" : "issues"}`, }); } }, @@ -60,13 +72,13 @@ export const useSubIssueOperations = ( setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", - message: "Sub-issues added successfully", + message: `${issueServiceType === EIssueServiceType.ISSUES ? "Sub-issues" : "Issues"} added successfully`, }); } catch (error) { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", - message: "Error adding sub-issue", + message: `Error adding ${issueServiceType === EIssueServiceType.ISSUES ? "sub-issues" : "issues"}`, }); } }, @@ -82,6 +94,30 @@ export const useSubIssueOperations = ( try { setSubIssueHelpers(parentIssueId, "issue_loader", issueId); await updateSubIssue(workspaceSlug, projectId, parentIssueId, issueId, issueData, oldIssue, fromModal); + + if (issueServiceType === EIssueServiceType.EPICS) { + const oldState = getStateById(oldIssue?.state_id)?.group; + + if (oldState && oldIssue && issueData && epicId) { + // Check if parent_id is changed if yes then decrement the epic analytics count + if (issueData.parent_id && oldIssue?.parent_id && issueData.parent_id !== oldIssue?.parent_id) { + updateAnalytics(workspaceSlug, projectId, epicId.toString(), { + decrementStateGroupCount: `${oldState}_issues`, + }); + } + + // Check if state_id is changed if yes then decrement the old state group count and increment the new state group count + if (issueData.state_id) { + const newState = getStateById(issueData.state_id)?.group; + if (oldState && newState && oldState !== newState) { + updateAnalytics(workspaceSlug, projectId, epicId.toString(), { + decrementStateGroupCount: `${oldState}_issues`, + incrementStateGroupCount: `${newState}_issues`, + }); + } + } + } + } captureIssueEvent({ eventName: "Sub-issue updated", payload: { ...oldIssue, ...issueData, state: "SUCCESS", element: "Issue detail page" }, @@ -118,6 +154,16 @@ export const useSubIssueOperations = ( try { setSubIssueHelpers(parentIssueId, "issue_loader", issueId); await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId); + if (issueServiceType === EIssueServiceType.EPICS) { + const issueBeforeRemoval = getIssueById(issueId); + const oldState = getStateById(issueBeforeRemoval?.state_id)?.group; + + if (epicId && oldState) { + updateAnalytics(workspaceSlug, projectId, epicId.toString(), { + decrementStateGroupCount: `${oldState}_issues`, + }); + } + } setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", diff --git a/web/core/components/issues/sub-issues/issue-list-item.tsx b/web/core/components/issues/sub-issues/issue-list-item.tsx index 64a5c7a02..bfecc0193 100644 --- a/web/core/components/issues/sub-issues/issue-list-item.tsx +++ b/web/core/components/issues/sub-issues/issue-list-item.tsx @@ -3,6 +3,7 @@ import React from "react"; import { observer } from "mobx-react"; import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; +import { EIssueServiceType } from "@plane/constants"; import { TIssue, TIssueServiceType } from "@plane/types"; // ui import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; @@ -50,14 +51,14 @@ export const IssueListItem: React.FC = observer((props) => { disabled, handleIssueCrudState, subIssueOperations, + issueServiceType = EIssueServiceType.ISSUES, } = props; const { issue: { getIssueById }, subIssues: { subIssueHelpersByIssueId, setSubIssueHelpers }, - toggleCreateIssueModal, - toggleDeleteIssueModal, - } = useIssueDetail(); + } = useIssueDetail(issueServiceType); + const { toggleCreateIssueModal, toggleDeleteIssueModal } = useIssueDetail(issueServiceType); const project = useProject(); const { getProjectStates } = useProjectState(); const { handleRedirection } = useIssuePeekOverviewRedirection(); @@ -164,6 +165,7 @@ export const IssueListItem: React.FC = observer((props) => { issueId={issueId} disabled={disabled} subIssueOperations={subIssueOperations} + issueServiceType={issueServiceType} /> @@ -209,7 +211,7 @@ export const IssueListItem: React.FC = observer((props) => { >
- Remove parent issue + {`Remove ${issueServiceType === EIssueServiceType.ISSUES ? "parent" : ""} issue`}
)} diff --git a/web/core/components/issues/sub-issues/properties.tsx b/web/core/components/issues/sub-issues/properties.tsx index c4ea3bbd2..731487668 100644 --- a/web/core/components/issues/sub-issues/properties.tsx +++ b/web/core/components/issues/sub-issues/properties.tsx @@ -17,11 +17,11 @@ export interface IIssueProperty { } export const IssueProperty: React.FC = (props) => { - const { workspaceSlug, parentIssueId, issueId, disabled, subIssueOperations } = props; + const { workspaceSlug, parentIssueId, issueId, disabled, subIssueOperations, issueServiceType } = props; // hooks const { issue: { getIssueById }, - } = useIssueDetail(); + } = useIssueDetail(issueServiceType); const issue = getIssueById(issueId); diff --git a/web/core/store/issue/helpers/base-issues.store.ts b/web/core/store/issue/helpers/base-issues.store.ts index 3b115bb06..6a369f69f 100644 --- a/web/core/store/issue/helpers/base-issues.store.ts +++ b/web/core/store/issue/helpers/base-issues.store.ts @@ -13,7 +13,7 @@ import update from "lodash/update"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // plane constants -import { EIssueLayoutTypes, ALL_ISSUES } from "@plane/constants"; +import { EIssueLayoutTypes, ALL_ISSUES, EIssueServiceType } from "@plane/constants"; // types import { TIssue, @@ -203,7 +203,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { // API Abort controller controller: AbortController; - constructor(_rootStore: IIssueRootStore, issueFilterStore: IBaseIssueFilterStore, isArchived = false) { + constructor( + _rootStore: IIssueRootStore, + issueFilterStore: IBaseIssueFilterStore, + isArchived = false, + serviceType = EIssueServiceType.ISSUES + ) { makeObservable(this, { // observable loader: observable, @@ -257,7 +262,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { this.isArchived = isArchived; - this.issueService = new IssueService(); + this.issueService = new IssueService(serviceType); this.issueArchiveService = new IssueArchiveService(); this.issueDraftService = new IssueDraftService(); this.moduleService = new ModuleService(); diff --git a/web/ee/helpers/epic-analytics.ts b/web/ee/helpers/epic-analytics.ts new file mode 100644 index 000000000..48884d4c1 --- /dev/null +++ b/web/ee/helpers/epic-analytics.ts @@ -0,0 +1 @@ +export * from "ce/helpers/epic-analytics";