+ {groups &&
+ groups.length > 0 &&
+ groups.map(
+ (_list: IGroupByColumn) =>
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && (
@@ -131,6 +135,7 @@ const GroupByList: React.FC = (props) => {
quickActions={quickActions}
displayProperties={displayProperties}
canEditProperties={canEditProperties}
+ containerRef={containerRef}
/>
)}
diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
index 2a97045fe..840ea39f9 100644
--- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from "react";
+import { Dispatch, MutableRefObject, SetStateAction, useRef, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// icons
@@ -7,6 +7,7 @@ import { ChevronRight, MoreHorizontal } from "lucide-react";
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
// components
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
+import RenderIfVisible from "components/core/render-if-visible-HOC";
import { IssueColumn } from "./issue-column";
// ui
import { ControlLink, Tooltip } from "@plane/ui";
@@ -32,6 +33,9 @@ interface Props {
portalElement: React.MutableRefObject;
nestingLevel: number;
issueId: string;
+ isScrolled: MutableRefObject;
+ containerRef: MutableRefObject;
+ issueIds: string[];
}
export const SpreadsheetIssueRow = observer((props: Props) => {
@@ -44,8 +48,96 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
handleIssues,
quickActions,
canEditProperties,
+ isScrolled,
+ containerRef,
+ issueIds,
} = props;
+ const [isExpanded, setExpanded] = useState(false);
+ const { subIssues: subIssuesStore } = useIssueDetail();
+
+ const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
+
+ return (
+ <>
+ {/* first column/ issue name and key column */}
+ }
+ changingReference={issueIds}
+ >
+
+
+
+ {isExpanded &&
+ subIssues &&
+ subIssues.length > 0 &&
+ subIssues.map((subIssueId: string) => (
+
+ ))}
+ >
+ );
+});
+
+interface IssueRowDetailsProps {
+ displayProperties: IIssueDisplayProperties;
+ isEstimateEnabled: boolean;
+ quickActions: (
+ issue: TIssue,
+ customActionButton?: React.ReactElement,
+ portalElement?: HTMLDivElement | null
+ ) => React.ReactNode;
+ canEditProperties: (projectId: string | undefined) => boolean;
+ handleIssues: (issue: TIssue, action: EIssueActions) => Promise;
+ portalElement: React.MutableRefObject;
+ nestingLevel: number;
+ issueId: string;
+ isScrolled: MutableRefObject;
+ isExpanded: boolean;
+ setExpanded: Dispatch>;
+}
+
+const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
+ const {
+ displayProperties,
+ issueId,
+ isEstimateEnabled,
+ nestingLevel,
+ portalElement,
+ handleIssues,
+ quickActions,
+ canEditProperties,
+ isScrolled,
+ isExpanded,
+ setExpanded,
+ } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -54,8 +146,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
const { peekIssue, setPeekIssue } = useIssueDetail();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
- const [isExpanded, setExpanded] = useState(false);
-
const menuActionRef = useRef(null);
const handleIssuePeekOverview = (issue: TIssue) => {
@@ -66,7 +156,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
- const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
const paddingLeft = `${nestingLevel * 54}px`;
@@ -91,81 +180,77 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
);
-
if (!issueDetail) return null;
const disableUserActions = !canEditProperties(issueDetail.project_id);
return (
<>
-
- {/* first column/ issue name and key column */}
-
-
-
-
-
- {getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id}
-
+
+
+
+
+ {getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id}
+
- {canEditProperties(issueDetail.project_id) && (
-
- {quickActions(issueDetail, customActionButton, portalElement.current)}
-
- )}
-
-
- {issueDetail.sub_issues_count > 0 && (
-
-
+ {canEditProperties(issueDetail.project_id) && (
+
+ {quickActions(issueDetail, customActionButton, portalElement.current)}
)}
-
- handleIssuePeekOverview(issueDetail)}
- className="clickable w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
- >
-
-
- 0 && (
+
+
-
-
-
- |
- {/* Rest of the columns */}
- {SPREADSHEET_PROPERTY_LIST.map((property) => (
+
+
+
+ )}
+
+
+ handleIssuePeekOverview(issueDetail)}
+ className="clickable w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
+ >
+
+
+
+ {issueDetail.name}
+
+
+
+
+
+ {/* Rest of the columns */}
+ {SPREADSHEET_PROPERTY_LIST.map((property) => (
{
isEstimateEnabled={isEstimateEnabled}
/>
))}
-
-
- {isExpanded &&
- subIssues &&
- subIssues.length > 0 &&
- subIssues.map((subIssueId: string) => (
-
- ))}
>
);
});
diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx
index e63b01dfb..5d45157cc 100644
--- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx
@@ -1,4 +1,5 @@
import { observer } from "mobx-react-lite";
+import { MutableRefObject, useEffect, useRef } from "react";
//types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types";
import { EIssueActions } from "../types";
@@ -21,6 +22,7 @@ type Props = {
handleIssues: (issue: TIssue, action: EIssueActions) => Promise
;
canEditProperties: (projectId: string | undefined) => boolean;
portalElement: React.MutableRefObject;
+ containerRef: MutableRefObject;
};
export const SpreadsheetTable = observer((props: Props) => {
@@ -34,8 +36,45 @@ export const SpreadsheetTable = observer((props: Props) => {
quickActions,
handleIssues,
canEditProperties,
+ containerRef,
} = props;
+ // states
+ const isScrolled = useRef(false);
+
+ const handleScroll = () => {
+ if (!containerRef.current) return;
+ const scrollLeft = containerRef.current.scrollLeft;
+
+ const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns
+ const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers
+
+ //The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly
+ if (scrollLeft > 0 !== isScrolled.current) {
+ const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child");
+
+ for (let i = 0; i < firtColumns.length; i++) {
+ const shadow = i === 0 ? headerShadow : columnShadow;
+ if (scrollLeft > 0) {
+ (firtColumns[i] as HTMLElement).style.boxShadow = shadow;
+ } else {
+ (firtColumns[i] as HTMLElement).style.boxShadow = "none";
+ }
+ }
+ isScrolled.current = scrollLeft > 0;
+ }
+ };
+
+ useEffect(() => {
+ const currentContainerRef = containerRef.current;
+
+ if (currentContainerRef) currentContainerRef.addEventListener("scroll", handleScroll);
+
+ return () => {
+ if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll);
+ };
+ }, []);
+
const handleKeyBoardNavigation = useTableKeyboardNavigation();
return (
@@ -58,6 +97,9 @@ export const SpreadsheetTable = observer((props: Props) => {
isEstimateEnabled={isEstimateEnabled}
handleIssues={handleIssues}
portalElement={portalElement}
+ containerRef={containerRef}
+ isScrolled={isScrolled}
+ issueIds={issueIds}
/>
))}
diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx
index e99b17850..1ac815ced 100644
--- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef } from "react";
+import React, { useRef } from "react";
import { observer } from "mobx-react-lite";
// components
import { Spinner } from "@plane/ui";
@@ -48,8 +48,6 @@ export const SpreadsheetView: React.FC = observer((props) => {
enableQuickCreateIssue,
disableIssueCreation,
} = props;
- // states
- const isScrolled = useRef(false);
// refs
const containerRef = useRef(null);
const portalRef = useRef(null);
@@ -58,39 +56,6 @@ export const SpreadsheetView: React.FC = observer((props) => {
const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null;
- const handleScroll = () => {
- if (!containerRef.current) return;
- const scrollLeft = containerRef.current.scrollLeft;
-
- const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns
- const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers
-
- //The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly
- if (scrollLeft > 0 !== isScrolled.current) {
- const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child");
-
- for (let i = 0; i < firtColumns.length; i++) {
- const shadow = i === 0 ? headerShadow : columnShadow;
- if (scrollLeft > 0) {
- (firtColumns[i] as HTMLElement).style.boxShadow = shadow;
- } else {
- (firtColumns[i] as HTMLElement).style.boxShadow = "none";
- }
- }
- isScrolled.current = scrollLeft > 0;
- }
- };
-
- useEffect(() => {
- const currentContainerRef = containerRef.current;
-
- if (currentContainerRef) currentContainerRef.addEventListener("scroll", handleScroll);
-
- return () => {
- if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll);
- };
- }, []);
-
if (!issueIds || issueIds.length === 0)
return (
@@ -112,6 +77,7 @@ export const SpreadsheetView: React.FC
= observer((props) => {
quickActions={quickActions}
handleIssues={handleIssues}
canEditProperties={canEditProperties}
+ containerRef={containerRef}
/>
diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx
index 83ec363b9..0c3367dc1 100644
--- a/web/components/issues/issue-layouts/utils.tsx
+++ b/web/components/issues/issue-layouts/utils.tsx
@@ -1,10 +1,10 @@
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
-import { ISSUE_PRIORITIES } from "constants/issue";
+import { EIssueListRow, ISSUE_PRIORITIES } from "constants/issue";
import { renderEmoji } from "helpers/emoji.helper";
import { IMemberRootStore } from "store/member";
import { IProjectStore } from "store/project/project.store";
import { IStateStore } from "store/state.store";
-import { GroupByColumnTypes, IGroupByColumn } from "@plane/types";
+import { GroupByColumnTypes, IGroupByColumn, IIssueListRow, TGroupedIssues, TUnGroupedIssues } from "@plane/types";
import { STATE_GROUPS } from "constants/state";
import { ILabelStore } from "store/label.store";
diff --git a/web/constants/issue.ts b/web/constants/issue.ts
index 57dff280e..5b6ce8187 100644
--- a/web/constants/issue.ts
+++ b/web/constants/issue.ts
@@ -412,6 +412,13 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
},
};
+export enum EIssueListRow {
+ HEADER = "HEADER",
+ ISSUE = "ISSUE",
+ NO_ISSUES = "NO_ISSUES",
+ QUICK_ADD = "QUICK_ADD",
+}
+
export const getValueFromObject = (object: Object, key: string): string | number | boolean | null => {
const keys = key ? key.split(".") : [];
@@ -442,4 +449,4 @@ export const groupReactionEmojis = (reactions: any) => {
}
return _groupedEmojis;
-};
+};
\ No newline at end of file
From 27037a2177c1a79da5f37eb8d2ae694512ea7de6 Mon Sep 17 00:00:00 2001
From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Date: Fri, 9 Feb 2024 15:53:54 +0530
Subject: [PATCH 07/11] feat: completed cycle snapshot (#3600)
* fix: transfer cycle old distribtion captured
* chore: active cycle snapshot
* chore: migration file changed
* chore: distribution payload changed
* chore: labels and assignee structure change
* chore: migration changes
* chore: cycle snapshot progress payload updated
* chore: cycle snapshot progress type added
* chore: snapshot progress stats updated in cycle sidebar
* chore: empty string validation
---------
Co-authored-by: Anmol Singh Bhatia
---
apiserver/plane/app/views/cycle.py | 224 +++++++++++++++++-
.../0060_cycle_progress_snapshot.py | 18 ++
apiserver/plane/db/models/cycle.py | 1 +
packages/types/src/cycles.d.ts | 18 ++
packages/ui/src/dropdowns/custom-menu.tsx | 4 -
web/components/cycles/sidebar.tsx | 152 ++++++++----
6 files changed, 370 insertions(+), 47 deletions(-)
create mode 100644 apiserver/plane/db/migrations/0060_cycle_progress_snapshot.py
diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py
index 32f593e1e..63d8d28ae 100644
--- a/apiserver/plane/app/views/cycle.py
+++ b/apiserver/plane/app/views/cycle.py
@@ -20,6 +20,7 @@ from django.core import serializers
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
+from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework.response import Response
@@ -312,6 +313,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"labels": label_distribution,
"completion_chart": {},
}
+
if data[0]["start_date"] and data[0]["end_date"]:
data[0]["distribution"][
"completion_chart"
@@ -840,10 +842,230 @@ class TransferCycleIssueEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
- new_cycle = Cycle.objects.get(
+ new_cycle = Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
+ ).first()
+
+ old_cycle = (
+ Cycle.objects.filter(
+ workspace__slug=slug, project_id=project_id, pk=cycle_id
+ )
+ .annotate(
+ total_issues=Count(
+ "issue_cycle",
+ filter=Q(
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ completed_issues=Count(
+ "issue_cycle__issue__state__group",
+ filter=Q(
+ issue_cycle__issue__state__group="completed",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ cancelled_issues=Count(
+ "issue_cycle__issue__state__group",
+ filter=Q(
+ issue_cycle__issue__state__group="cancelled",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ started_issues=Count(
+ "issue_cycle__issue__state__group",
+ filter=Q(
+ issue_cycle__issue__state__group="started",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ unstarted_issues=Count(
+ "issue_cycle__issue__state__group",
+ filter=Q(
+ issue_cycle__issue__state__group="unstarted",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ backlog_issues=Count(
+ "issue_cycle__issue__state__group",
+ filter=Q(
+ issue_cycle__issue__state__group="backlog",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ total_estimates=Sum("issue_cycle__issue__estimate_point")
+ )
+ .annotate(
+ completed_estimates=Sum(
+ "issue_cycle__issue__estimate_point",
+ filter=Q(
+ issue_cycle__issue__state__group="completed",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ started_estimates=Sum(
+ "issue_cycle__issue__estimate_point",
+ filter=Q(
+ issue_cycle__issue__state__group="started",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
)
+ # Pass the new_cycle queryset to burndown_plot
+ completion_chart = burndown_plot(
+ queryset=old_cycle.first(),
+ slug=slug,
+ project_id=project_id,
+ cycle_id=cycle_id,
+ )
+
+ assignee_distribution = (
+ Issue.objects.filter(
+ issue_cycle__cycle_id=cycle_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(display_name=F("assignees__display_name"))
+ .annotate(assignee_id=F("assignees__id"))
+ .annotate(avatar=F("assignees__avatar"))
+ .values("display_name", "assignee_id", "avatar")
+ .annotate(
+ total_issues=Count(
+ "id",
+ filter=Q(archived_at__isnull=True, is_draft=False),
+ ),
+ )
+ .annotate(
+ completed_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=False,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=True,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .order_by("display_name")
+ )
+
+ label_distribution = (
+ Issue.objects.filter(
+ issue_cycle__cycle_id=cycle_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(label_name=F("labels__name"))
+ .annotate(color=F("labels__color"))
+ .annotate(label_id=F("labels__id"))
+ .values("label_name", "color", "label_id")
+ .annotate(
+ total_issues=Count(
+ "id",
+ filter=Q(archived_at__isnull=True, is_draft=False),
+ )
+ )
+ .annotate(
+ completed_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=False,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=True,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .order_by("label_name")
+ )
+
+ assignee_distribution_data = [
+ {
+ "display_name": item["display_name"],
+ "assignee_id": str(item["assignee_id"]) if item["assignee_id"] else None,
+ "avatar": item["avatar"],
+ "total_issues": item["total_issues"],
+ "completed_issues": item["completed_issues"],
+ "pending_issues": item["pending_issues"],
+ }
+ for item in assignee_distribution
+ ]
+
+ label_distribution_data = [
+ {
+ "label_name": item["label_name"],
+ "color": item["color"],
+ "label_id": str(item["label_id"]) if item["label_id"] else None,
+ "total_issues": item["total_issues"],
+ "completed_issues": item["completed_issues"],
+ "pending_issues": item["pending_issues"],
+ }
+ for item in label_distribution
+ ]
+
+ current_cycle = Cycle.objects.filter(
+ workspace__slug=slug, project_id=project_id, pk=cycle_id
+ ).first()
+
+ current_cycle.progress_snapshot = {
+ "total_issues": old_cycle.first().total_issues,
+ "completed_issues": old_cycle.first().completed_issues,
+ "cancelled_issues": old_cycle.first().cancelled_issues,
+ "started_issues": old_cycle.first().started_issues,
+ "unstarted_issues": old_cycle.first().unstarted_issues,
+ "backlog_issues": old_cycle.first().backlog_issues,
+ "total_estimates": old_cycle.first().total_estimates,
+ "completed_estimates": old_cycle.first().completed_estimates,
+ "started_estimates": old_cycle.first().started_estimates,
+ "distribution":{
+ "labels": label_distribution_data,
+ "assignees": assignee_distribution_data,
+ "completion_chart": completion_chart,
+ },
+ }
+ current_cycle.save(update_fields=["progress_snapshot"])
+
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now().date()
diff --git a/apiserver/plane/db/migrations/0060_cycle_progress_snapshot.py b/apiserver/plane/db/migrations/0060_cycle_progress_snapshot.py
new file mode 100644
index 000000000..074e20a16
--- /dev/null
+++ b/apiserver/plane/db/migrations/0060_cycle_progress_snapshot.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.7 on 2024-02-08 09:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('db', '0059_auto_20240208_0957'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='cycle',
+ name='progress_snapshot',
+ field=models.JSONField(default=dict),
+ ),
+ ]
diff --git a/apiserver/plane/db/models/cycle.py b/apiserver/plane/db/models/cycle.py
index 5251c68ec..d802dbc1e 100644
--- a/apiserver/plane/db/models/cycle.py
+++ b/apiserver/plane/db/models/cycle.py
@@ -68,6 +68,7 @@ class Cycle(ProjectBaseModel):
sort_order = models.FloatField(default=65535)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
+ progress_snapshot = models.JSONField(default=dict)
class Meta:
verbose_name = "Cycle"
diff --git a/packages/types/src/cycles.d.ts b/packages/types/src/cycles.d.ts
index 12cbab4c6..5d715385a 100644
--- a/packages/types/src/cycles.d.ts
+++ b/packages/types/src/cycles.d.ts
@@ -31,6 +31,7 @@ export interface ICycle {
issue: string;
name: string;
owned_by: string;
+ progress_snapshot: TProgressSnapshot;
project: string;
project_detail: IProjectLite;
status: TCycleGroups;
@@ -49,6 +50,23 @@ export interface ICycle {
workspace_detail: IWorkspaceLite;
}
+export type TProgressSnapshot = {
+ backlog_issues: number;
+ cancelled_issues: number;
+ completed_estimates: number | null;
+ completed_issues: number;
+ distribution?: {
+ assignees: TAssigneesDistribution[];
+ completion_chart: TCompletionChartDistribution;
+ labels: TLabelsDistribution[];
+ };
+ started_estimates: number | null;
+ started_issues: number;
+ total_estimates: number | null;
+ total_issues: number;
+ unstarted_issues: number;
+};
+
export type TAssigneesDistribution = {
assignee_id: string | null;
avatar: string | null;
diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx
index 5dd2923a8..37aba932a 100644
--- a/packages/ui/src/dropdowns/custom-menu.tsx
+++ b/packages/ui/src/dropdowns/custom-menu.tsx
@@ -54,10 +54,6 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
setIsOpen(false);
};
- const handleOnChange = () => {
- if (closeOnSelect) closeDropdown();
- };
-
const selectActiveItem = () => {
const activeItem: HTMLElement | undefined | null = dropdownRef.current?.querySelector(
`[data-headlessui-state="active"] button`
diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx
index 299c71008..6966779b5 100644
--- a/web/components/cycles/sidebar.tsx
+++ b/web/components/cycles/sidebar.tsx
@@ -3,6 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react";
+import isEmpty from "lodash/isEmpty";
// services
import { CycleService } from "services/cycle.service";
// hooks
@@ -293,7 +294,11 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`);
const progressPercentage = cycleDetails
- ? Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100)
+ ? isCompleted
+ ? Math.round(
+ (cycleDetails.progress_snapshot.completed_issues / cycleDetails.progress_snapshot.total_issues) * 100
+ )
+ : Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100)
: null;
if (!cycleDetails)
@@ -317,7 +322,14 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const issueCount =
- cycleDetails.total_issues === 0 ? "0 Issue" : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
+ isCompleted && !isEmpty(cycleDetails.progress_snapshot)
+ ? cycleDetails.progress_snapshot.total_issues === 0
+ ? "0 Issue"
+ : `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}`
+ : cycleDetails.total_issues === 0
+ ? "0 Issue"
+ : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
+
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
@@ -568,49 +580,105 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
- {cycleDetails.distribution?.completion_chart &&
- cycleDetails.start_date &&
- cycleDetails.end_date ? (
-
-
-
-
-
-
Ideal
+ {isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? (
+ <>
+ {cycleDetails.progress_snapshot.distribution?.completion_chart &&
+ cycleDetails.start_date &&
+ cycleDetails.end_date && (
+
+
+
+
+
+ Ideal
+
+
+
+ Current
+
+
+
+
-
-
- Current
-
-
-
-
-
+ )}
+ >
) : (
- ""
+ <>
+ {cycleDetails.distribution?.completion_chart &&
+ cycleDetails.start_date &&
+ cycleDetails.end_date && (
+
+
+
+
+
+ Ideal
+
+
+
+ Current
+
+
+
+
+
+ )}
+ >
)}
- {cycleDetails.total_issues > 0 && cycleDetails.distribution && (
-
-
-
+ {/* stats */}
+ {isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? (
+ <>
+ {cycleDetails.progress_snapshot.total_issues > 0 &&
+ cycleDetails.progress_snapshot.distribution && (
+
+
+
+ )}
+ >
+ ) : (
+ <>
+ {cycleDetails.total_issues > 0 && cycleDetails.distribution && (
+
+
+
+ )}
+ >
)}
From 8d730e66804978430451b1fd15b180facc02be03 Mon Sep 17 00:00:00 2001
From: rahulramesha <71900764+rahulramesha@users.noreply.github.com>
Date: Fri, 9 Feb 2024 16:14:08 +0530
Subject: [PATCH 08/11] fix: spreadsheet date validation and sorting (#3607)
* fix validation for start and end date in spreadsheet layout
* revamp logic for sorting in all fields
---
.../spreadsheet/columns/due-date-column.tsx | 1 +
.../spreadsheet/columns/start-date-column.tsx | 1 +
web/store/issue/helpers/issue-helper.store.ts | 165 ++++++++++++++----
web/store/issue/root.store.ts | 39 +++--
web/store/member/workspace-member.store.ts | 8 +
5 files changed, 160 insertions(+), 54 deletions(-)
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx
index c5674cee9..98262b504 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx
@@ -21,6 +21,7 @@ export const SpreadsheetDueDateColumn: React.FC
= observer((props: Props)
{
const targetDate = data ? renderFormattedPayloadDate(data) : null;
onChange(
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx
index fcbd817b6..82c00fc12 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx
@@ -21,6 +21,7 @@ export const SpreadsheetStartDateColumn: React.FC = observer((props: Prop
{
const startDate = data ? renderFormattedPayloadDate(data) : null;
onChange(
diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts
index 5fdf0df82..ff5dba9dd 100644
--- a/web/store/issue/helpers/issue-helper.store.ts
+++ b/web/store/issue/helpers/issue-helper.store.ts
@@ -1,7 +1,7 @@
-import sortBy from "lodash/sortBy";
+import orderBy from "lodash/orderBy";
import get from "lodash/get";
import indexOf from "lodash/indexOf";
-import reverse from "lodash/reverse";
+import isEmpty from "lodash/isEmpty";
import values from "lodash/values";
// types
import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
@@ -144,98 +144,189 @@ export class IssueHelperStore implements TIssueHelperStore {
issueDisplayFiltersDefaultData = (groupBy: string | null): string[] => {
switch (groupBy) {
case "state":
- return this.rootStore?.states || [];
+ return Object.keys(this.rootStore?.stateMap || {});
case "state_detail.group":
return Object.keys(STATE_GROUPS);
case "priority":
return ISSUE_PRIORITIES.map((i) => i.key);
case "labels":
- return this.rootStore?.labels || [];
+ return Object.keys(this.rootStore?.labelMap || {});
case "created_by":
- return this.rootStore?.members || [];
+ return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {});
case "assignees":
- return this.rootStore?.members || [];
+ return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {});
case "project":
- return this.rootStore?.projects || [];
+ return Object.keys(this.rootStore?.projectMap || {});
default:
return [];
}
};
+ /**
+ * This Method is used to get data of the issue based on the ids of the data for states, labels adn assignees
+ * @param dataType what type of data is being sent
+ * @param dataIds id/ids of the data that is to be populated
+ * @param order ascending or descending for arrays of data
+ * @returns string | string[] of sortable fields to be used for sorting
+ */
+ populateIssueDataForSorting(
+ dataType: "state_id" | "label_ids" | "assignee_ids",
+ dataIds: string | string[] | null | undefined,
+ order?: "asc" | "desc"
+ ) {
+ if (!dataIds) return;
+
+ const dataValues: string[] = [];
+ const isDataIdsArray = Array.isArray(dataIds);
+ const dataIdsArray = isDataIdsArray ? dataIds : [dataIds];
+
+ switch (dataType) {
+ case "state_id":
+ const stateMap = this.rootStore?.stateMap;
+ if (!stateMap) break;
+ for (const dataId of dataIdsArray) {
+ const state = stateMap[dataId];
+ if (state && state.name) dataValues.push(state.name.toLocaleLowerCase());
+ }
+ break;
+ case "label_ids":
+ const labelMap = this.rootStore?.labelMap;
+ if (!labelMap) break;
+ for (const dataId of dataIdsArray) {
+ const label = labelMap[dataId];
+ if (label && label.name) dataValues.push(label.name.toLocaleLowerCase());
+ }
+ break;
+ case "assignee_ids":
+ const memberMap = this.rootStore?.memberMap;
+ if (!memberMap) break;
+ for (const dataId of dataIdsArray) {
+ const member = memberMap[dataId];
+ if (memberMap && member.first_name) dataValues.push(member.first_name.toLocaleLowerCase());
+ }
+ break;
+ }
+
+ return isDataIdsArray ? (order ? orderBy(dataValues, undefined, [order]) : dataValues) : dataValues[0];
+ }
+
+ /**
+ * This Method is mainly used to filter out empty values in the begining
+ * @param key key of the value that is to be checked if empty
+ * @param object any object in which the key's value is to be checked
+ * @returns 1 if emoty, 0 if not empty
+ */
+ getSortOrderToFilterEmptyValues(key: string, object: any) {
+ const value = object?.[key];
+
+ if (typeof value !== "number" && isEmpty(value)) return 1;
+
+ return 0;
+ }
+
issuesSortWithOrderBy = (issueObject: TIssueMap, key: Partial): TIssue[] => {
let array = values(issueObject);
- array = reverse(sortBy(array, "created_at"));
+ array = orderBy(array, "created_at");
+
switch (key) {
case "sort_order":
- return sortBy(array, "sort_order");
-
+ return orderBy(array, "sort_order");
case "state__name":
- return reverse(sortBy(array, "state"));
+ return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]));
case "-state__name":
- return sortBy(array, "state");
-
+ return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]), ["desc"]);
// dates
case "created_at":
- return sortBy(array, "created_at");
+ return orderBy(array, "created_at");
case "-created_at":
- return reverse(sortBy(array, "created_at"));
-
+ return orderBy(array, "created_at", ["desc"]);
case "updated_at":
- return sortBy(array, "updated_at");
+ return orderBy(array, "updated_at");
case "-updated_at":
- return reverse(sortBy(array, "updated_at"));
-
+ return orderBy(array, "updated_at", ["desc"]);
case "start_date":
- return sortBy(array, "start_date");
+ return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"]); //preferring sorting based on empty values to always keep the empty values below
case "-start_date":
- return reverse(sortBy(array, "start_date"));
+ return orderBy(
+ array,
+ [this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"], //preferring sorting based on empty values to always keep the empty values below
+ ["asc", "desc"]
+ );
case "target_date":
- return sortBy(array, "target_date");
+ return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"]); //preferring sorting based on empty values to always keep the empty values below
case "-target_date":
- return reverse(sortBy(array, "target_date"));
+ return orderBy(
+ array,
+ [this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"], //preferring sorting based on empty values to always keep the empty values below
+ ["asc", "desc"]
+ );
// custom
case "priority": {
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
- return reverse(sortBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority)));
+ return orderBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority), ["desc"]);
}
case "-priority": {
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
- return sortBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority));
+ return orderBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority));
}
// number
case "attachment_count":
- return sortBy(array, "attachment_count");
+ return orderBy(array, "attachment_count");
case "-attachment_count":
- return reverse(sortBy(array, "attachment_count"));
+ return orderBy(array, "attachment_count", ["desc"]);
case "estimate_point":
- return sortBy(array, "estimate_point");
+ return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"]); //preferring sorting based on empty values to always keep the empty values below
case "-estimate_point":
- return reverse(sortBy(array, "estimate_point"));
+ return orderBy(
+ array,
+ [this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"], //preferring sorting based on empty values to always keep the empty values below
+ ["asc", "desc"]
+ );
case "link_count":
- return sortBy(array, "link_count");
+ return orderBy(array, "link_count");
case "-link_count":
- return reverse(sortBy(array, "link_count"));
+ return orderBy(array, "link_count", ["desc"]);
case "sub_issues_count":
- return sortBy(array, "sub_issues_count");
+ return orderBy(array, "sub_issues_count");
case "-sub_issues_count":
- return reverse(sortBy(array, "sub_issues_count"));
+ return orderBy(array, "sub_issues_count", ["desc"]);
// Array
case "labels__name":
- return reverse(sortBy(array, "labels"));
+ return orderBy(array, [
+ this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
+ (issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "asc"),
+ ]);
case "-labels__name":
- return sortBy(array, "labels");
+ return orderBy(
+ array,
+ [
+ this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
+ (issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "desc"),
+ ],
+ ["asc", "desc"]
+ );
case "assignees__first_name":
- return reverse(sortBy(array, "assignees"));
+ return orderBy(array, [
+ this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
+ (issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "asc"),
+ ]);
case "-assignees__first_name":
- return sortBy(array, "assignees");
+ return orderBy(
+ array,
+ [
+ this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
+ (issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "desc"),
+ ],
+ ["asc", "desc"]
+ );
default:
return array;
diff --git a/web/store/issue/root.store.ts b/web/store/issue/root.store.ts
index b2425757c..ee2e6d84d 100644
--- a/web/store/issue/root.store.ts
+++ b/web/store/issue/root.store.ts
@@ -4,7 +4,7 @@ import isEmpty from "lodash/isEmpty";
import { RootStore } from "../root.store";
import { IStateStore, StateStore } from "../state.store";
// issues data store
-import { IState } from "@plane/types";
+import { IIssueLabel, IProject, IState, IUserLite } from "@plane/types";
import { IIssueStore, IssueStore } from "./issue.store";
import { IIssueDetail, IssueDetail } from "./issue-details/root.store";
import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace";
@@ -22,6 +22,7 @@ import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedI
import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft";
import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store";
import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store";
+import { IWorkspaceMembership } from "store/member/workspace-member.store";
export interface IIssueRootStore {
currentUserId: string | undefined;
@@ -32,11 +33,12 @@ export interface IIssueRootStore {
viewId: string | undefined;
globalViewId: string | undefined; // all issues view id
userId: string | undefined; // user profile detail Id
- states: string[] | undefined;
+ stateMap: Record | undefined;
stateDetails: IState[] | undefined;
- labels: string[] | undefined;
- members: string[] | undefined;
- projects: string[] | undefined;
+ labelMap: Record | undefined;
+ workSpaceMemberRolesMap: Record | undefined;
+ memberMap: Record | undefined;
+ projectMap: Record | undefined;
rootStore: RootStore;
@@ -83,11 +85,12 @@ export class IssueRootStore implements IIssueRootStore {
viewId: string | undefined = undefined;
globalViewId: string | undefined = undefined;
userId: string | undefined = undefined;
- states: string[] | undefined = undefined;
+ stateMap: Record | undefined = undefined;
stateDetails: IState[] | undefined = undefined;
- labels: string[] | undefined = undefined;
- members: string[] | undefined = undefined;
- projects: string[] | undefined = undefined;
+ labelMap: Record | undefined = undefined;
+ workSpaceMemberRolesMap: Record | undefined = undefined;
+ memberMap: Record | undefined = undefined;
+ projectMap: Record | undefined = undefined;
rootStore: RootStore;
@@ -133,11 +136,12 @@ export class IssueRootStore implements IIssueRootStore {
viewId: observable.ref,
userId: observable.ref,
globalViewId: observable.ref,
- states: observable,
+ stateMap: observable,
stateDetails: observable,
- labels: observable,
- members: observable,
- projects: observable,
+ labelMap: observable,
+ memberMap: observable,
+ workSpaceMemberRolesMap: observable,
+ projectMap: observable,
});
this.rootStore = rootStore;
@@ -151,13 +155,14 @@ export class IssueRootStore implements IIssueRootStore {
if (rootStore.app.router.viewId) this.viewId = rootStore.app.router.viewId;
if (rootStore.app.router.globalViewId) this.globalViewId = rootStore.app.router.globalViewId;
if (rootStore.app.router.userId) this.userId = rootStore.app.router.userId;
- if (!isEmpty(rootStore?.state?.stateMap)) this.states = Object.keys(rootStore?.state?.stateMap);
+ if (!isEmpty(rootStore?.state?.stateMap)) this.stateMap = rootStore?.state?.stateMap;
if (!isEmpty(rootStore?.state?.projectStates)) this.stateDetails = rootStore?.state?.projectStates;
- if (!isEmpty(rootStore?.label?.labelMap)) this.labels = Object.keys(rootStore?.label?.labelMap);
+ if (!isEmpty(rootStore?.label?.labelMap)) this.labelMap = rootStore?.label?.labelMap;
if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap))
- this.members = Object.keys(rootStore?.memberRoot?.workspace?.workspaceMemberMap);
+ this.workSpaceMemberRolesMap = rootStore?.memberRoot?.workspace?.memberMap || undefined;
+ if (!isEmpty(rootStore?.memberRoot?.memberMap)) this.memberMap = rootStore?.memberRoot?.memberMap || undefined;
if (!isEmpty(rootStore?.projectRoot?.project?.projectMap))
- this.projects = Object.keys(rootStore?.projectRoot?.project?.projectMap);
+ this.projectMap = rootStore?.projectRoot?.project?.projectMap;
});
this.issues = new IssueStore();
diff --git a/web/store/member/workspace-member.store.ts b/web/store/member/workspace-member.store.ts
index ff65d0eb9..1dae25bd4 100644
--- a/web/store/member/workspace-member.store.ts
+++ b/web/store/member/workspace-member.store.ts
@@ -26,6 +26,7 @@ export interface IWorkspaceMemberStore {
// computed
workspaceMemberIds: string[] | null;
workspaceMemberInvitationIds: string[] | null;
+ memberMap: Record | null;
// computed actions
getSearchedWorkspaceMemberIds: (searchQuery: string) => string[] | null;
getSearchedWorkspaceInvitationIds: (searchQuery: string) => string[] | null;
@@ -68,6 +69,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
// computed
workspaceMemberIds: computed,
workspaceMemberInvitationIds: computed,
+ memberMap: computed,
// actions
fetchWorkspaceMembers: action,
updateMember: action,
@@ -100,6 +102,12 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
return memberIds;
}
+ get memberMap() {
+ const workspaceSlug = this.routerStore.workspaceSlug;
+ if (!workspaceSlug) return null;
+ return this.workspaceMemberMap?.[workspaceSlug] ?? {};
+ }
+
get workspaceMemberInvitationIds() {
const workspaceSlug = this.routerStore.workspaceSlug;
if (!workspaceSlug) return null;
From be5d1eb9f96c066d9c949cd26d5f13a391505165 Mon Sep 17 00:00:00 2001
From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
Date: Fri, 9 Feb 2024 16:17:39 +0530
Subject: [PATCH 09/11] fix: notification popover responsiveness (#3602)
* fix: notification popover responsiveness
* fix: build errors
* fix: typo
---
.../notifications/notification-card.tsx | 289 ++++++++++++------
.../notifications/notification-header.tsx | 20 +-
.../notifications/notification-popover.tsx | 273 +++++++++--------
.../select-snooze-till-modal.tsx | 13 +-
4 files changed, 360 insertions(+), 235 deletions(-)
diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx
index 7a372c5d8..e709bbca3 100644
--- a/web/components/notifications/notification-card.tsx
+++ b/web/components/notifications/notification-card.tsx
@@ -1,7 +1,7 @@
-import React from "react";
+import React, { useEffect, useRef } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
-import { ArchiveRestore, Clock, MessageSquare, User2 } from "lucide-react";
+import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react";
import Link from "next/link";
// hooks
import useToast from "hooks/use-toast";
@@ -14,6 +14,7 @@ import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
// type
import type { IUserNotification } from "@plane/types";
+import { Menu } from "@headlessui/react";
type NotificationCardProps = {
notification: IUserNotification;
@@ -40,8 +41,73 @@ export const NotificationCard: React.FC = (props) => {
const router = useRouter();
const { workspaceSlug } = router.query;
-
+ // states
+ const [showSnoozeOptions, setshowSnoozeOptions] = React.useState(false);
+ // toast alert
const { setToastAlert } = useToast();
+ // refs
+ const snoozeRef = useRef(null);
+
+ const moreOptions = [
+ {
+ id: 1,
+ name: notification.read_at ? "Mark as unread" : "Mark as read",
+ icon: ,
+ onClick: () => {
+ markNotificationReadStatusToggle(notification.id).then(() => {
+ setToastAlert({
+ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
+ type: "success",
+ });
+ });
+ },
+ },
+ {
+ id: 2,
+ name: notification.archived_at ? "Unarchive" : "Archive",
+ icon: notification.archived_at ? (
+
+ ) : (
+
+ ),
+ onClick: () => {
+ markNotificationArchivedStatus(notification.id).then(() => {
+ setToastAlert({
+ title: notification.archived_at ? "Notification un-archived" : "Notification archived",
+ type: "success",
+ });
+ });
+ },
+ },
+ ];
+
+ const snoozeOptionOnClick = (date: Date | null) => {
+ if (!date) {
+ setSelectedNotificationForSnooze(notification.id);
+ return;
+ }
+ markSnoozeNotification(notification.id, date).then(() => {
+ setToastAlert({
+ title: `Notification snoozed till ${renderFormattedDate(date)}`,
+ type: "success",
+ });
+ });
+ };
+
+ // close snooze options on outside click
+ useEffect(() => {
+ const handleClickOutside = (event: any) => {
+ if (snoozeRef.current && !snoozeRef.current?.contains(event.target)) {
+ setshowSnoozeOptions(false);
+ }
+ };
+ document.addEventListener("mousedown", handleClickOutside, true);
+ document.addEventListener("touchend", handleClickOutside, true);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside, true);
+ document.removeEventListener("touchend", handleClickOutside, true);
+ };
+ }, []);
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
@@ -87,57 +153,136 @@ export const NotificationCard: React.FC = (props) => {
)}
- {!notification.message ? (
-
-
- {notification.triggered_by_details.is_bot
- ? notification.triggered_by_details.first_name
- : notification.triggered_by_details.display_name}{" "}
-
- {notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.verb}{" "}
- {notification.data.issue_activity.field === "comment"
- ? "commented"
- : notification.data.issue_activity.field === "None"
- ? null
- : replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
- {notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.field !== "None"
- ? "to"
- : ""}
-
- {" "}
- {notification.data.issue_activity.field !== "None" ? (
- notification.data.issue_activity.field !== "comment" ? (
- notification.data.issue_activity.field === "target_date" ? (
- renderFormattedDate(notification.data.issue_activity.new_value)
- ) : notification.data.issue_activity.field === "attachment" ? (
- "the issue"
- ) : notification.data.issue_activity.field === "description" ? (
- stripAndTruncateHTML(notification.data.issue_activity.new_value, 55)
+
+ {!notification.message ? (
+
+
+ {notification.triggered_by_details.is_bot
+ ? notification.triggered_by_details.first_name
+ : notification.triggered_by_details.display_name}{" "}
+
+ {notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.verb}{" "}
+ {notification.data.issue_activity.field === "comment"
+ ? "commented"
+ : notification.data.issue_activity.field === "None"
+ ? null
+ : replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
+ {notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.field !== "None"
+ ? "to"
+ : ""}
+
+ {" "}
+ {notification.data.issue_activity.field !== "None" ? (
+ notification.data.issue_activity.field !== "comment" ? (
+ notification.data.issue_activity.field === "target_date" ? (
+ renderFormattedDate(notification.data.issue_activity.new_value)
+ ) : notification.data.issue_activity.field === "attachment" ? (
+ "the issue"
+ ) : notification.data.issue_activity.field === "description" ? (
+ stripAndTruncateHTML(notification.data.issue_activity.new_value, 55)
+ ) : (
+ notification.data.issue_activity.new_value
+ )
) : (
- notification.data.issue_activity.new_value
+
+ {`"`}
+ {notification.data.issue_activity.new_value.length > 55
+ ? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
+ : notification.data.issue_activity.issue_comment}
+ {`"`}
+
)
) : (
-
- {`"`}
- {notification.data.issue_activity.new_value.length > 55
- ? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
- : notification.data.issue_activity.issue_comment}
- {`"`}
-
- )
- ) : (
- "the issue and assigned it to you."
+ "the issue and assigned it to you."
+ )}
+
+
+ ) : (
+
+ {notification.message}
+
+ )}
+
+
+ {showSnoozeOptions && (
+
+ {snoozeOptions.map((item) => (
+
{
+ e.stopPropagation();
+ e.preventDefault();
+ setshowSnoozeOptions(false);
+ snoozeOptionOnClick(item.value);
+ }}
+ >
+ {item.label}
+
+ ))}
+
+ )}
- ) : (
-
- {notification.message}
-
- )}
+
-
+
{truncateText(
`${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
50
@@ -152,43 +297,12 @@ export const NotificationCard: React.FC = (props) => {
) : (
-
{calculateTimeAgo(notification.created_at)}
+
{calculateTimeAgo(notification.created_at)}
)}
-
- {[
- {
- id: 1,
- name: notification.read_at ? "Mark as unread" : "Mark as read",
- icon:
,
- onClick: () => {
- markNotificationReadStatusToggle(notification.id).then(() => {
- setToastAlert({
- title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
- type: "success",
- });
- });
- },
- },
- {
- id: 2,
- name: notification.archived_at ? "Unarchive" : "Archive",
- icon: notification.archived_at ? (
-
- ) : (
-
- ),
- onClick: () => {
- markNotificationArchivedStatus(notification.id).then(() => {
- setToastAlert({
- title: notification.archived_at ? "Notification un-archived" : "Notification archived",
- type: "success",
- });
- });
- },
- },
- ].map((item) => (
+
+ {moreOptions.map((item) => (
diff --git a/web/components/notifications/notification-popover.tsx b/web/components/notifications/notification-popover.tsx
index 4b55ea4cb..47fdae6ef 100644
--- a/web/components/notifications/notification-popover.tsx
+++ b/web/components/notifications/notification-popover.tsx
@@ -5,6 +5,7 @@ import { observer } from "mobx-react-lite";
// hooks
import { useApplication } from "hooks/store";
import useUserNotification from "hooks/use-user-notifications";
+import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { EmptyState } from "components/common";
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
@@ -15,8 +16,12 @@ import emptyNotification from "public/empty-state/notification.svg";
import { getNumberCount } from "helpers/string.helper";
export const NotificationPopover = observer(() => {
+ // states
+ const [isActive, setIsActive] = React.useState(false);
// store hooks
const { theme: themeStore } = useApplication();
+ // refs
+ const notificationPopoverRef = React.useRef
(null);
const {
notifications,
@@ -44,8 +49,11 @@ export const NotificationPopover = observer(() => {
setFetchNotifications,
markAllNotificationsAsRead,
} = useUserNotification();
-
const isSidebarCollapsed = themeStore.sidebarCollapsed;
+ useOutsideClickDetector(notificationPopoverRef, () => {
+ // if snooze modal is open, then don't close the popover
+ if (selectedNotificationForSnooze === null) setIsActive(false);
+ });
return (
<>
@@ -54,141 +62,142 @@ export const NotificationPopover = observer(() => {
onClose={() => setSelectedNotificationForSnooze(null)}
onSubmit={markSnoozeNotification}
notification={notifications?.find((notification) => notification.id === selectedNotificationForSnooze) || null}
- onSuccess={() => {
- setSelectedNotificationForSnooze(null);
- }}
+ onSuccess={() => setSelectedNotificationForSnooze(null)}
/>
-
- {({ open: isActive, close: closePopover }) => {
- if (isActive) setFetchNotifications(true);
+
+ <>
+
+ {
+ if (window.innerWidth < 768) themeStore.toggleSidebar();
+ if (!isActive) setFetchNotifications(true);
+ setIsActive(!isActive);
+ }}
+ >
+
+ {isSidebarCollapsed ? null : Notifications}
+ {totalNotificationCount && totalNotificationCount > 0 ? (
+ isSidebarCollapsed ? (
+
+ ) : (
+
+ {getNumberCount(totalNotificationCount)}
+
+ )
+ ) : null}
+
+
+
+
+ setIsActive(false)}
+ isRefreshing={isRefreshing}
+ snoozed={snoozed}
+ archived={archived}
+ readNotification={readNotification}
+ selectedTab={selectedTab}
+ setSnoozed={setSnoozed}
+ setArchived={setArchived}
+ setReadNotification={setReadNotification}
+ setSelectedTab={setSelectedTab}
+ markAllNotificationsAsRead={markAllNotificationsAsRead}
+ />
- return (
- <>
-
-
-
- {isSidebarCollapsed ? null : Notifications}
- {totalNotificationCount && totalNotificationCount > 0 ? (
- isSidebarCollapsed ? (
-
- ) : (
-
- {getNumberCount(totalNotificationCount)}
-
- )
- ) : null}
-
-
-
-
-
-
- {notifications ? (
- notifications.length > 0 ? (
-
-
- {notifications.map((notification) => (
-
- ))}
-
- {isLoadingMore && (
-
-
-
Loading notifications
-
- )}
- {hasMore && !isLoadingMore && (
-
{
- setSize((prev) => prev + 1);
- }}
- >
- Load More
-
- )}
-
- ) : (
-
-
0 ? (
+
+
+ {notifications.map((notification) => (
+ setIsActive(false)}
+ notification={notification}
+ markNotificationArchivedStatus={markNotificationArchivedStatus}
+ markNotificationReadStatus={markNotificationAsRead}
+ markNotificationReadStatusToggle={markNotificationReadStatus}
+ setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
+ markSnoozeNotification={markSnoozeNotification}
/>
+ ))}
+
+ {isLoadingMore && (
+
+
+
Loading notifications
- )
- ) : (
-
-
-
-
-
-
-
- )}
-
-
- >
- );
- }}
+ )}
+ {hasMore && !isLoadingMore && (
+
{
+ setSize((prev) => prev + 1);
+ }}
+ >
+ Load More
+
+ )}
+
+ ) : (
+
+
+
+ )
+ ) : (
+
+
+
+
+
+
+
+ )}
+
+
+ >
>
);
diff --git a/web/components/notifications/select-snooze-till-modal.tsx b/web/components/notifications/select-snooze-till-modal.tsx
index ab3497bb8..2ad4b0ef2 100644
--- a/web/components/notifications/select-snooze-till-modal.tsx
+++ b/web/components/notifications/select-snooze-till-modal.tsx
@@ -109,7 +109,12 @@ export const SnoozeNotificationModal: FC = (props) => {
};
const handleClose = () => {
- onClose();
+ // This is a workaround to fix the issue of the Notification popover modal close on closing this modal
+ const closeTimeout = setTimeout(() => {
+ onClose();
+ clearTimeout(closeTimeout);
+ }, 50);
+
const timeout = setTimeout(() => {
reset({ ...defaultValues });
clearTimeout(timeout);
@@ -142,7 +147,7 @@ export const SnoozeNotificationModal: FC = (props) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
-
+
-
-
+
+
Pick a date
Date: Fri, 9 Feb 2024 16:22:08 +0530
Subject: [PATCH 10/11] chore: added sign-up/in, onboarding, dashboard,
all-issues related events (#3595)
* chore: added event constants
* chore: added workspace events
* chore: added workspace group for events
* chore: member invitation event added
* chore: added project pages related events.
* fix: member integer role to string
* chore: added sign-up & sign-in events
* chore: added global-view related events
* chore: added notification related events
* chore: project, cycle property change added
* chore: cycle favourite, and change-properties added
* chore: module davorite, and sidebar property changes added
* fix: build errors
* chore: all events defined in constants
---
apiserver/plane/app/views/auth_extended.py | 12 +-
apiserver/plane/app/views/authentication.py | 8 +-
apiserver/plane/app/views/oauth.py | 4 +-
.../sign-in-forms/optional-set-password.tsx | 27 +++-
.../account/sign-in-forms/password.tsx | 16 +-
web/components/account/sign-in-forms/root.tsx | 11 +-
.../account/sign-in-forms/unique-code.tsx | 18 ++-
.../sign-up-forms/optional-set-password.tsx | 27 +++-
web/components/account/sign-up-forms/root.tsx | 11 +-
.../account/sign-up-forms/unique-code.tsx | 19 ++-
web/components/cycles/cycles-board-card.tsx | 47 ++++--
web/components/cycles/cycles-list-item.tsx | 59 ++++---
web/components/cycles/delete-modal.tsx | 6 +-
web/components/cycles/form.tsx | 6 +-
web/components/cycles/modal.tsx | 23 ++-
web/components/cycles/sidebar.tsx | 73 ++++++---
web/components/headers/pages.tsx | 13 +-
.../headers/workspace-dashboard.tsx | 21 ++-
web/components/inbox/inbox-issue-actions.tsx | 17 +-
.../inbox/modals/create-issue-modal.tsx | 17 +-
web/components/issues/attachment/root.tsx | 8 +-
web/components/issues/issue-detail/root.tsx | 25 +--
.../calendar/quick-add-issue-form.tsx | 6 +-
.../roots/global-view-root.tsx | 12 +-
.../gantt/quick-add-issue-form.tsx | 6 +-
.../issue-layouts/kanban/base-kanban-root.tsx | 3 +-
.../kanban/quick-add-issue-form.tsx | 6 +-
.../list/quick-add-issue-form.tsx | 6 +-
.../properties/all-properties.tsx | 16 +-
.../spreadsheet/quick-add-issue-form.tsx | 6 +-
web/components/issues/issue-modal/modal.tsx | 30 +---
web/components/issues/peek-overview/root.tsx | 25 +--
.../modules/delete-module-modal.tsx | 6 +-
web/components/modules/form.tsx | 6 +-
web/components/modules/modal.tsx | 18 ++-
web/components/modules/module-card-item.tsx | 47 ++++--
web/components/modules/module-list-item.tsx | 43 +++--
web/components/modules/sidebar.tsx | 33 +++-
.../notifications/notification-card.tsx | 29 +++-
.../notifications/notification-header.tsx | 21 ++-
.../notifications/notification-popover.tsx | 11 +-
web/components/onboarding/invitations.tsx | 23 ++-
web/components/onboarding/invite-members.tsx | 28 +++-
web/components/onboarding/tour/root.tsx | 19 ++-
web/components/onboarding/user-details.tsx | 20 ++-
web/components/onboarding/workspace.tsx | 35 +++-
.../page-views/workspace-dashboard.tsx | 6 +-
.../pages/create-update-page-modal.tsx | 34 +++-
web/components/pages/delete-page-modal.tsx | 19 ++-
.../project/create-project-modal.tsx | 17 +-
.../project/delete-project-modal.tsx | 16 +-
web/components/project/form.tsx | 26 ++-
.../project/leave-project-modal.tsx | 8 +-
web/components/project/member-list-item.tsx | 9 +-
.../project/send-project-invitation-modal.tsx | 43 +++--
.../project/settings/features-list.tsx | 10 +-
.../states/create-update-state-inline.tsx | 42 +++--
web/components/states/delete-state-modal.tsx | 20 ++-
.../workspace/create-workspace-form.tsx | 26 +--
.../workspace/delete-workspace-modal.tsx | 23 ++-
.../workspace/settings/members-list-item.tsx | 12 +-
.../workspace/settings/workspace-details.tsx | 21 ++-
web/components/workspace/sidebar-dropdown.tsx | 1 -
web/components/workspace/sidebar-menu.tsx | 25 +--
.../workspace/views/delete-view-modal.tsx | 21 ++-
web/components/workspace/views/header.tsx | 11 +-
web/components/workspace/views/modal.tsx | 38 ++++-
.../workspace/views/view-list-item.tsx | 4 +-
web/constants/event-tracker.ts | 139 ++++++++++++++--
web/lib/app-provider.tsx | 4 +-
web/lib/posthog-provider.tsx | 23 ++-
web/package.json | 2 +-
.../projects/[projectId]/pages/index.tsx | 8 +-
.../[workspaceSlug]/settings/members.tsx | 36 +++--
web/pages/accounts/forgot-password.tsx | 16 +-
web/pages/accounts/reset-password.tsx | 21 ++-
web/pages/invitations/index.tsx | 21 ++-
web/pages/onboarding/index.tsx | 4 +-
web/store/event-tracker.store.ts | 152 +++++++++++++-----
web/store/user/index.ts | 2 +
80 files changed, 1276 insertions(+), 507 deletions(-)
diff --git a/apiserver/plane/app/views/auth_extended.py b/apiserver/plane/app/views/auth_extended.py
index 501f47657..29cb43e38 100644
--- a/apiserver/plane/app/views/auth_extended.py
+++ b/apiserver/plane/app/views/auth_extended.py
@@ -401,8 +401,8 @@ class EmailCheckEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
- event_name="SIGN_IN",
- medium="MAGIC_LINK",
+ event_name="Sign up",
+ medium="Magic link",
first_time=True,
)
key, token, current_attempt = generate_magic_token(email=email)
@@ -438,8 +438,8 @@ class EmailCheckEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
- event_name="SIGN_IN",
- medium="MAGIC_LINK",
+ event_name="Sign in",
+ medium="Magic link",
first_time=False,
)
@@ -468,8 +468,8 @@ class EmailCheckEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
- event_name="SIGN_IN",
- medium="EMAIL",
+ event_name="Sign in",
+ medium="Email",
first_time=False,
)
diff --git a/apiserver/plane/app/views/authentication.py b/apiserver/plane/app/views/authentication.py
index a41200d61..c2b3e0b7e 100644
--- a/apiserver/plane/app/views/authentication.py
+++ b/apiserver/plane/app/views/authentication.py
@@ -274,8 +274,8 @@ class SignInEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
- event_name="SIGN_IN",
- medium="EMAIL",
+ event_name="Sign in",
+ medium="Email",
first_time=False,
)
@@ -349,8 +349,8 @@ class MagicSignInEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
- event_name="SIGN_IN",
- medium="MAGIC_LINK",
+ event_name="Sign in",
+ medium="Magic link",
first_time=False,
)
diff --git a/apiserver/plane/app/views/oauth.py b/apiserver/plane/app/views/oauth.py
index de90e4337..8152fb0ee 100644
--- a/apiserver/plane/app/views/oauth.py
+++ b/apiserver/plane/app/views/oauth.py
@@ -296,7 +296,7 @@ class OauthEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
- event_name="SIGN_IN",
+ event_name="Sign in",
medium=medium.upper(),
first_time=False,
)
@@ -427,7 +427,7 @@ class OauthEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
- event_name="SIGN_IN",
+ event_name="Sign up",
medium=medium.upper(),
first_time=True,
)
diff --git a/web/components/account/sign-in-forms/optional-set-password.tsx b/web/components/account/sign-in-forms/optional-set-password.tsx
index d7a595298..1ea5ca792 100644
--- a/web/components/account/sign-in-forms/optional-set-password.tsx
+++ b/web/components/account/sign-in-forms/optional-set-password.tsx
@@ -4,12 +4,14 @@ import { Controller, useForm } from "react-hook-form";
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
+import { useEventTracker } from "hooks/store";
// ui
import { Button, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// icons
import { Eye, EyeOff } from "lucide-react";
+import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED } from "constants/event-tracker";
type Props = {
email: string;
@@ -34,6 +36,8 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => {
// states
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
const [showPassword, setShowPassword] = useState(false);
+ // store hooks
+ const { captureEvent } = useEventTracker();
// toast alert
const { setToastAlert } = useToast();
// form info
@@ -63,21 +67,34 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => {
title: "Success!",
message: "Password created successfully.",
});
+ captureEvent(PASSWORD_CREATE_SELECTED, {
+ state: "SUCCESS",
+ first_time: false,
+ });
await handleSignInRedirection();
})
- .catch((err) =>
+ .catch((err) => {
+ captureEvent(PASSWORD_CREATE_SELECTED, {
+ state: "FAILED",
+ first_time: false,
+ });
setToastAlert({
type: "error",
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
- })
- );
+ });
+ });
};
const handleGoToWorkspace = async () => {
setIsGoingToWorkspace(true);
-
- await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false));
+ await handleSignInRedirection().finally(() => {
+ captureEvent(PASSWORD_CREATE_SKIPPED, {
+ state: "SUCCESS",
+ first_time: false,
+ });
+ setIsGoingToWorkspace(false);
+ });
};
return (
diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx
index fe20d5b10..98719df63 100644
--- a/web/components/account/sign-in-forms/password.tsx
+++ b/web/components/account/sign-in-forms/password.tsx
@@ -7,7 +7,7 @@ import { Eye, EyeOff, XCircle } from "lucide-react";
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
-import { useApplication } from "hooks/store";
+import { useApplication, useEventTracker } from "hooks/store";
// components
import { ESignInSteps, ForgotPasswordPopover } from "components/account";
// ui
@@ -16,6 +16,8 @@ import { Button, Input } from "@plane/ui";
import { checkEmailValidity } from "helpers/string.helper";
// types
import { IPasswordSignInData } from "@plane/types";
+// constants
+import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker";
type Props = {
email: string;
@@ -46,6 +48,7 @@ export const SignInPasswordForm: React.FC = observer((props) => {
const {
config: { envConfig },
} = useApplication();
+ const { captureEvent } = useEventTracker();
// derived values
const isSmtpConfigured = envConfig?.is_smtp_configured;
// form info
@@ -72,7 +75,13 @@ export const SignInPasswordForm: React.FC = observer((props) => {
await authService
.passwordSignIn(payload)
- .then(async () => await onSubmit())
+ .then(async () => {
+ captureEvent(SIGN_IN_WITH_PASSWORD, {
+ state: "SUCCESS",
+ first_time: false,
+ });
+ await onSubmit();
+ })
.catch((err) =>
setToastAlert({
type: "error",
@@ -182,9 +191,10 @@ export const SignInPasswordForm: React.FC = observer((props) => {
)}
/>
-
+
{isSmtpConfigured ? (
captureEvent(FORGOT_PASSWORD)}
href={`/accounts/forgot-password?email=${email}`}
className="text-xs font-medium text-custom-primary-100"
>
diff --git a/web/components/account/sign-in-forms/root.tsx b/web/components/account/sign-in-forms/root.tsx
index c92cd4bd4..62f63caea 100644
--- a/web/components/account/sign-in-forms/root.tsx
+++ b/web/components/account/sign-in-forms/root.tsx
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
// hooks
-import { useApplication } from "hooks/store";
+import { useApplication, useEventTracker } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection";
// components
import { LatestFeatureBlock } from "components/common";
@@ -13,6 +13,8 @@ import {
OAuthOptions,
SignInOptionalSetPasswordForm,
} from "components/account";
+// constants
+import { NAVIGATE_TO_SIGNUP } from "constants/event-tracker";
export enum ESignInSteps {
EMAIL = "EMAIL",
@@ -32,6 +34,7 @@ export const SignInRoot = observer(() => {
const {
config: { envConfig },
} = useApplication();
+ const { captureEvent } = useEventTracker();
// derived values
const isSmtpConfigured = envConfig?.is_smtp_configured;
@@ -110,7 +113,11 @@ export const SignInRoot = observer(() => {
Don{"'"}t have an account?{" "}
-
+ captureEvent(NAVIGATE_TO_SIGNUP, {})}
+ className="text-custom-primary-100 font-medium underline"
+ >
Sign up
diff --git a/web/components/account/sign-in-forms/unique-code.tsx b/web/components/account/sign-in-forms/unique-code.tsx
index 6e0ae3745..55dbe86e2 100644
--- a/web/components/account/sign-in-forms/unique-code.tsx
+++ b/web/components/account/sign-in-forms/unique-code.tsx
@@ -7,12 +7,15 @@ import { UserService } from "services/user.service";
// hooks
import useToast from "hooks/use-toast";
import useTimer from "hooks/use-timer";
+import { useEventTracker } from "hooks/store";
// ui
import { Button, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// types
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
+// constants
+import { CODE_VERIFIED } from "constants/event-tracker";
type Props = {
email: string;
@@ -41,6 +44,8 @@ export const SignInUniqueCodeForm: React.FC
= (props) => {
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
// toast alert
const { setToastAlert } = useToast();
+ // store hooks
+ const { captureEvent } = useEventTracker();
// timer
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
// form info
@@ -69,17 +74,22 @@ export const SignInUniqueCodeForm: React.FC = (props) => {
await authService
.magicSignIn(payload)
.then(async () => {
+ captureEvent(CODE_VERIFIED, {
+ state: "SUCCESS",
+ });
const currentUser = await userService.currentUser();
-
await onSubmit(currentUser.is_password_autoset);
})
- .catch((err) =>
+ .catch((err) => {
+ captureEvent(CODE_VERIFIED, {
+ state: "FAILED",
+ });
setToastAlert({
type: "error",
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
- })
- );
+ });
+ });
};
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
diff --git a/web/components/account/sign-up-forms/optional-set-password.tsx b/web/components/account/sign-up-forms/optional-set-password.tsx
index db14f0ccb..b49adabbb 100644
--- a/web/components/account/sign-up-forms/optional-set-password.tsx
+++ b/web/components/account/sign-up-forms/optional-set-password.tsx
@@ -4,12 +4,14 @@ import { Controller, useForm } from "react-hook-form";
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
+import { useEventTracker } from "hooks/store";
// ui
import { Button, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// constants
import { ESignUpSteps } from "components/account";
+import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker";
// icons
import { Eye, EyeOff } from "lucide-react";
@@ -37,6 +39,8 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => {
// states
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
const [showPassword, setShowPassword] = useState(false);
+ // store hooks
+ const { captureEvent } = useEventTracker();
// toast alert
const { setToastAlert } = useToast();
// form info
@@ -66,21 +70,34 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => {
title: "Success!",
message: "Password created successfully.",
});
+ captureEvent(SETUP_PASSWORD, {
+ state: "SUCCESS",
+ first_time: true,
+ });
await handleSignInRedirection();
})
- .catch((err) =>
+ .catch((err) => {
+ captureEvent(SETUP_PASSWORD, {
+ state: "FAILED",
+ first_time: true,
+ });
setToastAlert({
type: "error",
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
- })
- );
+ });
+ });
};
const handleGoToWorkspace = async () => {
setIsGoingToWorkspace(true);
-
- await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false));
+ await handleSignInRedirection().finally(() => {
+ captureEvent(PASSWORD_CREATE_SKIPPED, {
+ state: "SUCCESS",
+ first_time: true,
+ });
+ setIsGoingToWorkspace(false);
+ });
};
return (
diff --git a/web/components/account/sign-up-forms/root.tsx b/web/components/account/sign-up-forms/root.tsx
index da9d7d79a..8eeb5e99f 100644
--- a/web/components/account/sign-up-forms/root.tsx
+++ b/web/components/account/sign-up-forms/root.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
// hooks
-import { useApplication } from "hooks/store";
+import { useApplication, useEventTracker } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection";
// components
import {
@@ -12,6 +12,8 @@ import {
SignUpUniqueCodeForm,
} from "components/account";
import Link from "next/link";
+// constants
+import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker";
export enum ESignUpSteps {
EMAIL = "EMAIL",
@@ -32,6 +34,7 @@ export const SignUpRoot = observer(() => {
const {
config: { envConfig },
} = useApplication();
+ const { captureEvent } = useEventTracker();
// step 1 submit handler- email verification
const handleEmailVerification = () => setSignInStep(ESignUpSteps.UNIQUE_CODE);
@@ -86,7 +89,11 @@ export const SignUpRoot = observer(() => {
Already using Plane?{" "}
-
+ captureEvent(NAVIGATE_TO_SIGNIN, {})}
+ className="text-custom-primary-100 font-medium underline"
+ >
Sign in
diff --git a/web/components/account/sign-up-forms/unique-code.tsx b/web/components/account/sign-up-forms/unique-code.tsx
index 7764b627e..1b54ef9eb 100644
--- a/web/components/account/sign-up-forms/unique-code.tsx
+++ b/web/components/account/sign-up-forms/unique-code.tsx
@@ -8,12 +8,15 @@ import { UserService } from "services/user.service";
// hooks
import useToast from "hooks/use-toast";
import useTimer from "hooks/use-timer";
+import { useEventTracker } from "hooks/store";
// ui
import { Button, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// types
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
+// constants
+import { CODE_VERIFIED } from "constants/event-tracker";
type Props = {
email: string;
@@ -39,6 +42,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => {
const { email, handleEmailClear, onSubmit } = props;
// states
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
+ // store hooks
+ const { captureEvent } = useEventTracker();
// toast alert
const { setToastAlert } = useToast();
// timer
@@ -69,17 +74,22 @@ export const SignUpUniqueCodeForm: React.FC = (props) => {
await authService
.magicSignIn(payload)
.then(async () => {
+ captureEvent(CODE_VERIFIED, {
+ state: "SUCCESS",
+ });
const currentUser = await userService.currentUser();
-
await onSubmit(currentUser.is_password_autoset);
})
- .catch((err) =>
+ .catch((err) => {
+ captureEvent(CODE_VERIFIED, {
+ state: "FAILED",
+ });
setToastAlert({
type: "error",
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
- })
- );
+ });
+ });
};
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
@@ -96,7 +106,6 @@ export const SignUpUniqueCodeForm: React.FC = (props) => {
title: "Success!",
message: "A new unique code has been sent to your email.",
});
-
reset({
email: formData.email,
token: "",
diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx
index bad7df0e5..375c15301 100644
--- a/web/components/cycles/cycles-board-card.tsx
+++ b/web/components/cycles/cycles-board-card.tsx
@@ -16,6 +16,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
// constants
import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace";
+import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker";
//.types
import { TCycleGroups } from "@plane/types";
@@ -33,7 +34,7 @@ export const CyclesBoardCard: FC = (props) => {
// router
const router = useRouter();
// store
- const { setTrackElement } = useEventTracker();
+ const { setTrackElement, captureEvent } = useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
@@ -90,39 +91,55 @@ export const CyclesBoardCard: FC = (props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
- addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Couldn't add the cycle to favorites. Please try again.",
+ addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
+ .then(() => {
+ captureEvent(CYCLE_FAVORITED, {
+ cycle_id: cycleId,
+ element: "Grid layout",
+ state: "SUCCESS",
+ });
+ })
+ .catch(() => {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Couldn't add the cycle to favorites. Please try again.",
+ });
});
- });
};
const handleRemoveFromFavorites = (e: MouseEvent) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
- removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Couldn't add the cycle to favorites. Please try again.",
+ removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
+ .then(() => {
+ captureEvent(CYCLE_UNFAVORITED, {
+ cycle_id: cycleId,
+ element: "Grid layout",
+ state: "SUCCESS",
+ });
+ })
+ .catch(() => {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Couldn't add the cycle to favorites. Please try again.",
+ });
});
- });
};
const handleEditCycle = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
- setTrackElement("Cycles page board layout");
+ setTrackElement("Cycles page grid layout");
setUpdateModal(true);
};
const handleDeleteCycle = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
- setTrackElement("Cycles page board layout");
+ setTrackElement("Cycles page grid layout");
setDeleteModal(true);
};
diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx
index 725480241..98392cd0e 100644
--- a/web/components/cycles/cycles-list-item.tsx
+++ b/web/components/cycles/cycles-list-item.tsx
@@ -18,6 +18,7 @@ import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace";
// types
import { TCycleGroups } from "@plane/types";
+import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker";
type TCyclesListItem = {
cycleId: string;
@@ -37,7 +38,7 @@ export const CyclesListItem: FC = (props) => {
// router
const router = useRouter();
// store hooks
- const { setTrackElement } = useEventTracker();
+ const { setTrackElement, captureEvent } = useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
@@ -63,26 +64,42 @@ export const CyclesListItem: FC = (props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
- addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Couldn't add the cycle to favorites. Please try again.",
+ addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
+ .then(() => {
+ captureEvent(CYCLE_FAVORITED, {
+ cycle_id: cycleId,
+ element: "List layout",
+ state: "SUCCESS",
+ });
+ })
+ .catch(() => {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Couldn't add the cycle to favorites. Please try again.",
+ });
});
- });
};
const handleRemoveFromFavorites = (e: MouseEvent) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
- removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Couldn't add the cycle to favorites. Please try again.",
+ removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
+ .then(() => {
+ captureEvent(CYCLE_UNFAVORITED, {
+ cycle_id: cycleId,
+ element: "List layout",
+ state: "SUCCESS",
+ });
+ })
+ .catch(() => {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Couldn't add the cycle to favorites. Please try again.",
+ });
});
- });
};
const handleEditCycle = (e: MouseEvent) => {
@@ -159,9 +176,9 @@ export const CyclesListItem: FC = (props) => {
projectId={projectId}
/>
-
-
-
+
+
+
{isCompleted ? (
@@ -181,20 +198,20 @@ export const CyclesListItem: FC = (props) => {
-
+
{cycleDetails.name}
-
+
{currentCycle && (
= (props) => {
)}
-
+
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
-
+
{cycleDetails.assignees.length > 0 ? (
diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx
index 32e067833..5dc0306ab 100644
--- a/web/components/cycles/delete-modal.tsx
+++ b/web/components/cycles/delete-modal.tsx
@@ -10,6 +10,8 @@ import useToast from "hooks/use-toast";
import { Button } from "@plane/ui";
// types
import { ICycle } from "@plane/types";
+// constants
+import { CYCLE_DELETED } from "constants/event-tracker";
interface ICycleDelete {
cycle: ICycle;
@@ -45,13 +47,13 @@ export const CycleDeleteModal: React.FC
= observer((props) => {
message: "Cycle deleted successfully.",
});
captureCycleEvent({
- eventName: "Cycle deleted",
+ eventName: CYCLE_DELETED,
payload: { ...cycle, state: "SUCCESS" },
});
})
.catch(() => {
captureCycleEvent({
- eventName: "Cycle deleted",
+ eventName: CYCLE_DELETED,
payload: { ...cycle, state: "FAILED" },
});
});
diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx
index 865cc68a1..dfe2a878e 100644
--- a/web/components/cycles/form.tsx
+++ b/web/components/cycles/form.tsx
@@ -10,7 +10,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { ICycle } from "@plane/types";
type Props = {
- handleFormSubmit: (values: Partial) => Promise;
+ handleFormSubmit: (values: Partial, dirtyFields: any) => Promise;
handleClose: () => void;
status: boolean;
projectId: string;
@@ -29,7 +29,7 @@ export const CycleForm: React.FC = (props) => {
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
// form data
const {
- formState: { errors, isSubmitting },
+ formState: { errors, isSubmitting, dirtyFields },
handleSubmit,
control,
watch,
@@ -61,7 +61,7 @@ export const CycleForm: React.FC = (props) => {
maxDate?.setDate(maxDate.getDate() - 1);
return (
-
diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx
index 82253af88..998ad268c 100644
--- a/web/components/inbox/inbox-issue-actions.tsx
+++ b/web/components/inbox/inbox-issue-actions.tsx
@@ -20,6 +20,7 @@ import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle
// types
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
import { EUserProjectRoles } from "constants/project";
+import { ISSUE_DELETED } from "constants/event-tracker";
type TInboxIssueActionsHeader = {
workspaceSlug: string;
@@ -86,17 +87,12 @@ export const InboxIssueActionsHeader: FC = observer((p
throw new Error("Missing required parameters");
await removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
captureIssueEvent({
- eventName: "Issue deleted",
+ eventName: ISSUE_DELETED,
payload: {
id: inboxIssueId,
state: "SUCCESS",
element: "Inbox page",
- },
- group: {
- isGrouping: true,
- groupType: "Workspace_metrics",
- groupId: currentWorkspace?.id!,
- },
+ }
});
router.push({
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
@@ -108,17 +104,12 @@ export const InboxIssueActionsHeader: FC = observer((p
message: "Something went wrong while deleting inbox issue. Please try again.",
});
captureIssueEvent({
- eventName: "Issue deleted",
+ eventName: ISSUE_DELETED,
payload: {
id: inboxIssueId,
state: "FAILED",
element: "Inbox page",
},
- group: {
- isGrouping: true,
- groupType: "Workspace_metrics",
- groupId: currentWorkspace?.id!,
- },
});
}
},
diff --git a/web/components/inbox/modals/create-issue-modal.tsx b/web/components/inbox/modals/create-issue-modal.tsx
index 066f172ca..84c4bef1e 100644
--- a/web/components/inbox/modals/create-issue-modal.tsx
+++ b/web/components/inbox/modals/create-issue-modal.tsx
@@ -18,6 +18,8 @@ import { GptAssistantPopover } from "components/core";
import { Button, Input, ToggleSwitch } from "@plane/ui";
// types
import { TIssue } from "@plane/types";
+// constants
+import { ISSUE_CREATED } from "constants/event-tracker";
type Props = {
isOpen: boolean;
@@ -65,7 +67,6 @@ export const CreateInboxIssueModal: React.FC = observer((props) => {
config: { envConfig },
} = useApplication();
const { captureIssueEvent } = useEventTracker();
- const { currentWorkspace } = useWorkspace();
const {
control,
@@ -94,34 +95,24 @@ export const CreateInboxIssueModal: React.FC = observer((props) => {
handleClose();
} else reset(defaultValues);
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: {
...formData,
state: "SUCCESS",
element: "Inbox page",
},
- group: {
- isGrouping: true,
- groupType: "Workspace_metrics",
- groupId: currentWorkspace?.id!,
- },
path: router.pathname,
});
})
.catch((error) => {
console.error(error);
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: {
...formData,
state: "FAILED",
element: "Inbox page",
},
- group: {
- isGrouping: true,
- groupType: "Workspace_metrics",
- groupId: currentWorkspace?.id!,
- },
path: router.pathname,
});
});
diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx
index 11d74af0e..ffa17d337 100644
--- a/web/components/issues/attachment/root.tsx
+++ b/web/components/issues/attachment/root.tsx
@@ -38,7 +38,7 @@ export const IssueAttachmentRoot: FC = (props) => {
title: "Attachment uploaded",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: "Issue attachment added",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "attachment",
@@ -47,7 +47,7 @@ export const IssueAttachmentRoot: FC = (props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: "Issue attachment added",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
});
setToastAlert({
@@ -67,7 +67,7 @@ export const IssueAttachmentRoot: FC = (props) => {
title: "Attachment removed",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: "Issue attachment deleted",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "attachment",
@@ -76,7 +76,7 @@ export const IssueAttachmentRoot: FC = (props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: "Issue attachment deleted",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "attachment",
diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx
index 2e0303a8e..92badf4b2 100644
--- a/web/components/issues/issue-detail/root.tsx
+++ b/web/components/issues/issue-detail/root.tsx
@@ -16,6 +16,7 @@ import { TIssue } from "@plane/types";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue";
+import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
export type TIssueOperations = {
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise;
@@ -102,7 +103,7 @@ export const IssueDetailRoot: FC = (props) => {
});
}
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: Object.keys(data).join(","),
@@ -112,7 +113,7 @@ export const IssueDetailRoot: FC = (props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: Object.keys(data).join(","),
@@ -138,7 +139,7 @@ export const IssueDetailRoot: FC = (props) => {
message: "Issue deleted successfully",
});
captureIssueEvent({
- eventName: "Issue deleted",
+ eventName: ISSUE_DELETED,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
path: router.asPath,
});
@@ -149,7 +150,7 @@ export const IssueDetailRoot: FC = (props) => {
message: "Issue delete failed",
});
captureIssueEvent({
- eventName: "Issue deleted",
+ eventName: ISSUE_DELETED,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
path: router.asPath,
});
@@ -164,7 +165,7 @@ export const IssueDetailRoot: FC = (props) => {
message: "Issue added to issue successfully",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "cycle_id",
@@ -174,7 +175,7 @@ export const IssueDetailRoot: FC = (props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "cycle_id",
@@ -198,7 +199,7 @@ export const IssueDetailRoot: FC = (props) => {
message: "Cycle removed from issue successfully",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "cycle_id",
@@ -208,7 +209,7 @@ export const IssueDetailRoot: FC = (props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "cycle_id",
@@ -232,7 +233,7 @@ export const IssueDetailRoot: FC = (props) => {
message: "Module added to issue successfully",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "module_id",
@@ -242,7 +243,7 @@ export const IssueDetailRoot: FC = (props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "module_id",
@@ -266,7 +267,7 @@ export const IssueDetailRoot: FC = (props) => {
message: "Module removed from issue successfully",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "module_id",
@@ -276,7 +277,7 @@ export const IssueDetailRoot: FC = (props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "module_id",
diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx
index 1f62c248c..6db9323fa 100644
--- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx
+++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx
@@ -13,6 +13,8 @@ import { createIssuePayload } from "helpers/issue.helper";
import { PlusIcon } from "lucide-react";
// types
import { TIssue } from "@plane/types";
+// constants
+import { ISSUE_CREATED } from "constants/event-tracker";
type Props = {
formKey: keyof TIssue;
@@ -129,7 +131,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => {
viewId
).then((res) => {
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...res, state: "SUCCESS", element: "Calendar quick add" },
path: router.asPath,
});
@@ -142,7 +144,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => {
} catch (err: any) {
console.error(err);
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED", element: "Calendar quick add" },
path: router.asPath,
});
diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx
index 0dae3c8bd..c03e86504 100644
--- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx
+++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import isEqual from "lodash/isEqual";
// hooks
-import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
+import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
//ui
import { Button } from "@plane/ui";
// components
@@ -11,6 +11,8 @@ import { AppliedFiltersList } from "components/issues";
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
+// constants
+import { GLOBAL_VIEW_UPDATED } from "constants/event-tracker";
type Props = {
globalViewId: string;
@@ -27,6 +29,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
} = useIssues(EIssuesStoreType.GLOBAL);
const { workspaceLabels } = useLabel();
const { globalViewMap, updateGlobalView } = useGlobalView();
+ const { captureEvent } = useEventTracker();
const {
membership: { currentWorkspaceRole },
} = useUser();
@@ -91,6 +94,13 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
filters: {
...(appliedFilters ?? {}),
},
+ }).then((res) => {
+ captureEvent(GLOBAL_VIEW_UPDATED, {
+ view_id: res.id,
+ applied_filters: res.filters,
+ state: "SUCCESS",
+ element: "Spreadsheet view",
+ });
});
};
diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx
index e89f60688..bfecb993b 100644
--- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx
+++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx
@@ -13,6 +13,8 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { createIssuePayload } from "helpers/issue.helper";
// types
import { IProject, TIssue } from "@plane/types";
+// constants
+import { ISSUE_CREATED } from "constants/event-tracker";
interface IInputProps {
formKey: string;
@@ -111,7 +113,7 @@ export const GanttQuickAddIssueForm: React.FC = observe
quickAddCallback &&
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...res, state: "SUCCESS", element: "Gantt quick add" },
path: router.asPath,
});
@@ -123,7 +125,7 @@ export const GanttQuickAddIssueForm: React.FC = observe
});
} catch (err: any) {
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED", element: "Gantt quick add" },
path: router.asPath,
});
diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
index 36d5e0315..83f72d8ea 100644
--- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
+++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
@@ -25,6 +25,7 @@ import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile";
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue";
+import { ISSUE_DELETED } from "constants/event-tracker";
export interface IBaseKanBanLayout {
issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues;
@@ -212,7 +213,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas
setDeleteIssueModal(false);
setDragState({});
captureIssueEvent({
- eventName: "Issue deleted",
+ eventName: ISSUE_DELETED,
payload: { id: dragState.draggedIssueId!, state: "FAILED", element: "Kanban layout drag & drop" },
path: router.asPath,
});
diff --git a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx
index 8880ca278..513163431 100644
--- a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx
+++ b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx
@@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { createIssuePayload } from "helpers/issue.helper";
// types
import { TIssue } from "@plane/types";
+// constants
+import { ISSUE_CREATED } from "constants/event-tracker";
const Inputs = (props: any) => {
const { register, setFocus, projectDetail } = props;
@@ -106,7 +108,7 @@ export const KanBanQuickAddIssueForm: React.FC = obser
viewId
).then((res) => {
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...res, state: "SUCCESS", element: "Kanban quick add" },
path: router.asPath,
});
@@ -118,7 +120,7 @@ export const KanBanQuickAddIssueForm: React.FC = obser
});
} catch (err: any) {
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED", element: "Kanban quick add" },
path: router.asPath,
});
diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx
index dd63f09aa..8d1ce6d9c 100644
--- a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx
+++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx
@@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { TIssue, IProject } from "@plane/types";
// types
import { createIssuePayload } from "helpers/issue.helper";
+// constants
+import { ISSUE_CREATED } from "constants/event-tracker";
interface IInputProps {
formKey: string;
@@ -103,7 +105,7 @@ export const ListQuickAddIssueForm: FC = observer((props
quickAddCallback &&
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...res, state: "SUCCESS", element: "List quick add" },
path: router.asPath,
});
@@ -115,7 +117,7 @@ export const ListQuickAddIssueForm: FC = observer((props
});
} catch (err: any) {
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED", element: "List quick add" },
path: router.asPath,
});
diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx
index e0a0dbd5c..4d851545e 100644
--- a/web/components/issues/issue-layouts/properties/all-properties.tsx
+++ b/web/components/issues/issue-layouts/properties/all-properties.tsx
@@ -18,6 +18,8 @@ import {
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
+// constants
+import { ISSUE_UPDATED } from "constants/event-tracker";
export interface IIssueProperties {
issue: TIssue;
@@ -40,7 +42,7 @@ export const IssueProperties: React.FC = observer((props) => {
const handleState = (stateId: string) => {
handleIssues({ ...issue, state_id: stateId }).then(() => {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath,
updates: {
@@ -54,7 +56,7 @@ export const IssueProperties: React.FC = observer((props) => {
const handlePriority = (value: TIssuePriorities) => {
handleIssues({ ...issue, priority: value }).then(() => {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath,
updates: {
@@ -68,7 +70,7 @@ export const IssueProperties: React.FC = observer((props) => {
const handleLabel = (ids: string[]) => {
handleIssues({ ...issue, label_ids: ids }).then(() => {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath,
updates: {
@@ -82,7 +84,7 @@ export const IssueProperties: React.FC = observer((props) => {
const handleAssignee = (ids: string[]) => {
handleIssues({ ...issue, assignee_ids: ids }).then(() => {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath,
updates: {
@@ -96,7 +98,7 @@ export const IssueProperties: React.FC = observer((props) => {
const handleStartDate = (date: Date | null) => {
handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath,
updates: {
@@ -110,7 +112,7 @@ export const IssueProperties: React.FC = observer((props) => {
const handleTargetDate = (date: Date | null) => {
handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath,
updates: {
@@ -124,7 +126,7 @@ export const IssueProperties: React.FC = observer((props) => {
const handleEstimate = (value: number | null) => {
handleIssues({ ...issue, estimate_point: value }).then(() => {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath,
updates: {
diff --git a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx
index b0acd7237..3cba3c6cd 100644
--- a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx
@@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { createIssuePayload } from "helpers/issue.helper";
// types
import { TIssue } from "@plane/types";
+// constants
+import { ISSUE_CREATED } from "constants/event-tracker";
type Props = {
formKey: keyof TIssue;
@@ -162,7 +164,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) =>
(await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then(
(res) => {
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" },
path: router.asPath,
});
@@ -175,7 +177,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) =>
});
} catch (err: any) {
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" },
path: router.asPath,
});
diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx
index 02a087314..97d977ace 100644
--- a/web/components/issues/issue-modal/modal.tsx
+++ b/web/components/issues/issue-modal/modal.tsx
@@ -13,6 +13,8 @@ import { IssueFormRoot } from "./form";
import type { TIssue } from "@plane/types";
// constants
import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue";
+import { ISSUE_CREATED, ISSUE_UPDATED } from "constants/event-tracker";
+
export interface IssuesModalProps {
data?: Partial;
isOpen: boolean;
@@ -157,14 +159,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop
message: "Issue created successfully.",
});
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...response, state: "SUCCESS" },
path: router.asPath,
- group: {
- isGrouping: true,
- groupType: "Workspace_metrics",
- groupId: currentWorkspace?.id!,
- },
});
!createMore && handleClose();
return response;
@@ -175,14 +172,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop
message: "Issue could not be created. Please try again.",
});
captureIssueEvent({
- eventName: "Issue created",
+ eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED" },
path: router.asPath,
- group: {
- isGrouping: true,
- groupType: "Workspace_metrics",
- groupId: currentWorkspace?.id!,
- },
});
}
};
@@ -198,14 +190,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop
message: "Issue updated successfully.",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS" },
path: router.asPath,
- group: {
- isGrouping: true,
- groupType: "Workspace_metrics",
- groupId: currentWorkspace?.id!,
- },
});
handleClose();
return response;
@@ -216,14 +203,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop
message: "Issue could not be created. Please try again.",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...payload, state: "FAILED" },
path: router.asPath,
- group: {
- isGrouping: true,
- groupType: "Workspace_metrics",
- groupId: currentWorkspace?.id!,
- },
});
}
};
diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx
index f14018ed4..b491ebe36 100644
--- a/web/components/issues/peek-overview/root.tsx
+++ b/web/components/issues/peek-overview/root.tsx
@@ -11,6 +11,7 @@ import { TIssue } from "@plane/types";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue";
+import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
interface IIssuePeekOverview {
is_archived?: boolean;
@@ -103,7 +104,7 @@ export const IssuePeekOverview: FC = observer((props) => {
message: "Issue updated successfully",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: Object.keys(data).join(","),
@@ -113,7 +114,7 @@ export const IssuePeekOverview: FC = observer((props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue peek-overview" },
path: router.asPath,
});
@@ -135,7 +136,7 @@ export const IssuePeekOverview: FC = observer((props) => {
message: "Issue deleted successfully",
});
captureIssueEvent({
- eventName: "Issue deleted",
+ eventName: ISSUE_DELETED,
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
path: router.asPath,
});
@@ -146,7 +147,7 @@ export const IssuePeekOverview: FC = observer((props) => {
message: "Issue delete failed",
});
captureIssueEvent({
- eventName: "Issue deleted",
+ eventName: ISSUE_DELETED,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
path: router.asPath,
});
@@ -161,7 +162,7 @@ export const IssuePeekOverview: FC = observer((props) => {
message: "Issue added to issue successfully",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: "cycle_id",
@@ -171,7 +172,7 @@ export const IssuePeekOverview: FC = observer((props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue peek-overview" },
updates: {
changed_property: "cycle_id",
@@ -195,7 +196,7 @@ export const IssuePeekOverview: FC = observer((props) => {
message: "Cycle removed from issue successfully",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: "cycle_id",
@@ -210,7 +211,7 @@ export const IssuePeekOverview: FC = observer((props) => {
message: "Cycle remove from issue failed",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue peek-overview" },
updates: {
changed_property: "cycle_id",
@@ -229,7 +230,7 @@ export const IssuePeekOverview: FC = observer((props) => {
message: "Module added to issue successfully",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: "module_id",
@@ -239,7 +240,7 @@ export const IssuePeekOverview: FC = observer((props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
updates: {
changed_property: "module_id",
@@ -263,7 +264,7 @@ export const IssuePeekOverview: FC = observer((props) => {
message: "Module removed from issue successfully",
});
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: "module_id",
@@ -273,7 +274,7 @@ export const IssuePeekOverview: FC = observer((props) => {
});
} catch (error) {
captureIssueEvent({
- eventName: "Issue updated",
+ eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
updates: {
changed_property: "module_id",
diff --git a/web/components/modules/delete-module-modal.tsx b/web/components/modules/delete-module-modal.tsx
index 2727b4e3b..636a828ae 100644
--- a/web/components/modules/delete-module-modal.tsx
+++ b/web/components/modules/delete-module-modal.tsx
@@ -11,6 +11,8 @@ import { Button } from "@plane/ui";
import { AlertTriangle } from "lucide-react";
// types
import type { IModule } from "@plane/types";
+// constants
+import { MODULE_DELETED } from "constants/event-tracker";
type Props = {
data: IModule;
@@ -51,7 +53,7 @@ export const DeleteModuleModal: React.FC = observer((props) => {
message: "Module deleted successfully.",
});
captureModuleEvent({
- eventName: "Module deleted",
+ eventName: MODULE_DELETED,
payload: { ...data, state: "SUCCESS" },
});
})
@@ -62,7 +64,7 @@ export const DeleteModuleModal: React.FC = observer((props) => {
message: "Module could not be deleted. Please try again.",
});
captureModuleEvent({
- eventName: "Module deleted",
+ eventName: MODULE_DELETED,
payload: { ...data, state: "FAILED" },
});
})
diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx
index be0792caa..8fa63e826 100644
--- a/web/components/modules/form.tsx
+++ b/web/components/modules/form.tsx
@@ -11,7 +11,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { IModule } from "@plane/types";
type Props = {
- handleFormSubmit: (values: Partial) => Promise;
+ handleFormSubmit: (values: Partial, dirtyFields: any) => Promise;
handleClose: () => void;
status: boolean;
projectId: string;
@@ -36,7 +36,7 @@ export const ModuleForm: React.FC = ({
data,
}) => {
const {
- formState: { errors, isSubmitting },
+ formState: { errors, isSubmitting, dirtyFields },
handleSubmit,
watch,
control,
@@ -53,7 +53,7 @@ export const ModuleForm: React.FC = ({
});
const handleCreateUpdateModule = async (formData: Partial) => {
- await handleFormSubmit(formData);
+ await handleFormSubmit(formData, dirtyFields);
reset({
...defaultValues,
diff --git a/web/components/modules/modal.tsx b/web/components/modules/modal.tsx
index 0852434c3..7990386df 100644
--- a/web/components/modules/modal.tsx
+++ b/web/components/modules/modal.tsx
@@ -9,6 +9,8 @@ import useToast from "hooks/use-toast";
import { ModuleForm } from "components/modules";
// types
import type { IModule } from "@plane/types";
+// constants
+import { MODULE_CREATED, MODULE_UPDATED } from "constants/event-tracker";
type Props = {
isOpen: boolean;
@@ -59,7 +61,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => {
message: "Module created successfully.",
});
captureModuleEvent({
- eventName: "Module created",
+ eventName: MODULE_CREATED,
payload: { ...res, state: "SUCCESS" },
});
})
@@ -70,13 +72,13 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => {
message: err.detail ?? "Module could not be created. Please try again.",
});
captureModuleEvent({
- eventName: "Module created",
+ eventName: MODULE_CREATED,
payload: { ...data, state: "FAILED" },
});
});
};
- const handleUpdateModule = async (payload: Partial) => {
+ const handleUpdateModule = async (payload: Partial, dirtyFields: any) => {
if (!workspaceSlug || !projectId || !data) return;
const selectedProjectId = payload.project ?? projectId.toString();
@@ -90,8 +92,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => {
message: "Module updated successfully.",
});
captureModuleEvent({
- eventName: "Module updated",
- payload: { ...res, state: "SUCCESS" },
+ eventName: MODULE_UPDATED,
+ payload: { ...res, changed_properties: Object.keys(dirtyFields), state: "SUCCESS" },
});
})
.catch((err) => {
@@ -101,20 +103,20 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => {
message: err.detail ?? "Module could not be updated. Please try again.",
});
captureModuleEvent({
- eventName: "Module updated",
+ eventName: MODULE_UPDATED,
payload: { ...data, state: "FAILED" },
});
});
};
- const handleFormSubmit = async (formData: Partial) => {
+ const handleFormSubmit = async (formData: Partial, dirtyFields: any) => {
if (!workspaceSlug || !projectId) return;
const payload: Partial = {
...formData,
};
if (!data) await handleCreateModule(payload);
- else await handleUpdateModule(payload);
+ else await handleUpdateModule(payload, dirtyFields);
};
useEffect(() => {
diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx
index 3d83be010..219942550 100644
--- a/web/components/modules/module-card-item.tsx
+++ b/web/components/modules/module-card-item.tsx
@@ -16,6 +16,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
// constants
import { MODULE_STATUS } from "constants/module";
import { EUserProjectRoles } from "constants/project";
+import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker";
type Props = {
moduleId: string;
@@ -36,7 +37,7 @@ export const ModuleCardItem: React.FC = observer((props) => {
membership: { currentProjectRole },
} = useUser();
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
- const { setTrackElement } = useEventTracker();
+ const { setTrackElement, captureEvent } = useEventTracker();
// derived values
const moduleDetails = getModuleById(moduleId);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
@@ -46,13 +47,21 @@ export const ModuleCardItem: React.FC = observer((props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
- addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => {
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Couldn't add the module to favorites. Please try again.",
+ addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
+ .then(() => {
+ captureEvent(MODULE_FAVORITED, {
+ module_id: moduleId,
+ element: "Grid layout",
+ state: "SUCCESS",
+ });
+ })
+ .catch(() => {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Couldn't add the module to favorites. Please try again.",
+ });
});
- });
};
const handleRemoveFromFavorites = (e: React.MouseEvent) => {
@@ -60,13 +69,21 @@ export const ModuleCardItem: React.FC = observer((props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
- removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => {
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Couldn't remove the module from favorites. Please try again.",
+ removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
+ .then(() => {
+ captureEvent(MODULE_UNFAVORITED, {
+ module_id: moduleId,
+ element: "Grid layout",
+ state: "SUCCESS",
+ });
+ })
+ .catch(() => {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Couldn't remove the module from favorites. Please try again.",
+ });
});
- });
};
const handleCopyText = (e: React.MouseEvent) => {
@@ -84,14 +101,14 @@ export const ModuleCardItem: React.FC = observer((props) => {
const handleEditModule = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
- setTrackElement("Modules page board layout");
+ setTrackElement("Modules page grid layout");
setEditModal(true);
};
const handleDeleteModule = (e: React.MouseEvent