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 ae0235af6..4f37a624e 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 9c9cbec7a..fa0c8a2b8 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-primary-text-subtle" > diff --git a/web/components/account/sign-in-forms/root.tsx b/web/components/account/sign-in-forms/root.tsx index a22d2a5e5..2ff820262 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-primary-text-subtle 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 a2f72e96e..0ab0d1a7b 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 dca386ea2..16b7172a8 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 15b84c64f..6f6bff5e7 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-primary-text-subtle 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 30776bb05..84e1833e3 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 80ff68b2e..e564e0f94 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 97d0e2f38..3add09ece 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 ? ( @@ -221,7 +238,7 @@ export const CyclesListItem: FC = (props) => { ))} ) : ( - + )} diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 6911382cc..d49486f69 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 30e84c4ab..d324d0e75 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 ( -
+ handleFormSubmit(formData,dirtyFields))}>
{!status && ( diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index e44f29465..b1bde52d6 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -10,6 +10,8 @@ import useLocalStorage from "hooks/use-local-storage"; import { CycleForm } from "components/cycles"; // types import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types"; +// constants +import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker"; type CycleModalProps = { isOpen: boolean; @@ -47,7 +49,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { message: "Cycle created successfully.", }); captureCycleEvent({ - eventName: "Cycle created", + eventName: CYCLE_CREATED, payload: { ...res, state: "SUCCESS" }, }); }) @@ -58,18 +60,23 @@ export const CycleCreateUpdateModal: React.FC = (props) => { message: err.detail ?? "Error in creating cycle. Please try again.", }); captureCycleEvent({ - eventName: "Cycle created", + eventName: CYCLE_CREATED, payload: { ...payload, state: "FAILED" }, }); }); }; - const handleUpdateCycle = async (cycleId: string, payload: Partial) => { + const handleUpdateCycle = async (cycleId: string, payload: Partial, dirtyFields: any) => { if (!workspaceSlug || !projectId) return; const selectedProjectId = payload.project ?? projectId.toString(); await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload) - .then(() => { + .then((res) => { + const changed_properties = Object.keys(dirtyFields); + captureCycleEvent({ + eventName: CYCLE_UPDATED, + payload: { ...res, changed_properties: changed_properties, state: "SUCCESS" }, + }); setToastAlert({ type: "success", title: "Success!", @@ -77,6 +84,10 @@ export const CycleCreateUpdateModal: React.FC = (props) => { }); }) .catch((err) => { + captureCycleEvent({ + eventName: CYCLE_UPDATED, + payload: { ...payload, state: "FAILED" }, + }); setToastAlert({ type: "error", title: "Error!", @@ -95,7 +106,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { return status; }; - const handleFormSubmit = async (formData: Partial) => { + const handleFormSubmit = async (formData: Partial, dirtyFields: any) => { if (!workspaceSlug || !projectId) return; const payload: Partial = { @@ -119,7 +130,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { } if (isDateValid) { - if (data) await handleUpdateCycle(data.id, payload); + if (data) await handleUpdateCycle(data.id, payload, dirtyFields); else { await handleCreateCycle(payload).then(() => { setCycleTab("all"); diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index dbd8b0128..8da1f28c2 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -39,6 +39,7 @@ import { import { ICycle } from "@plane/types"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; +import { CYCLE_UPDATED } from "constants/event-tracker"; // fetch-keys import { CYCLE_STATUS } from "constants/cycle"; @@ -67,7 +68,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; // store hooks - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureCycleEvent } = useEventTracker(); const { membership: { currentProjectRole }, } = useUser(); @@ -83,10 +84,32 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { defaultValues, }); - const submitChanges = (data: Partial) => { + const submitChanges = (data: Partial, changedProperty: string) => { if (!workspaceSlug || !projectId || !cycleId) return; - updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); + updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data) + .then((res) => { + captureCycleEvent({ + eventName: CYCLE_UPDATED, + payload: { + ...res, + changed_properties: [changedProperty], + element: "Right side-peek", + state: "SUCCESS", + }, + }); + }) + + .catch((_) => { + captureCycleEvent({ + eventName: CYCLE_UPDATED, + payload: { + ...data, + element: "Right side-peek", + state: "FAILED", + }, + }); + }); }; const handleCopyText = () => { @@ -146,10 +169,13 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }); if (isDateValidForExistingCycle) { - submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }); + submitChanges( + { + start_date: renderFormattedPayloadDate(`${watch("start_date")}`), + end_date: renderFormattedPayloadDate(`${watch("end_date")}`), + }, + "start_date" + ); setToastAlert({ type: "success", title: "Success!", @@ -174,10 +200,13 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }); if (isDateValid) { - submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }); + submitChanges( + { + start_date: renderFormattedPayloadDate(`${watch("start_date")}`), + end_date: renderFormattedPayloadDate(`${watch("end_date")}`), + }, + "start_date" + ); setToastAlert({ type: "success", title: "Success!", @@ -219,10 +248,13 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }); if (isDateValidForExistingCycle) { - submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }); + submitChanges( + { + start_date: renderFormattedPayloadDate(`${watch("start_date")}`), + end_date: renderFormattedPayloadDate(`${watch("end_date")}`), + }, + "end_date" + ); setToastAlert({ type: "success", title: "Success!", @@ -246,10 +278,13 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }); if (isDateValid) { - submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }); + submitChanges( + { + start_date: renderFormattedPayloadDate(`${watch("start_date")}`), + end_date: renderFormattedPayloadDate(`${watch("end_date")}`), + }, + "end_date" + ); setToastAlert({ type: "success", title: "Success!", diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index d76257b09..90ee9de63 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -2,7 +2,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { FileText, Plus } from "lucide-react"; // hooks -import { useApplication, useProject, useUser } from "hooks/store"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // helpers @@ -25,6 +25,7 @@ export const PagesHeader = observer(() => { membership: { currentProjectRole }, } = useUser(); const { currentProjectDetails } = useProject(); + const { setTrackElement } = useEventTracker(); const canUserCreatePage = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); @@ -64,7 +65,15 @@ export const PagesHeader = observer(() => {
{canUserCreatePage && (
-
diff --git a/web/components/headers/workspace-dashboard.tsx b/web/components/headers/workspace-dashboard.tsx index deb74a532..adb89e4f2 100644 --- a/web/components/headers/workspace-dashboard.tsx +++ b/web/components/headers/workspace-dashboard.tsx @@ -4,18 +4,23 @@ import { useTheme } from "next-themes"; // images import githubBlackImage from "/public/logos/github-black.png"; import githubWhiteImage from "/public/logos/github-white.png"; +// hooks +import { useEventTracker } from "hooks/store"; // components import { BreadcrumbLink } from "components/common"; import { Breadcrumbs } from "@plane/ui"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +// constants +import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker"; export const WorkspaceDashboardHeader = () => { // hooks + const { captureEvent } = useEventTracker(); const { resolvedTheme } = useTheme(); return ( <> -
+
diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 731045b6f..3a0e95205 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 949d3f6e4..87fd9c2f1 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 14d77b401..1dfa5622c 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 2993292ba..2afa2c69f 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 59f5cb640..8e86c1669 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 b31d665a6..38c4fb92c 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 a69f3e5ea..d6db9d86e 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 385c090f4..c9c9bef30 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 9dc4b1e1b..fa82cec98 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/columns/due-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx index 2965fd604..bc837b7b8 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 37b1e0731..c1605a795 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/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx index 92eaed933..a8d1fd95c 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 744ebf675..b55b68b7d 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 21d7d76fa..5e7d265e0 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 288702cd4..d43aff30a 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 4c5fde3ab..c36bfb70b 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 2491ecbd2..a61b1c5b5 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) => { e.preventDefault(); e.stopPropagation(); - setTrackElement("Modules page board layout"); + setTrackElement("Modules page grid layout"); setDeleteModal(true); }; diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 6b6b489be..9035472c1 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-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 ModuleListItem: 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 ModuleListItem: 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 ModuleListItem: 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) => { diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index bc51cb48c..141925d1c 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -34,6 +34,7 @@ import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; // constant import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; +import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker"; const defaultValues: Partial = { lead: "", @@ -66,7 +67,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { membership: { currentProjectRole }, } = useUser(); const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule(); - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker(); const moduleDetails = getModuleById(moduleId); const { setToastAlert } = useToast(); @@ -77,7 +78,19 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !moduleId) return; - updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data); + updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data) + .then((res) => { + captureModuleEvent({ + eventName: MODULE_UPDATED, + payload: { ...res, changed_properties: Object.keys(data)[0], element: "Right side-peek", state: "SUCCESS" }, + }); + }) + .catch((_) => { + captureModuleEvent({ + eventName: MODULE_UPDATED, + payload: { ...data, state: "FAILED" }, + }); + }); }; const handleCreateLink = async (formData: ModuleLink) => { @@ -87,6 +100,10 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload) .then(() => { + captureEvent(MODULE_LINK_CREATED, { + module_id: moduleId, + state: "SUCCESS", + }); setToastAlert({ type: "success", title: "Module link created", @@ -109,6 +126,10 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload) .then(() => { + captureEvent(MODULE_LINK_UPDATED, { + module_id: moduleId, + state: "SUCCESS", + }); setToastAlert({ type: "success", title: "Module link updated", @@ -129,6 +150,10 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId) .then(() => { + captureEvent(MODULE_LINK_DELETED, { + module_id: moduleId, + state: "SUCCESS", + }); setToastAlert({ type: "success", title: "Module link deleted", @@ -187,8 +212,8 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), target_date: renderFormattedPayloadDate(`${watch("target_date")}`), + start_date: renderFormattedPayloadDate(`${watch("start_date")}`), }); setToastAlert({ type: "success", @@ -294,7 +319,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { ( + render={({ field: { value, onChange } }) => ( void; @@ -28,6 +33,7 @@ type NotificationCardProps = { export const NotificationCard: React.FC = (props) => { const { + selectedTab, notification, isSnoozedTabOpen, closePopover, @@ -37,11 +43,78 @@ export const NotificationCard: React.FC = (props) => { setSelectedNotificationForSnooze, markSnoozeNotification, } = props; + // store hooks + const { captureEvent } = useEventTracker(); 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; @@ -49,6 +122,10 @@ export const NotificationCard: React.FC = (props) => { { markNotificationReadStatus(notification.id); + captureEvent(ISSUE_OPENED, { + issue_id: notification.data.issue.id, + element: "notification", + }); closePopover(); }} href={`/${workspaceSlug}/projects/${notification.project}/${ @@ -87,57 +164,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} +
+ )} +
+ + {({ open }) => ( + <> + + + + {open && ( + +
+ {moreOptions.map((item) => ( + + {({ close }) => ( + + )} + + ))} + +
{ + e.stopPropagation(); + e.preventDefault(); + setshowSnoozeOptions(true); + }} + className="flex gap-x-2 items-center p-1.5" + > + + Snooze +
+
+
+
+ )} + )} - +
+ {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,7 +308,9 @@ export const NotificationCard: React.FC = (props) => {

) : ( -

{calculateTimeAgo(notification.created_at)}

+

+ {calculateTimeAgo(notification.created_at)} +

)}
@@ -164,6 +322,11 @@ export const NotificationCard: React.FC = (props) => { icon: , onClick: () => { markNotificationReadStatusToggle(notification.id).then(() => { + captureEvent(NOTIFICATIONS_READ, { + issue_id: notification.data.issue.id, + tab: selectedTab, + state: "SUCCESS", + }); setToastAlert({ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread", type: "success", @@ -181,6 +344,11 @@ export const NotificationCard: React.FC = (props) => { ), onClick: () => { markNotificationArchivedStatus(notification.id).then(() => { + captureEvent(NOTIFICATION_ARCHIVED, { + issue_id: notification.data.issue.id, + tab: selectedTab, + state: "SUCCESS", + }); setToastAlert({ title: notification.archived_at ? "Notification un-archived" : "Notification archived", type: "success", @@ -195,7 +363,6 @@ export const NotificationCard: React.FC = (props) => { onClick={(e) => { e.stopPropagation(); e.preventDefault(); - item.onClick(); }} key={item.id} @@ -228,6 +395,11 @@ export const NotificationCard: React.FC = (props) => { } markSnoozeNotification(notification.id, item.value).then(() => { + captureEvent(NOTIFICATION_SNOOZED, { + issue_id: notification.data.issue.id, + tab: selectedTab, + state: "SUCCESS", + }); setToastAlert({ title: `Notification snoozed till ${renderFormattedDate(item.value)}`, type: "success", diff --git a/web/components/notifications/notification-header.tsx b/web/components/notifications/notification-header.tsx index aecdd5d59..084e26abd 100644 --- a/web/components/notifications/notification-header.tsx +++ b/web/components/notifications/notification-header.tsx @@ -1,11 +1,22 @@ import React from "react"; import { ArrowLeft, CheckCheck, Clock, ListFilter, MoreVertical, RefreshCw, X } from "lucide-react"; +// components +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // ui import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui"; +// hooks +import { useEventTracker } from "hooks/store"; // helpers import { getNumberCount } from "helpers/string.helper"; // type import type { NotificationType, NotificationCount } from "@plane/types"; +// constants +import { + ARCHIVED_NOTIFICATIONS, + NOTIFICATIONS_READ, + SNOOZED_NOTIFICATIONS, + UNREAD_NOTIFICATIONS, +} from "constants/event-tracker"; type NotificationHeaderProps = { notificationCount?: NotificationCount | null; @@ -39,6 +50,8 @@ export const NotificationHeader: React.FC = (props) => setSelectedTab, markAllNotificationsAsRead, } = props; + // store hooks + const { captureEvent } = useEventTracker(); const notificationTabs: Array<{ label: string; @@ -65,7 +78,11 @@ export const NotificationHeader: React.FC = (props) => return ( <>
-

Notifications

+
+ +

Notifications

+
+
- +
+ + + +
@@ -165,7 +192,7 @@ export const NotificationHeader: React.FC = (props) => onClick={() => setSelectedTab(tab.value)} className={`whitespace-nowrap border-b-2 px-1 pb-4 text-sm font-medium outline-none ${ tab.value === selectedTab - ? "border-custom-primary-100 text-primary-text-subtle" + ? "border-primary-border-subtle text-primary-text-subtle" : "border-transparent text-neutral-text-medium" }`} > @@ -174,7 +201,7 @@ export const NotificationHeader: React.FC = (props) => diff --git a/web/components/notifications/notification-popover.tsx b/web/components/notifications/notification-popover.tsx index 4f09beb6c..803ff4a69 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"; @@ -16,8 +17,12 @@ import { getNumberCount } from "helpers/string.helper"; import { cn } from "helpers/common.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, @@ -45,8 +50,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 ( <> @@ -55,144 +63,143 @@ 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); + + <> + + + + + + 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... -
-

Loading notifications

-
- )} - {hasMore && !isLoadingMore && ( - - )} -
- ) : ( -
- 0 ? ( +
+
+ {notifications.map((notification) => ( + setIsActive(false)} + notification={notification} + markNotificationArchivedStatus={markNotificationArchivedStatus} + markNotificationReadStatus={markNotificationAsRead} + markNotificationReadStatusToggle={markNotificationReadStatus} + setSelectedNotificationForSnooze={setSelectedNotificationForSnooze} + markSnoozeNotification={markSnoozeNotification} /> + ))} +
+ {isLoadingMore && ( +
+
+ + Loading... +
+

Loading notifications

- ) - ) : ( - - - - - - - - )} - - - - ); - }} + )} + {hasMore && !isLoadingMore && ( + + )} +
+ ) : ( +
+ +
+ ) + ) : ( + + + + + + + + )} + + + ); diff --git a/web/components/notifications/select-snooze-till-modal.tsx b/web/components/notifications/select-snooze-till-modal.tsx index d35947a83..85799e1f2 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" > - +
@@ -156,8 +161,8 @@ export const SnoozeNotificationModal: FC = (props) => {
-
-
+
+
Pick a date
void; @@ -58,11 +60,19 @@ export const Invitations: React.FC = (props) => { if (invitationsRespond.length <= 0) return; setIsJoiningWorkspaces(true); + const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]); await workspaceService .joinWorkspaces({ invitations: invitationsRespond }) - .then(async (res) => { - captureEvent("Member accepted", { ...res, state: "SUCCESS", accepted_from: "App" }); + .then(async () => { + captureEvent(MEMBER_ACCEPTED, { + member_id: invitation?.id, + role: getUserRole(invitation?.role!), + project_id: undefined, + accepted_from: "App", + state: "SUCCESS", + element: "Workspace invitations page", + }); await fetchWorkspaces(); await mutate(USER_WORKSPACES); await updateLastWorkspace(); @@ -71,7 +81,14 @@ export const Invitations: React.FC = (props) => { }) .catch((error) => { console.error(error); - captureEvent("Member accepted", { state: "FAILED", accepted_from: "App" }); + captureEvent(MEMBER_ACCEPTED, { + member_id: invitation?.id, + role: getUserRole(invitation?.role!), + project_id: undefined, + accepted_from: "App", + state: "FAILED", + element: "Workspace invitations page", + }); }) .finally(() => setIsJoiningWorkspaces(false)); }; diff --git a/web/components/onboarding/invite-members.tsx b/web/components/onboarding/invite-members.tsx index 77104b261..827d0c932 100644 --- a/web/components/onboarding/invite-members.tsx +++ b/web/components/onboarding/invite-members.tsx @@ -18,6 +18,7 @@ import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; import { WorkspaceService } from "services/workspace.service"; // hooks import useToast from "hooks/use-toast"; +import { useEventTracker } from "hooks/store"; // ui import { Button, Input } from "@plane/ui"; // components @@ -28,6 +29,9 @@ import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +import { MEMBER_INVITED } from "constants/event-tracker"; +// helpers +import { getUserRole } from "helpers/user.helper"; // assets import user1 from "public/users/user-1.png"; import user2 from "public/users/user-2.png"; @@ -267,6 +271,8 @@ export const InviteMembers: React.FC = (props) => { const { setToastAlert } = useToast(); const { resolvedTheme } = useTheme(); + // store hooks + const { captureEvent } = useEventTracker(); const { control, @@ -305,6 +311,17 @@ export const InviteMembers: React.FC = (props) => { })), }) .then(async () => { + captureEvent(MEMBER_INVITED, { + emails: [ + ...payload.emails.map((email) => ({ + email: email.email, + role: getUserRole(email.role), + })), + ], + project_id: undefined, + state: "SUCCESS", + element: "Onboarding", + }); setToastAlert({ type: "success", title: "Success!", @@ -313,13 +330,18 @@ export const InviteMembers: React.FC = (props) => { await nextStep(); }) - .catch((err) => + .catch((err) => { + captureEvent(MEMBER_INVITED, { + project_id: undefined, + state: "FAILED", + element: "Onboarding", + }); setToastAlert({ type: "error", title: "Error!", message: err?.error, - }) - ); + }); + }); }; const appendField = () => { diff --git a/web/components/onboarding/tour/root.tsx b/web/components/onboarding/tour/root.tsx index 487b0412e..57e66e88a 100644 --- a/web/components/onboarding/tour/root.tsx +++ b/web/components/onboarding/tour/root.tsx @@ -15,6 +15,8 @@ import CyclesTour from "public/onboarding/cycles.webp"; import ModulesTour from "public/onboarding/modules.webp"; import ViewsTour from "public/onboarding/views.webp"; import PagesTour from "public/onboarding/pages.webp"; +// constants +import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker"; type Props = { onComplete: () => void; @@ -79,7 +81,7 @@ export const TourRoot: React.FC = observer((props) => { const [step, setStep] = useState("welcome"); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureEvent } = useEventTracker(); const { currentUser } = useUser(); const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step); @@ -103,13 +105,22 @@ export const TourRoot: React.FC = observer((props) => {

- @@ -156,8 +167,8 @@ export const TourRoot: React.FC = observer((props) => { )} diff --git a/web/pages/accounts/forgot-password.tsx b/web/pages/accounts/forgot-password.tsx index ef2945200..fc76a7a4c 100644 --- a/web/pages/accounts/forgot-password.tsx +++ b/web/pages/accounts/forgot-password.tsx @@ -7,6 +7,7 @@ import { AuthService } from "services/auth.service"; // hooks import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; +import { useEventTracker } from "hooks/store"; // layouts import DefaultLayout from "layouts/default-layout"; // components @@ -19,6 +20,7 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import { checkEmailValidity } from "helpers/string.helper"; // type import { NextPageWithLayout } from "lib/types"; +import { FORGOT_PASS_LINK } from "constants/event-tracker"; type TForgotPasswordFormValues = { email: string; @@ -35,6 +37,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => { // router const router = useRouter(); const { email } = router.query; + // store hooks + const { captureEvent } = useEventTracker(); // toast const { setToastAlert } = useToast(); // timer @@ -57,6 +61,9 @@ const ForgotPasswordPage: NextPageWithLayout = () => { email: formData.email, }) .then(() => { + captureEvent(FORGOT_PASS_LINK, { + state: "SUCCESS", + }); setToastAlert({ type: "success", title: "Email sent", @@ -65,13 +72,16 @@ const ForgotPasswordPage: NextPageWithLayout = () => { }); setResendCodeTimer(30); }) - .catch((err) => + .catch((err) => { + captureEvent(FORGOT_PASS_LINK, { + state: "FAILED", + }); setToastAlert({ type: "error", title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", - }) - ); + }); + }); }; return ( diff --git a/web/pages/accounts/reset-password.tsx b/web/pages/accounts/reset-password.tsx index 8bdf921c6..997007e7c 100644 --- a/web/pages/accounts/reset-password.tsx +++ b/web/pages/accounts/reset-password.tsx @@ -7,6 +7,7 @@ import { AuthService } from "services/auth.service"; // hooks import useToast from "hooks/use-toast"; import useSignInRedirection from "hooks/use-sign-in-redirection"; +import { useEventTracker } from "hooks/store"; // layouts import DefaultLayout from "layouts/default-layout"; // components @@ -21,6 +22,8 @@ import { checkEmailValidity } from "helpers/string.helper"; import { NextPageWithLayout } from "lib/types"; // icons import { Eye, EyeOff } from "lucide-react"; +// constants +import { NEW_PASS_CREATED } from "constants/event-tracker"; type TResetPasswordFormValues = { email: string; @@ -41,6 +44,8 @@ const ResetPasswordPage: NextPageWithLayout = () => { const { uidb64, token, email } = router.query; // states const [showPassword, setShowPassword] = useState(false); + // store hooks + const { captureEvent } = useEventTracker(); // toast const { setToastAlert } = useToast(); // sign in redirection hook @@ -66,14 +71,22 @@ const ResetPasswordPage: NextPageWithLayout = () => { await authService .resetPassword(uidb64.toString(), token.toString(), payload) - .then(() => handleRedirection()) - .catch((err) => + .then(() => { + captureEvent(NEW_PASS_CREATED, { + state: "SUCCESS", + }); + handleRedirection(); + }) + .catch((err) => { + captureEvent(NEW_PASS_CREATED, { + state: "FAILED", + }); setToastAlert({ type: "error", title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", - }) - ); + }); + }); }; return ( diff --git a/web/pages/invitations/index.tsx b/web/pages/invitations/index.tsx index 0ef11dea8..7e477b77d 100644 --- a/web/pages/invitations/index.tsx +++ b/web/pages/invitations/index.tsx @@ -23,11 +23,13 @@ import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-l import emptyInvitation from "public/empty-state/invitation.svg"; // helpers import { truncateText } from "helpers/string.helper"; +import { getUserRole } from "helpers/user.helper"; // types import { NextPageWithLayout } from "lib/types"; import type { IWorkspaceMemberInvitation } from "@plane/types"; // constants import { ROLE } from "constants/workspace"; +import { MEMBER_ACCEPTED } from "constants/event-tracker"; // components import { EmptyState } from "components/common"; @@ -40,7 +42,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { const [invitationsRespond, setInvitationsRespond] = useState([]); const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); // store hooks - const { captureEvent } = useEventTracker(); + const { captureEvent, joinWorkspaceMetricGroup } = useEventTracker(); const { currentUser, currentUserSettings } = useUser(); // router const router = useRouter(); @@ -81,11 +83,16 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { .then((res) => { mutate("USER_WORKSPACES"); const firstInviteId = invitationsRespond[0]; + const invitation = invitations?.find((i) => i.id === firstInviteId); const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace; - captureEvent("Member accepted", { - ...res, - state: "SUCCESS", + joinWorkspaceMetricGroup(redirectWorkspace?.id); + captureEvent(MEMBER_ACCEPTED, { + member_id: invitation?.id, + role: getUserRole(invitation?.role!), + project_id: undefined, accepted_from: "App", + state: "SUCCESS", + element: "Workspace invitations page", }); userService .updateUser({ last_workspace_id: redirectWorkspace?.id }) @@ -103,6 +110,12 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { }); }) .catch(() => { + captureEvent(MEMBER_ACCEPTED, { + project_id: undefined, + accepted_from: "App", + state: "FAILED", + element: "Workspace invitations page", + }); setToastAlert({ type: "error", title: "Error!", diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index bcca846e8..8c868ad5f 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -24,6 +24,8 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types import { IUser, TOnboardingSteps } from "@plane/types"; import { NextPageWithLayout } from "lib/types"; +// constants +import { USER_ONBOARDING_COMPLETED } from "constants/event-tracker"; // services const workspaceService = new WorkspaceService(); @@ -79,7 +81,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => { await updateUserOnBoard() .then(() => { - captureEvent("User onboarding completed", { + captureEvent(USER_ONBOARDING_COMPLETED, { user_role: user.role, email: user.email, user_id: user.id, diff --git a/web/store/event-tracker.store.ts b/web/store/event-tracker.store.ts index 89e279c40..744ad44fb 100644 --- a/web/store/event-tracker.store.ts +++ b/web/store/event-tracker.store.ts @@ -3,39 +3,49 @@ import posthog from "posthog-js"; // stores import { RootStore } from "./root.store"; import { - EventGroupProps, + GROUP_WORKSPACE, + WORKSPACE_CREATED, EventProps, IssueEventProps, getCycleEventPayload, getIssueEventPayload, getModuleEventPayload, getProjectEventPayload, + getProjectStateEventPayload, + getWorkspaceEventPayload, + getPageEventPayload, } from "constants/event-tracker"; export interface IEventTrackerStore { // properties - trackElement: string; + trackElement: string | undefined; // computed - getRequiredPayload: any; + getRequiredProperties: any; // actions + resetSession: () => void; setTrackElement: (element: string) => void; - captureEvent: (eventName: string, payload: object | [] | null, group?: EventGroupProps) => void; + captureEvent: (eventName: string, payload?: any) => void; + joinWorkspaceMetricGroup: (workspaceId?: string) => void; + captureWorkspaceEvent: (props: EventProps) => void; captureProjectEvent: (props: EventProps) => void; captureCycleEvent: (props: EventProps) => void; captureModuleEvent: (props: EventProps) => void; + capturePageEvent: (props: EventProps) => void; captureIssueEvent: (props: IssueEventProps) => void; + captureProjectStateEvent: (props: EventProps) => void; } export class EventTrackerStore implements IEventTrackerStore { - trackElement: string = ""; + trackElement: string | undefined = undefined; rootStore; constructor(_rootStore: RootStore) { makeObservable(this, { // properties trackElement: observable, // computed - getRequiredPayload: computed, + getRequiredProperties: computed, // actions + resetSession: action, setTrackElement: action, captureEvent: action, captureProjectEvent: action, @@ -48,12 +58,12 @@ export class EventTrackerStore implements IEventTrackerStore { /** * @description: Returns the necessary property for the event tracking */ - get getRequiredPayload() { + get getRequiredProperties() { const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace; const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails; return { - workspace_id: currentWorkspaceDetails?.id ?? "", - project_id: currentProjectDetails?.id ?? "", + workspace_id: currentWorkspaceDetails?.id, + project_id: currentProjectDetails?.id, }; } @@ -61,42 +71,74 @@ export class EventTrackerStore implements IEventTrackerStore { * @description: Set the trigger point of event. * @param {string} element */ - setTrackElement = (element: string) => { + setTrackElement = (element?: string) => { this.trackElement = element; }; - postHogGroup = (group: EventGroupProps) => { - if (group && group!.isGrouping === true) { - posthog?.group(group!.groupType!, group!.groupId!, { - date: new Date(), - workspace_id: group!.groupId, - }); - } + /** + * @description: Reset the session. + */ + resetSession = () => { + posthog?.reset(); }; - captureEvent = (eventName: string, payload: object | [] | null) => { - posthog?.capture(eventName, { - ...payload, - element: this.trackElement ?? "", + /** + * @description: Creates the workspace metric group. + * @param {string} userEmail + * @param {string} workspaceId + */ + joinWorkspaceMetricGroup = (workspaceId?: string) => { + if (!workspaceId) return; + posthog?.group(GROUP_WORKSPACE, workspaceId, { + date: new Date().toDateString(), + workspace_id: workspaceId, }); }; + /** + * @description: Captures the event. + * @param {string} eventName + * @param {any} payload + */ + captureEvent = (eventName: string, payload?: any) => { + posthog?.capture(eventName, { + ...this.getRequiredProperties, + ...payload, + element: payload?.element ?? this.trackElement, + }); + this.setTrackElement(undefined); + }; + + /** + * @description: Captures the workspace crud related events. + * @param {EventProps} props + */ + captureWorkspaceEvent = (props: EventProps) => { + const { eventName, payload } = props; + if (eventName === WORKSPACE_CREATED && payload.state == "SUCCESS") { + this.joinWorkspaceMetricGroup(payload.id); + } + const eventPayload: any = getWorkspaceEventPayload({ + ...payload, + element: payload.element ?? this.trackElement, + }); + posthog?.capture(eventName, eventPayload); + this.setTrackElement(undefined); + }; + /** * @description: Captures the project related events. * @param {EventProps} props */ captureProjectEvent = (props: EventProps) => { - const { eventName, payload, group } = props; - if (group) { - this.postHogGroup(group); - } + const { eventName, payload } = props; const eventPayload: any = getProjectEventPayload({ - ...this.getRequiredPayload, + ...this.getRequiredProperties, ...payload, element: payload.element ?? this.trackElement, }); posthog?.capture(eventName, eventPayload); - this.setTrackElement(""); + this.setTrackElement(undefined); }; /** @@ -104,17 +146,14 @@ export class EventTrackerStore implements IEventTrackerStore { * @param {EventProps} props */ captureCycleEvent = (props: EventProps) => { - const { eventName, payload, group } = props; - if (group) { - this.postHogGroup(group); - } + const { eventName, payload } = props; const eventPayload: any = getCycleEventPayload({ - ...this.getRequiredPayload, + ...this.getRequiredProperties, ...payload, element: payload.element ?? this.trackElement, }); posthog?.capture(eventName, eventPayload); - this.setTrackElement(""); + this.setTrackElement(undefined); }; /** @@ -122,17 +161,29 @@ export class EventTrackerStore implements IEventTrackerStore { * @param {EventProps} props */ captureModuleEvent = (props: EventProps) => { - const { eventName, payload, group } = props; - if (group) { - this.postHogGroup(group); - } + const { eventName, payload } = props; const eventPayload: any = getModuleEventPayload({ - ...this.getRequiredPayload, + ...this.getRequiredProperties, ...payload, element: payload.element ?? this.trackElement, }); posthog?.capture(eventName, eventPayload); - this.setTrackElement(""); + this.setTrackElement(undefined); + }; + + /** + * @description: Captures the project pages related events. + * @param {EventProps} props + */ + capturePageEvent = (props: EventProps) => { + const { eventName, payload } = props; + const eventPayload: any = getPageEventPayload({ + ...this.getRequiredProperties, + ...payload, + element: payload.element ?? this.trackElement, + }); + posthog?.capture(eventName, eventPayload); + this.setTrackElement(undefined); }; /** @@ -140,16 +191,29 @@ export class EventTrackerStore implements IEventTrackerStore { * @param {IssueEventProps} props */ captureIssueEvent = (props: IssueEventProps) => { - const { eventName, payload, group } = props; - if (group) { - this.postHogGroup(group); - } + const { eventName, payload } = props; const eventPayload: any = { ...getIssueEventPayload(props), - ...this.getRequiredPayload, + ...this.getRequiredProperties, state_group: this.rootStore.state.getStateById(payload.state_id)?.group ?? "", element: payload.element ?? this.trackElement, }; posthog?.capture(eventName, eventPayload); + this.setTrackElement(undefined); + }; + + /** + * @description: Captures the issue related events. + * @param {IssueEventProps} props + */ + captureProjectStateEvent = (props: EventProps) => { + const { eventName, payload } = props; + const eventPayload: any = getProjectStateEventPayload({ + ...this.getRequiredProperties, + ...payload, + element: payload.element ?? this.trackElement, + }); + posthog?.capture(eventName, eventPayload); + this.setTrackElement(undefined); }; } 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; diff --git a/web/store/user/index.ts b/web/store/user/index.ts index b07764a05..15f9e5772 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -250,6 +250,7 @@ export class UserRootStore implements IUserRootStore { this.isUserLoggedIn = false; }); this.membership = new UserMembershipStore(this.rootStore); + this.rootStore.eventTracker.resetSession(); this.rootStore.resetOnSignout(); }); @@ -264,6 +265,7 @@ export class UserRootStore implements IUserRootStore { this.isUserLoggedIn = false; }); this.membership = new UserMembershipStore(this.rootStore); + this.rootStore.eventTracker.resetSession(); this.rootStore.resetOnSignout(); }); }