[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
This commit is contained in:
parent
ff936887d2
commit
fedcdf0c84
8 changed files with 104 additions and 18 deletions
16
packages/types/src/epics.d.ts
vendored
Normal file
16
packages/types/src/epics.d.ts
vendored
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
1
packages/types/src/index.d.ts
vendored
1
packages/types/src/index.d.ts
vendored
|
|
@ -36,3 +36,4 @@ export * from "./workspace-draft-issues/base";
|
||||||
export * from "./command-palette";
|
export * from "./command-palette";
|
||||||
export * from "./timezone";
|
export * from "./timezone";
|
||||||
export * from "./activity";
|
export * from "./activity";
|
||||||
|
export * from "./epics";
|
||||||
|
|
|
||||||
15
web/ce/helpers/epic-analytics.ts
Normal file
15
web/ce/helpers/epic-analytics.ts
Normal file
|
|
@ -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 };
|
||||||
|
};
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import { EIssueServiceType } from "@plane/constants";
|
import { EIssueServiceType } from "@plane/constants";
|
||||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// helper
|
// helper
|
||||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||||
// hooks
|
// 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
|
// type
|
||||||
import { TSubIssueOperations } from "../../sub-issues";
|
import { TSubIssueOperations } from "../../sub-issues";
|
||||||
|
|
||||||
|
|
@ -20,16 +22,26 @@ export type TRelationIssueOperations = {
|
||||||
export const useSubIssueOperations = (
|
export const useSubIssueOperations = (
|
||||||
issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES
|
issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES
|
||||||
): TSubIssueOperations => {
|
): TSubIssueOperations => {
|
||||||
|
// router
|
||||||
|
const { epicId: epicIdParam } = useParams();
|
||||||
|
const pathname = usePathname();
|
||||||
|
// store hooks
|
||||||
const {
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
subIssues: { setSubIssueHelpers },
|
subIssues: { setSubIssueHelpers },
|
||||||
fetchSubIssues,
|
|
||||||
createSubIssues,
|
createSubIssues,
|
||||||
updateSubIssue,
|
updateSubIssue,
|
||||||
deleteSubIssue,
|
deleteSubIssue,
|
||||||
} = useIssueDetail();
|
} = 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 { captureIssueEvent } = useEventTracker();
|
||||||
const pathname = usePathname();
|
|
||||||
|
// derived values
|
||||||
|
const epicId = epicIdParam || epicPeekIssue?.issueId;
|
||||||
|
|
||||||
const subIssueOperations: TSubIssueOperations = useMemo(
|
const subIssueOperations: TSubIssueOperations = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -39,7 +51,7 @@ export const useSubIssueOperations = (
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Link Copied!",
|
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({
|
setToast({
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: "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({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Sub-issues added successfully",
|
message: `${issueServiceType === EIssueServiceType.ISSUES ? "Sub-issues" : "Issues"} added successfully`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: "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 {
|
try {
|
||||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||||
await updateSubIssue(workspaceSlug, projectId, parentIssueId, issueId, issueData, oldIssue, fromModal);
|
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({
|
captureIssueEvent({
|
||||||
eventName: "Sub-issue updated",
|
eventName: "Sub-issue updated",
|
||||||
payload: { ...oldIssue, ...issueData, state: "SUCCESS", element: "Issue detail page" },
|
payload: { ...oldIssue, ...issueData, state: "SUCCESS", element: "Issue detail page" },
|
||||||
|
|
@ -118,6 +154,16 @@ export const useSubIssueOperations = (
|
||||||
try {
|
try {
|
||||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||||
await removeSubIssue(workspaceSlug, projectId, parentIssueId, 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({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
||||||
|
import { EIssueServiceType } from "@plane/constants";
|
||||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
||||||
|
|
@ -50,14 +51,14 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
||||||
disabled,
|
disabled,
|
||||||
handleIssueCrudState,
|
handleIssueCrudState,
|
||||||
subIssueOperations,
|
subIssueOperations,
|
||||||
|
issueServiceType = EIssueServiceType.ISSUES,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
subIssues: { subIssueHelpersByIssueId, setSubIssueHelpers },
|
subIssues: { subIssueHelpersByIssueId, setSubIssueHelpers },
|
||||||
toggleCreateIssueModal,
|
} = useIssueDetail(issueServiceType);
|
||||||
toggleDeleteIssueModal,
|
const { toggleCreateIssueModal, toggleDeleteIssueModal } = useIssueDetail(issueServiceType);
|
||||||
} = useIssueDetail();
|
|
||||||
const project = useProject();
|
const project = useProject();
|
||||||
const { getProjectStates } = useProjectState();
|
const { getProjectStates } = useProjectState();
|
||||||
const { handleRedirection } = useIssuePeekOverviewRedirection();
|
const { handleRedirection } = useIssuePeekOverviewRedirection();
|
||||||
|
|
@ -164,6 +165,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
subIssueOperations={subIssueOperations}
|
subIssueOperations={subIssueOperations}
|
||||||
|
issueServiceType={issueServiceType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -209,7 +211,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<X className="h-3.5 w-3.5" strokeWidth={2} />
|
<X className="h-3.5 w-3.5" strokeWidth={2} />
|
||||||
<span>Remove parent issue</span>
|
<span>{`Remove ${issueServiceType === EIssueServiceType.ISSUES ? "parent" : ""} issue`}</span>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ export interface IIssueProperty {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueProperty: React.FC<IIssueProperty> = (props) => {
|
export const IssueProperty: React.FC<IIssueProperty> = (props) => {
|
||||||
const { workspaceSlug, parentIssueId, issueId, disabled, subIssueOperations } = props;
|
const { workspaceSlug, parentIssueId, issueId, disabled, subIssueOperations, issueServiceType } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail(issueServiceType);
|
||||||
|
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import update from "lodash/update";
|
||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// plane constants
|
// plane constants
|
||||||
import { EIssueLayoutTypes, ALL_ISSUES } from "@plane/constants";
|
import { EIssueLayoutTypes, ALL_ISSUES, EIssueServiceType } from "@plane/constants";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
TIssue,
|
TIssue,
|
||||||
|
|
@ -203,7 +203,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
// API Abort controller
|
// API Abort controller
|
||||||
controller: AbortController;
|
controller: AbortController;
|
||||||
|
|
||||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IBaseIssueFilterStore, isArchived = false) {
|
constructor(
|
||||||
|
_rootStore: IIssueRootStore,
|
||||||
|
issueFilterStore: IBaseIssueFilterStore,
|
||||||
|
isArchived = false,
|
||||||
|
serviceType = EIssueServiceType.ISSUES
|
||||||
|
) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observable
|
// observable
|
||||||
loader: observable,
|
loader: observable,
|
||||||
|
|
@ -257,7 +262,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
|
|
||||||
this.isArchived = isArchived;
|
this.isArchived = isArchived;
|
||||||
|
|
||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService(serviceType);
|
||||||
this.issueArchiveService = new IssueArchiveService();
|
this.issueArchiveService = new IssueArchiveService();
|
||||||
this.issueDraftService = new IssueDraftService();
|
this.issueDraftService = new IssueDraftService();
|
||||||
this.moduleService = new ModuleService();
|
this.moduleService = new ModuleService();
|
||||||
|
|
|
||||||
1
web/ee/helpers/epic-analytics.ts
Normal file
1
web/ee/helpers/epic-analytics.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/helpers/epic-analytics";
|
||||||
Loading…
Add table
Add a link
Reference in a new issue