fix: merge conflicts resolved

This commit is contained in:
sriram veeraghanta 2024-02-09 16:30:05 +05:30
commit b86c6c906a
85 changed files with 1474 additions and 565 deletions

View File

@ -401,8 +401,8 @@ class EmailCheckEndpoint(BaseAPIView):
email=email, email=email,
user_agent=request.META.get("HTTP_USER_AGENT"), user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"), ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN", event_name="Sign up",
medium="MAGIC_LINK", medium="Magic link",
first_time=True, first_time=True,
) )
key, token, current_attempt = generate_magic_token(email=email) key, token, current_attempt = generate_magic_token(email=email)
@ -438,8 +438,8 @@ class EmailCheckEndpoint(BaseAPIView):
email=email, email=email,
user_agent=request.META.get("HTTP_USER_AGENT"), user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"), ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN", event_name="Sign in",
medium="MAGIC_LINK", medium="Magic link",
first_time=False, first_time=False,
) )
@ -468,8 +468,8 @@ class EmailCheckEndpoint(BaseAPIView):
email=email, email=email,
user_agent=request.META.get("HTTP_USER_AGENT"), user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"), ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN", event_name="Sign in",
medium="EMAIL", medium="Email",
first_time=False, first_time=False,
) )

View File

@ -274,8 +274,8 @@ class SignInEndpoint(BaseAPIView):
email=email, email=email,
user_agent=request.META.get("HTTP_USER_AGENT"), user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"), ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN", event_name="Sign in",
medium="EMAIL", medium="Email",
first_time=False, first_time=False,
) )
@ -349,8 +349,8 @@ class MagicSignInEndpoint(BaseAPIView):
email=email, email=email,
user_agent=request.META.get("HTTP_USER_AGENT"), user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"), ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN", event_name="Sign in",
medium="MAGIC_LINK", medium="Magic link",
first_time=False, first_time=False,
) )

View File

@ -296,7 +296,7 @@ class OauthEndpoint(BaseAPIView):
email=email, email=email,
user_agent=request.META.get("HTTP_USER_AGENT"), user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"), ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN", event_name="Sign in",
medium=medium.upper(), medium=medium.upper(),
first_time=False, first_time=False,
) )
@ -427,7 +427,7 @@ class OauthEndpoint(BaseAPIView):
email=email, email=email,
user_agent=request.META.get("HTTP_USER_AGENT"), user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"), ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN", event_name="Sign up",
medium=medium.upper(), medium=medium.upper(),
first_time=True, first_time=True,
) )

View File

@ -4,12 +4,14 @@ import { Controller, useForm } from "react-hook-form";
import { AuthService } from "services/auth.service"; import { AuthService } from "services/auth.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useEventTracker } from "hooks/store";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// helpers // helpers
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// icons // icons
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED } from "constants/event-tracker";
type Props = { type Props = {
email: string; email: string;
@ -34,6 +36,8 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
// states // states
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false); const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// form info // form info
@ -63,21 +67,34 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
title: "Success!", title: "Success!",
message: "Password created successfully.", message: "Password created successfully.",
}); });
captureEvent(PASSWORD_CREATE_SELECTED, {
state: "SUCCESS",
first_time: false,
});
await handleSignInRedirection(); await handleSignInRedirection();
}) })
.catch((err) => .catch((err) => {
captureEvent(PASSWORD_CREATE_SELECTED, {
state: "FAILED",
first_time: false,
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.", message: err?.error ?? "Something went wrong. Please try again.",
}) });
); });
}; };
const handleGoToWorkspace = async () => { const handleGoToWorkspace = async () => {
setIsGoingToWorkspace(true); setIsGoingToWorkspace(true);
await handleSignInRedirection().finally(() => {
await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false)); captureEvent(PASSWORD_CREATE_SKIPPED, {
state: "SUCCESS",
first_time: false,
});
setIsGoingToWorkspace(false);
});
}; };
return ( return (

View File

@ -7,7 +7,7 @@ import { Eye, EyeOff, XCircle } from "lucide-react";
import { AuthService } from "services/auth.service"; import { AuthService } from "services/auth.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useApplication } from "hooks/store"; import { useApplication, useEventTracker } from "hooks/store";
// components // components
import { ESignInSteps, ForgotPasswordPopover } from "components/account"; import { ESignInSteps, ForgotPasswordPopover } from "components/account";
// ui // ui
@ -16,6 +16,8 @@ import { Button, Input } from "@plane/ui";
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// types // types
import { IPasswordSignInData } from "@plane/types"; import { IPasswordSignInData } from "@plane/types";
// constants
import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker";
type Props = { type Props = {
email: string; email: string;
@ -46,6 +48,7 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
const { const {
config: { envConfig }, config: { envConfig },
} = useApplication(); } = useApplication();
const { captureEvent } = useEventTracker();
// derived values // derived values
const isSmtpConfigured = envConfig?.is_smtp_configured; const isSmtpConfigured = envConfig?.is_smtp_configured;
// form info // form info
@ -72,7 +75,13 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
await authService await authService
.passwordSignIn(payload) .passwordSignIn(payload)
.then(async () => await onSubmit()) .then(async () => {
captureEvent(SIGN_IN_WITH_PASSWORD, {
state: "SUCCESS",
first_time: false,
});
await onSubmit();
})
.catch((err) => .catch((err) =>
setToastAlert({ setToastAlert({
type: "error", type: "error",
@ -182,9 +191,10 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
</div> </div>
)} )}
/> />
<div className="w-full text-right mt-2 pb-3"> <div className="mt-2 w-full pb-3 text-right">
{isSmtpConfigured ? ( {isSmtpConfigured ? (
<Link <Link
onClick={() => captureEvent(FORGOT_PASSWORD)}
href={`/accounts/forgot-password?email=${email}`} href={`/accounts/forgot-password?email=${email}`}
className="text-xs font-medium text-custom-primary-100" className="text-xs font-medium text-custom-primary-100"
> >

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useApplication } from "hooks/store"; import { useApplication, useEventTracker } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection"; import useSignInRedirection from "hooks/use-sign-in-redirection";
// components // components
import { LatestFeatureBlock } from "components/common"; import { LatestFeatureBlock } from "components/common";
@ -13,6 +13,8 @@ import {
OAuthOptions, OAuthOptions,
SignInOptionalSetPasswordForm, SignInOptionalSetPasswordForm,
} from "components/account"; } from "components/account";
// constants
import { NAVIGATE_TO_SIGNUP } from "constants/event-tracker";
export enum ESignInSteps { export enum ESignInSteps {
EMAIL = "EMAIL", EMAIL = "EMAIL",
@ -32,6 +34,7 @@ export const SignInRoot = observer(() => {
const { const {
config: { envConfig }, config: { envConfig },
} = useApplication(); } = useApplication();
const { captureEvent } = useEventTracker();
// derived values // derived values
const isSmtpConfigured = envConfig?.is_smtp_configured; const isSmtpConfigured = envConfig?.is_smtp_configured;
@ -110,7 +113,11 @@ export const SignInRoot = observer(() => {
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_in" /> <OAuthOptions handleSignInRedirection={handleRedirection} type="sign_in" />
<p className="text-xs text-onboarding-text-300 text-center mt-6"> <p className="text-xs text-onboarding-text-300 text-center mt-6">
Don{"'"}t have an account?{" "} Don{"'"}t have an account?{" "}
<Link href="/accounts/sign-up" className="text-custom-primary-100 font-medium underline"> <Link
href="/accounts/sign-up"
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
className="text-custom-primary-100 font-medium underline"
>
Sign up Sign up
</Link> </Link>
</p> </p>

View File

@ -7,12 +7,15 @@ import { UserService } from "services/user.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useTimer from "hooks/use-timer"; import useTimer from "hooks/use-timer";
import { useEventTracker } from "hooks/store";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// helpers // helpers
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// types // types
import { IEmailCheckData, IMagicSignInData } from "@plane/types"; import { IEmailCheckData, IMagicSignInData } from "@plane/types";
// constants
import { CODE_VERIFIED } from "constants/event-tracker";
type Props = { type Props = {
email: string; email: string;
@ -41,6 +44,8 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// store hooks
const { captureEvent } = useEventTracker();
// timer // timer
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30); const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
// form info // form info
@ -69,17 +74,22 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
await authService await authService
.magicSignIn(payload) .magicSignIn(payload)
.then(async () => { .then(async () => {
captureEvent(CODE_VERIFIED, {
state: "SUCCESS",
});
const currentUser = await userService.currentUser(); const currentUser = await userService.currentUser();
await onSubmit(currentUser.is_password_autoset); await onSubmit(currentUser.is_password_autoset);
}) })
.catch((err) => .catch((err) => {
captureEvent(CODE_VERIFIED, {
state: "FAILED",
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.", message: err?.error ?? "Something went wrong. Please try again.",
}) });
); });
}; };
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => { const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {

View File

@ -4,12 +4,14 @@ import { Controller, useForm } from "react-hook-form";
import { AuthService } from "services/auth.service"; import { AuthService } from "services/auth.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useEventTracker } from "hooks/store";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// helpers // helpers
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// constants // constants
import { ESignUpSteps } from "components/account"; import { ESignUpSteps } from "components/account";
import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker";
// icons // icons
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
@ -37,6 +39,8 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
// states // states
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false); const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// form info // form info
@ -66,21 +70,34 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
title: "Success!", title: "Success!",
message: "Password created successfully.", message: "Password created successfully.",
}); });
captureEvent(SETUP_PASSWORD, {
state: "SUCCESS",
first_time: true,
});
await handleSignInRedirection(); await handleSignInRedirection();
}) })
.catch((err) => .catch((err) => {
captureEvent(SETUP_PASSWORD, {
state: "FAILED",
first_time: true,
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.", message: err?.error ?? "Something went wrong. Please try again.",
}) });
); });
}; };
const handleGoToWorkspace = async () => { const handleGoToWorkspace = async () => {
setIsGoingToWorkspace(true); setIsGoingToWorkspace(true);
await handleSignInRedirection().finally(() => {
await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false)); captureEvent(PASSWORD_CREATE_SKIPPED, {
state: "SUCCESS",
first_time: true,
});
setIsGoingToWorkspace(false);
});
}; };
return ( return (

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useApplication } from "hooks/store"; import { useApplication, useEventTracker } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection"; import useSignInRedirection from "hooks/use-sign-in-redirection";
// components // components
import { import {
@ -12,6 +12,8 @@ import {
SignUpUniqueCodeForm, SignUpUniqueCodeForm,
} from "components/account"; } from "components/account";
import Link from "next/link"; import Link from "next/link";
// constants
import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker";
export enum ESignUpSteps { export enum ESignUpSteps {
EMAIL = "EMAIL", EMAIL = "EMAIL",
@ -32,6 +34,7 @@ export const SignUpRoot = observer(() => {
const { const {
config: { envConfig }, config: { envConfig },
} = useApplication(); } = useApplication();
const { captureEvent } = useEventTracker();
// step 1 submit handler- email verification // step 1 submit handler- email verification
const handleEmailVerification = () => setSignInStep(ESignUpSteps.UNIQUE_CODE); const handleEmailVerification = () => setSignInStep(ESignUpSteps.UNIQUE_CODE);
@ -86,7 +89,11 @@ export const SignUpRoot = observer(() => {
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_up" /> <OAuthOptions handleSignInRedirection={handleRedirection} type="sign_up" />
<p className="text-xs text-onboarding-text-300 text-center mt-6"> <p className="text-xs text-onboarding-text-300 text-center mt-6">
Already using Plane?{" "} Already using Plane?{" "}
<Link href="/" className="text-custom-primary-100 font-medium underline"> <Link
href="/"
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
className="text-custom-primary-100 font-medium underline"
>
Sign in Sign in
</Link> </Link>
</p> </p>

View File

@ -8,12 +8,15 @@ import { UserService } from "services/user.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useTimer from "hooks/use-timer"; import useTimer from "hooks/use-timer";
import { useEventTracker } from "hooks/store";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// helpers // helpers
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// types // types
import { IEmailCheckData, IMagicSignInData } from "@plane/types"; import { IEmailCheckData, IMagicSignInData } from "@plane/types";
// constants
import { CODE_VERIFIED } from "constants/event-tracker";
type Props = { type Props = {
email: string; email: string;
@ -39,6 +42,8 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
const { email, handleEmailClear, onSubmit } = props; const { email, handleEmailClear, onSubmit } = props;
// states // states
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// timer // timer
@ -69,17 +74,22 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
await authService await authService
.magicSignIn(payload) .magicSignIn(payload)
.then(async () => { .then(async () => {
captureEvent(CODE_VERIFIED, {
state: "SUCCESS",
});
const currentUser = await userService.currentUser(); const currentUser = await userService.currentUser();
await onSubmit(currentUser.is_password_autoset); await onSubmit(currentUser.is_password_autoset);
}) })
.catch((err) => .catch((err) => {
captureEvent(CODE_VERIFIED, {
state: "FAILED",
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.", message: err?.error ?? "Something went wrong. Please try again.",
}) });
); });
}; };
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => { const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
@ -96,7 +106,6 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
title: "Success!", title: "Success!",
message: "A new unique code has been sent to your email.", message: "A new unique code has been sent to your email.",
}); });
reset({ reset({
email: formData.email, email: formData.email,
token: "", token: "",

View File

@ -16,6 +16,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
// constants // constants
import { CYCLE_STATUS } from "constants/cycle"; import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker";
//.types //.types
import { TCycleGroups } from "@plane/types"; import { TCycleGroups } from "@plane/types";
@ -33,7 +34,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
// router // router
const router = useRouter(); const router = useRouter();
// store // store
const { setTrackElement } = useEventTracker(); const { setTrackElement, captureEvent } = useEventTracker();
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
@ -90,7 +91,15 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
captureEvent(CYCLE_FAVORITED, {
cycle_id: cycleId,
element: "Grid layout",
state: "SUCCESS",
});
})
.catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -103,7 +112,15 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
captureEvent(CYCLE_UNFAVORITED, {
cycle_id: cycleId,
element: "Grid layout",
state: "SUCCESS",
});
})
.catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -115,14 +132,14 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => { const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setTrackElement("Cycles page board layout"); setTrackElement("Cycles page grid layout");
setUpdateModal(true); setUpdateModal(true);
}; };
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => { const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setTrackElement("Cycles page board layout"); setTrackElement("Cycles page grid layout");
setDeleteModal(true); setDeleteModal(true);
}; };

View File

@ -18,6 +18,7 @@ import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
// types // types
import { TCycleGroups } from "@plane/types"; import { TCycleGroups } from "@plane/types";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker";
type TCyclesListItem = { type TCyclesListItem = {
cycleId: string; cycleId: string;
@ -37,7 +38,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
// router // router
const router = useRouter(); const router = useRouter();
// store hooks // store hooks
const { setTrackElement } = useEventTracker(); const { setTrackElement, captureEvent } = useEventTracker();
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
@ -63,7 +64,15 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
captureEvent(CYCLE_FAVORITED, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
});
})
.catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -76,7 +85,15 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
captureEvent(CYCLE_UNFAVORITED, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
});
})
.catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -159,9 +176,9 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
projectId={projectId} projectId={projectId}
/> />
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="group flex flex-col md:flex-row w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90"> <div className="group flex w-full flex-col items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90 md:flex-row">
<div className="relative w-full flex items-center justify-between gap-3 overflow-hidden"> <div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
<div className="relative w-full flex items-center gap-3 overflow-hidden"> <div className="relative flex w-full items-center gap-3 overflow-hidden">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}> <CircularProgressIndicator size={38} percentage={progress}>
{isCompleted ? ( {isCompleted ? (
@ -181,20 +198,20 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
<div className="relative flex items-center gap-2.5 overflow-hidden"> <div className="relative flex items-center gap-2.5 overflow-hidden">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" /> <CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
<Tooltip tooltipContent={cycleDetails.name} position="top"> <Tooltip tooltipContent={cycleDetails.name} position="top">
<span className="truncate line-clamp-1 inline-block overflow-hidden text-base font-medium"> <span className="line-clamp-1 inline-block overflow-hidden truncate text-base font-medium">
{cycleDetails.name} {cycleDetails.name}
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
<button onClick={openCycleOverview} className="flex-shrink-0 z-10 invisible group-hover:visible"> <button onClick={openCycleOverview} className="invisible z-10 flex-shrink-0 group-hover:visible">
<Info className="h-4 w-4 text-custom-text-400" /> <Info className="h-4 w-4 text-custom-text-400" />
</button> </button>
</div> </div>
{currentCycle && ( {currentCycle && (
<div <div
className="flex-shrink-0 relative flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs" className="relative flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
style={{ style={{
color: currentCycle.color, color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`, backgroundColor: `${currentCycle.color}20`,
@ -206,12 +223,12 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</div> </div>
)} )}
</div> </div>
<div className="flex-shrink-0 relative overflow-hidden flex w-full items-center justify-between md:justify-end gap-2.5 md:w-auto md:flex-shrink-0 "> <div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 overflow-hidden md:w-auto md:flex-shrink-0 md:justify-end ">
<div className="text-xs text-custom-text-300"> <div className="text-xs text-custom-text-300">
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`} {renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
</div> </div>
<div className="flex-shrink-0 relative flex items-center gap-3"> <div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}> <Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
<div className="flex w-10 cursor-default items-center justify-center"> <div className="flex w-10 cursor-default items-center justify-center">
{cycleDetails.assignees.length > 0 ? ( {cycleDetails.assignees.length > 0 ? (

View File

@ -10,6 +10,8 @@ import useToast from "hooks/use-toast";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
import { ICycle } from "@plane/types"; import { ICycle } from "@plane/types";
// constants
import { CYCLE_DELETED } from "constants/event-tracker";
interface ICycleDelete { interface ICycleDelete {
cycle: ICycle; cycle: ICycle;
@ -45,13 +47,13 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
message: "Cycle deleted successfully.", message: "Cycle deleted successfully.",
}); });
captureCycleEvent({ captureCycleEvent({
eventName: "Cycle deleted", eventName: CYCLE_DELETED,
payload: { ...cycle, state: "SUCCESS" }, payload: { ...cycle, state: "SUCCESS" },
}); });
}) })
.catch(() => { .catch(() => {
captureCycleEvent({ captureCycleEvent({
eventName: "Cycle deleted", eventName: CYCLE_DELETED,
payload: { ...cycle, state: "FAILED" }, payload: { ...cycle, state: "FAILED" },
}); });
}); });

View File

@ -10,7 +10,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { ICycle } from "@plane/types"; import { ICycle } from "@plane/types";
type Props = { type Props = {
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>; handleFormSubmit: (values: Partial<ICycle>, dirtyFields: any) => Promise<void>;
handleClose: () => void; handleClose: () => void;
status: boolean; status: boolean;
projectId: string; projectId: string;
@ -29,7 +29,7 @@ export const CycleForm: React.FC<Props> = (props) => {
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props; const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
// form data // form data
const { const {
formState: { errors, isSubmitting }, formState: { errors, isSubmitting, dirtyFields },
handleSubmit, handleSubmit,
control, control,
watch, watch,
@ -61,7 +61,7 @@ export const CycleForm: React.FC<Props> = (props) => {
maxDate?.setDate(maxDate.getDate() - 1); maxDate?.setDate(maxDate.getDate() - 1);
return ( return (
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit((formData)=>handleFormSubmit(formData,dirtyFields))}>
<div className="space-y-5"> <div className="space-y-5">
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
{!status && ( {!status && (

View File

@ -10,6 +10,8 @@ import useLocalStorage from "hooks/use-local-storage";
import { CycleForm } from "components/cycles"; import { CycleForm } from "components/cycles";
// types // types
import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types"; import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types";
// constants
import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker";
type CycleModalProps = { type CycleModalProps = {
isOpen: boolean; isOpen: boolean;
@ -47,7 +49,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
message: "Cycle created successfully.", message: "Cycle created successfully.",
}); });
captureCycleEvent({ captureCycleEvent({
eventName: "Cycle created", eventName: CYCLE_CREATED,
payload: { ...res, state: "SUCCESS" }, payload: { ...res, state: "SUCCESS" },
}); });
}) })
@ -58,18 +60,23 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
message: err.detail ?? "Error in creating cycle. Please try again.", message: err.detail ?? "Error in creating cycle. Please try again.",
}); });
captureCycleEvent({ captureCycleEvent({
eventName: "Cycle created", eventName: CYCLE_CREATED,
payload: { ...payload, state: "FAILED" }, payload: { ...payload, state: "FAILED" },
}); });
}); });
}; };
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>) => { const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>, dirtyFields: any) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const selectedProjectId = payload.project ?? projectId.toString(); const selectedProjectId = payload.project ?? projectId.toString();
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload) 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({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -77,6 +84,10 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
}); });
}) })
.catch((err) => { .catch((err) => {
captureCycleEvent({
eventName: CYCLE_UPDATED,
payload: { ...payload, state: "FAILED" },
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -95,7 +106,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
return status; return status;
}; };
const handleFormSubmit = async (formData: Partial<ICycle>) => { const handleFormSubmit = async (formData: Partial<ICycle>, dirtyFields: any) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const payload: Partial<ICycle> = { const payload: Partial<ICycle> = {
@ -119,7 +130,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
} }
if (isDateValid) { if (isDateValid) {
if (data) await handleUpdateCycle(data.id, payload); if (data) await handleUpdateCycle(data.id, payload, dirtyFields);
else { else {
await handleCreateCycle(payload).then(() => { await handleCreateCycle(payload).then(() => {
setCycleTab("all"); setCycleTab("all");

View File

@ -39,6 +39,7 @@ import {
import { ICycle } from "@plane/types"; import { ICycle } from "@plane/types";
// constants // constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { CYCLE_UPDATED } from "constants/event-tracker";
// fetch-keys // fetch-keys
import { CYCLE_STATUS } from "constants/cycle"; import { CYCLE_STATUS } from "constants/cycle";
@ -67,7 +68,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query; const { workspaceSlug, projectId, peekCycle } = router.query;
// store hooks // store hooks
const { setTrackElement } = useEventTracker(); const { setTrackElement, captureCycleEvent } = useEventTracker();
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
@ -83,10 +84,32 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
defaultValues, defaultValues,
}); });
const submitChanges = (data: Partial<ICycle>) => { const submitChanges = (data: Partial<ICycle>, changedProperty: string) => {
if (!workspaceSlug || !projectId || !cycleId) return; 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 = () => { const handleCopyText = () => {
@ -146,10 +169,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}); });
if (isDateValidForExistingCycle) { if (isDateValidForExistingCycle) {
submitChanges({ submitChanges(
{
start_date: renderFormattedPayloadDate(`${watch("start_date")}`), start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
end_date: renderFormattedPayloadDate(`${watch("end_date")}`), end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
}); },
"start_date"
);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -174,10 +200,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}); });
if (isDateValid) { if (isDateValid) {
submitChanges({ submitChanges(
{
start_date: renderFormattedPayloadDate(`${watch("start_date")}`), start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
end_date: renderFormattedPayloadDate(`${watch("end_date")}`), end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
}); },
"start_date"
);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -219,10 +248,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}); });
if (isDateValidForExistingCycle) { if (isDateValidForExistingCycle) {
submitChanges({ submitChanges(
{
start_date: renderFormattedPayloadDate(`${watch("start_date")}`), start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
end_date: renderFormattedPayloadDate(`${watch("end_date")}`), end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
}); },
"end_date"
);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -246,10 +278,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}); });
if (isDateValid) { if (isDateValid) {
submitChanges({ submitChanges(
{
start_date: renderFormattedPayloadDate(`${watch("start_date")}`), start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
end_date: renderFormattedPayloadDate(`${watch("end_date")}`), end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
}); },
"end_date"
);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",

View File

@ -2,7 +2,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { FileText, Plus } from "lucide-react"; import { FileText, Plus } from "lucide-react";
// hooks // hooks
import { useApplication, useProject, useUser } from "hooks/store"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
// ui // ui
import { Breadcrumbs, Button } from "@plane/ui"; import { Breadcrumbs, Button } from "@plane/ui";
// helpers // helpers
@ -25,6 +25,7 @@ export const PagesHeader = observer(() => {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { setTrackElement } = useEventTracker();
const canUserCreatePage = const canUserCreatePage =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
@ -64,7 +65,15 @@ export const PagesHeader = observer(() => {
</div> </div>
{canUserCreatePage && ( {canUserCreatePage && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => toggleCreatePageModal(true)}> <Button
variant="primary"
prependIcon={<Plus />}
size="sm"
onClick={() => {
setTrackElement("Project pages page");
toggleCreatePageModal(true);
}}
>
Create Page Create Page
</Button> </Button>
</div> </div>

View File

@ -4,13 +4,18 @@ import { useTheme } from "next-themes";
// images // images
import githubBlackImage from "/public/logos/github-black.png"; import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png"; import githubWhiteImage from "/public/logos/github-white.png";
// hooks
import { useEventTracker } from "hooks/store";
// components // components
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { Breadcrumbs } from "@plane/ui"; import { Breadcrumbs } from "@plane/ui";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// constants
import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker";
export const WorkspaceDashboardHeader = () => { export const WorkspaceDashboardHeader = () => {
// hooks // hooks
const { captureEvent } = useEventTracker();
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
return ( return (
@ -31,16 +36,26 @@ export const WorkspaceDashboardHeader = () => {
</div> </div>
<div className="flex items-center gap-3 px-3"> <div className="flex items-center gap-3 px-3">
<a <a
onClick={() =>
captureEvent(CHANGELOG_REDIRECTED, {
element: "navbar",
})
}
href="https://plane.so/changelog" href="https://plane.so/changelog"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5" className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5"
> >
<Zap size={14} strokeWidth={2} fill="rgb(var(--color-text-100))" /> <Zap size={14} strokeWidth={2} fill="rgb(var(--color-text-100))" />
<span className="text-xs hidden sm:hidden md:block font-medium">{"What's new?"}</span> <span className="hidden text-xs font-medium sm:hidden md:block">{"What's new?"}</span>
</a> </a>
<a <a
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5 " onClick={() =>
captureEvent(GITHUB_REDIRECTED, {
element: "navbar",
})
}
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5"
href="https://github.com/makeplane/plane" href="https://github.com/makeplane/plane"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@ -51,7 +66,7 @@ export const WorkspaceDashboardHeader = () => {
width={16} width={16}
alt="GitHub Logo" alt="GitHub Logo"
/> />
<span className="text-xs font-medium hidden sm:hidden md:block">Star us on GitHub</span> <span className="hidden text-xs font-medium sm:hidden md:block">Star us on GitHub</span>
</a> </a>
</div> </div>
</div> </div>

View File

@ -20,6 +20,7 @@ import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle
// types // types
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types"; import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { ISSUE_DELETED } from "constants/event-tracker";
type TInboxIssueActionsHeader = { type TInboxIssueActionsHeader = {
workspaceSlug: string; workspaceSlug: string;
@ -86,17 +87,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
throw new Error("Missing required parameters"); throw new Error("Missing required parameters");
await removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId); await removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
captureIssueEvent({ captureIssueEvent({
eventName: "Issue deleted", eventName: ISSUE_DELETED,
payload: { payload: {
id: inboxIssueId, id: inboxIssueId,
state: "SUCCESS", state: "SUCCESS",
element: "Inbox page", element: "Inbox page",
}, }
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
}); });
router.push({ router.push({
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
@ -108,17 +104,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
message: "Something went wrong while deleting inbox issue. Please try again.", message: "Something went wrong while deleting inbox issue. Please try again.",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue deleted", eventName: ISSUE_DELETED,
payload: { payload: {
id: inboxIssueId, id: inboxIssueId,
state: "FAILED", state: "FAILED",
element: "Inbox page", element: "Inbox page",
}, },
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
}); });
} }
}, },

View File

@ -18,6 +18,8 @@ import { GptAssistantPopover } from "components/core";
import { Button, Input, ToggleSwitch } from "@plane/ui"; import { Button, Input, ToggleSwitch } from "@plane/ui";
// types // types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// constants
import { ISSUE_CREATED } from "constants/event-tracker";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -65,7 +67,6 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
config: { envConfig }, config: { envConfig },
} = useApplication(); } = useApplication();
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
const { currentWorkspace } = useWorkspace();
const { const {
control, control,
@ -94,34 +95,24 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
handleClose(); handleClose();
} else reset(defaultValues); } else reset(defaultValues);
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { payload: {
...formData, ...formData,
state: "SUCCESS", state: "SUCCESS",
element: "Inbox page", element: "Inbox page",
}, },
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
path: router.pathname, path: router.pathname,
}); });
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { payload: {
...formData, ...formData,
state: "FAILED", state: "FAILED",
element: "Inbox page", element: "Inbox page",
}, },
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
path: router.pathname, path: router.pathname,
}); });
}); });

View File

@ -38,7 +38,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
title: "Attachment uploaded", title: "Attachment uploaded",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: "Issue attachment added",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: { updates: {
changed_property: "attachment", changed_property: "attachment",
@ -47,7 +47,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: "Issue attachment added",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
}); });
setToastAlert({ setToastAlert({
@ -67,7 +67,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
title: "Attachment removed", title: "Attachment removed",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: "Issue attachment deleted",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: { updates: {
changed_property: "attachment", changed_property: "attachment",
@ -76,7 +76,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: "Issue attachment deleted",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: { updates: {
changed_property: "attachment", changed_property: "attachment",

View File

@ -16,6 +16,7 @@ import { TIssue } from "@plane/types";
// constants // constants
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue"; import { EIssuesStoreType } from "constants/issue";
import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
export type TIssueOperations = { export type TIssueOperations = {
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
@ -102,7 +103,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
}); });
} }
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
updates: { updates: {
changed_property: Object.keys(data).join(","), changed_property: Object.keys(data).join(","),
@ -112,7 +113,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue detail page" }, payload: { state: "FAILED", element: "Issue detail page" },
updates: { updates: {
changed_property: Object.keys(data).join(","), changed_property: Object.keys(data).join(","),
@ -138,7 +139,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
message: "Issue deleted successfully", message: "Issue deleted successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue deleted", eventName: ISSUE_DELETED,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
path: router.asPath, path: router.asPath,
}); });
@ -149,7 +150,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
message: "Issue delete failed", message: "Issue delete failed",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue deleted", eventName: ISSUE_DELETED,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
path: router.asPath, path: router.asPath,
}); });
@ -164,7 +165,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
message: "Issue added to issue successfully", message: "Issue added to issue successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
updates: { updates: {
changed_property: "cycle_id", changed_property: "cycle_id",
@ -174,7 +175,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue detail page" }, payload: { state: "FAILED", element: "Issue detail page" },
updates: { updates: {
changed_property: "cycle_id", changed_property: "cycle_id",
@ -198,7 +199,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
message: "Cycle removed from issue successfully", message: "Cycle removed from issue successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
updates: { updates: {
changed_property: "cycle_id", changed_property: "cycle_id",
@ -208,7 +209,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue detail page" }, payload: { state: "FAILED", element: "Issue detail page" },
updates: { updates: {
changed_property: "cycle_id", changed_property: "cycle_id",
@ -232,7 +233,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
message: "Module added to issue successfully", message: "Module added to issue successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
updates: { updates: {
changed_property: "module_id", changed_property: "module_id",
@ -242,7 +243,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: { updates: {
changed_property: "module_id", changed_property: "module_id",
@ -266,7 +267,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
message: "Module removed from issue successfully", message: "Module removed from issue successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: { updates: {
changed_property: "module_id", changed_property: "module_id",
@ -276,7 +277,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: { updates: {
changed_property: "module_id", changed_property: "module_id",

View File

@ -13,6 +13,8 @@ import { createIssuePayload } from "helpers/issue.helper";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
// types // types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// constants
import { ISSUE_CREATED } from "constants/event-tracker";
type Props = { type Props = {
formKey: keyof TIssue; formKey: keyof TIssue;
@ -129,7 +131,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
viewId viewId
).then((res) => { ).then((res) => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...res, state: "SUCCESS", element: "Calendar quick add" }, payload: { ...res, state: "SUCCESS", element: "Calendar quick add" },
path: router.asPath, path: router.asPath,
}); });
@ -142,7 +144,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
} catch (err: any) { } catch (err: any) {
console.error(err); console.error(err);
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED", element: "Calendar quick add" }, payload: { ...payload, state: "FAILED", element: "Calendar quick add" },
path: router.asPath, path: router.asPath,
}); });

View File

@ -2,7 +2,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
// hooks // hooks
import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store"; import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
//ui //ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// components // components
@ -11,6 +11,8 @@ import { AppliedFiltersList } from "components/issues";
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types"; import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
// constants
import { GLOBAL_VIEW_UPDATED } from "constants/event-tracker";
type Props = { type Props = {
globalViewId: string; globalViewId: string;
@ -27,6 +29,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
} = useIssues(EIssuesStoreType.GLOBAL); } = useIssues(EIssuesStoreType.GLOBAL);
const { workspaceLabels } = useLabel(); const { workspaceLabels } = useLabel();
const { globalViewMap, updateGlobalView } = useGlobalView(); const { globalViewMap, updateGlobalView } = useGlobalView();
const { captureEvent } = useEventTracker();
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
@ -91,6 +94,13 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
filters: { filters: {
...(appliedFilters ?? {}), ...(appliedFilters ?? {}),
}, },
}).then((res) => {
captureEvent(GLOBAL_VIEW_UPDATED, {
view_id: res.id,
applied_filters: res.filters,
state: "SUCCESS",
element: "Spreadsheet view",
});
}); });
}; };

View File

@ -13,6 +13,8 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { createIssuePayload } from "helpers/issue.helper"; import { createIssuePayload } from "helpers/issue.helper";
// types // types
import { IProject, TIssue } from "@plane/types"; import { IProject, TIssue } from "@plane/types";
// constants
import { ISSUE_CREATED } from "constants/event-tracker";
interface IInputProps { interface IInputProps {
formKey: string; formKey: string;
@ -111,7 +113,7 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
quickAddCallback && quickAddCallback &&
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => { (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...res, state: "SUCCESS", element: "Gantt quick add" }, payload: { ...res, state: "SUCCESS", element: "Gantt quick add" },
path: router.asPath, path: router.asPath,
}); });
@ -123,7 +125,7 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
}); });
} catch (err: any) { } catch (err: any) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED", element: "Gantt quick add" }, payload: { ...payload, state: "FAILED", element: "Gantt quick add" },
path: router.asPath, path: router.asPath,
}); });

View File

@ -25,6 +25,7 @@ import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile";
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue"; import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue";
import { ISSUE_DELETED } from "constants/event-tracker";
export interface IBaseKanBanLayout { export interface IBaseKanBanLayout {
issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues; issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues;
@ -212,7 +213,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
setDeleteIssueModal(false); setDeleteIssueModal(false);
setDragState({}); setDragState({});
captureIssueEvent({ captureIssueEvent({
eventName: "Issue deleted", eventName: ISSUE_DELETED,
payload: { id: dragState.draggedIssueId!, state: "FAILED", element: "Kanban layout drag & drop" }, payload: { id: dragState.draggedIssueId!, state: "FAILED", element: "Kanban layout drag & drop" },
path: router.asPath, path: router.asPath,
}); });

View File

@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { createIssuePayload } from "helpers/issue.helper"; import { createIssuePayload } from "helpers/issue.helper";
// types // types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// constants
import { ISSUE_CREATED } from "constants/event-tracker";
const Inputs = (props: any) => { const Inputs = (props: any) => {
const { register, setFocus, projectDetail } = props; const { register, setFocus, projectDetail } = props;
@ -106,7 +108,7 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
viewId viewId
).then((res) => { ).then((res) => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...res, state: "SUCCESS", element: "Kanban quick add" }, payload: { ...res, state: "SUCCESS", element: "Kanban quick add" },
path: router.asPath, path: router.asPath,
}); });
@ -118,7 +120,7 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
}); });
} catch (err: any) { } catch (err: any) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED", element: "Kanban quick add" }, payload: { ...payload, state: "FAILED", element: "Kanban quick add" },
path: router.asPath, path: router.asPath,
}); });

View File

@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { TIssue, IProject } from "@plane/types"; import { TIssue, IProject } from "@plane/types";
// types // types
import { createIssuePayload } from "helpers/issue.helper"; import { createIssuePayload } from "helpers/issue.helper";
// constants
import { ISSUE_CREATED } from "constants/event-tracker";
interface IInputProps { interface IInputProps {
formKey: string; formKey: string;
@ -103,7 +105,7 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
quickAddCallback && quickAddCallback &&
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => { (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...res, state: "SUCCESS", element: "List quick add" }, payload: { ...res, state: "SUCCESS", element: "List quick add" },
path: router.asPath, path: router.asPath,
}); });
@ -115,7 +117,7 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
}); });
} catch (err: any) { } catch (err: any) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED", element: "List quick add" }, payload: { ...payload, state: "FAILED", element: "List quick add" },
path: router.asPath, path: router.asPath,
}); });

View File

@ -18,6 +18,8 @@ import {
import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types // types
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
// constants
import { ISSUE_UPDATED } from "constants/event-tracker";
export interface IIssueProperties { export interface IIssueProperties {
issue: TIssue; issue: TIssue;
@ -40,7 +42,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handleState = (stateId: string) => { const handleState = (stateId: string) => {
handleIssues({ ...issue, state_id: stateId }).then(() => { handleIssues({ ...issue, state_id: stateId }).then(() => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout }, payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath, path: router.asPath,
updates: { updates: {
@ -54,7 +56,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handlePriority = (value: TIssuePriorities) => { const handlePriority = (value: TIssuePriorities) => {
handleIssues({ ...issue, priority: value }).then(() => { handleIssues({ ...issue, priority: value }).then(() => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout }, payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath, path: router.asPath,
updates: { updates: {
@ -68,7 +70,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handleLabel = (ids: string[]) => { const handleLabel = (ids: string[]) => {
handleIssues({ ...issue, label_ids: ids }).then(() => { handleIssues({ ...issue, label_ids: ids }).then(() => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout }, payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath, path: router.asPath,
updates: { updates: {
@ -82,7 +84,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handleAssignee = (ids: string[]) => { const handleAssignee = (ids: string[]) => {
handleIssues({ ...issue, assignee_ids: ids }).then(() => { handleIssues({ ...issue, assignee_ids: ids }).then(() => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout }, payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath, path: router.asPath,
updates: { updates: {
@ -96,7 +98,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handleStartDate = (date: Date | null) => { const handleStartDate = (date: Date | null) => {
handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => { handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout }, payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath, path: router.asPath,
updates: { updates: {
@ -110,7 +112,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handleTargetDate = (date: Date | null) => { const handleTargetDate = (date: Date | null) => {
handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => { handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout }, payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath, path: router.asPath,
updates: { updates: {
@ -124,7 +126,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handleEstimate = (value: number | null) => { const handleEstimate = (value: number | null) => {
handleIssues({ ...issue, estimate_point: value }).then(() => { handleIssues({ ...issue, estimate_point: value }).then(() => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...issue, state: "SUCCESS", element: currentLayout }, payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: router.asPath, path: router.asPath,
updates: { updates: {

View File

@ -21,6 +21,7 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props)
<div className="h-11 border-b-[0.5px] border-custom-border-200"> <div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown <DateDropdown
value={issue.target_date} value={issue.target_date}
minDate={issue.start_date ? new Date(issue.start_date) : undefined}
onChange={(data) => { onChange={(data) => {
const targetDate = data ? renderFormattedPayloadDate(data) : null; const targetDate = data ? renderFormattedPayloadDate(data) : null;
onChange( onChange(

View File

@ -21,6 +21,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Prop
<div className="h-11 border-b-[0.5px] border-custom-border-200"> <div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown <DateDropdown
value={issue.start_date} value={issue.start_date}
maxDate={issue.target_date ? new Date(issue.target_date) : undefined}
onChange={(data) => { onChange={(data) => {
const startDate = data ? renderFormattedPayloadDate(data) : null; const startDate = data ? renderFormattedPayloadDate(data) : null;
onChange( onChange(

View File

@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { createIssuePayload } from "helpers/issue.helper"; import { createIssuePayload } from "helpers/issue.helper";
// types // types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// constants
import { ISSUE_CREATED } from "constants/event-tracker";
type Props = { type Props = {
formKey: keyof TIssue; formKey: keyof TIssue;
@ -162,7 +164,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) =>
(await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then( (await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then(
(res) => { (res) => {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" }, payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" },
path: router.asPath, path: router.asPath,
}); });
@ -175,7 +177,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) =>
}); });
} catch (err: any) { } catch (err: any) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" }, payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" },
path: router.asPath, path: router.asPath,
}); });

View File

@ -13,6 +13,8 @@ import { IssueFormRoot } from "./form";
import type { TIssue } from "@plane/types"; import type { TIssue } from "@plane/types";
// constants // constants
import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue"; import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue";
import { ISSUE_CREATED, ISSUE_UPDATED } from "constants/event-tracker";
export interface IssuesModalProps { export interface IssuesModalProps {
data?: Partial<TIssue>; data?: Partial<TIssue>;
isOpen: boolean; isOpen: boolean;
@ -157,14 +159,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
message: "Issue created successfully.", message: "Issue created successfully.",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...response, state: "SUCCESS" }, payload: { ...response, state: "SUCCESS" },
path: router.asPath, path: router.asPath,
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
}); });
!createMore && handleClose(); !createMore && handleClose();
return response; return response;
@ -175,14 +172,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
message: "Issue could not be created. Please try again.", message: "Issue could not be created. Please try again.",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue created", eventName: ISSUE_CREATED,
payload: { ...payload, state: "FAILED" }, payload: { ...payload, state: "FAILED" },
path: router.asPath, path: router.asPath,
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
}); });
} }
}; };
@ -198,14 +190,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
message: "Issue updated successfully.", message: "Issue updated successfully.",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS" }, payload: { ...response, state: "SUCCESS" },
path: router.asPath, path: router.asPath,
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
}); });
handleClose(); handleClose();
return response; return response;
@ -216,14 +203,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
message: "Issue could not be created. Please try again.", message: "Issue could not be created. Please try again.",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...payload, state: "FAILED" }, payload: { ...payload, state: "FAILED" },
path: router.asPath, path: router.asPath,
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
}); });
} }
}; };

View File

@ -11,6 +11,7 @@ import { TIssue } from "@plane/types";
// constants // constants
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue"; import { EIssuesStoreType } from "constants/issue";
import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
interface IIssuePeekOverview { interface IIssuePeekOverview {
is_archived?: boolean; is_archived?: boolean;
@ -103,7 +104,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
message: "Issue updated successfully", message: "Issue updated successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
updates: { updates: {
changed_property: Object.keys(data).join(","), changed_property: Object.keys(data).join(","),
@ -113,7 +114,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue peek-overview" }, payload: { state: "FAILED", element: "Issue peek-overview" },
path: router.asPath, path: router.asPath,
}); });
@ -135,7 +136,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
message: "Issue deleted successfully", message: "Issue deleted successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue deleted", eventName: ISSUE_DELETED,
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
path: router.asPath, path: router.asPath,
}); });
@ -146,7 +147,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
message: "Issue delete failed", message: "Issue delete failed",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue deleted", eventName: ISSUE_DELETED,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
path: router.asPath, path: router.asPath,
}); });
@ -161,7 +162,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
message: "Issue added to issue successfully", message: "Issue added to issue successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
updates: { updates: {
changed_property: "cycle_id", changed_property: "cycle_id",
@ -171,7 +172,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue peek-overview" }, payload: { state: "FAILED", element: "Issue peek-overview" },
updates: { updates: {
changed_property: "cycle_id", changed_property: "cycle_id",
@ -195,7 +196,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
message: "Cycle removed from issue successfully", message: "Cycle removed from issue successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
updates: { updates: {
changed_property: "cycle_id", changed_property: "cycle_id",
@ -210,7 +211,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
message: "Cycle remove from issue failed", message: "Cycle remove from issue failed",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue peek-overview" }, payload: { state: "FAILED", element: "Issue peek-overview" },
updates: { updates: {
changed_property: "cycle_id", changed_property: "cycle_id",
@ -229,7 +230,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
message: "Module added to issue successfully", message: "Module added to issue successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
updates: { updates: {
changed_property: "module_id", changed_property: "module_id",
@ -239,7 +240,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
updates: { updates: {
changed_property: "module_id", changed_property: "module_id",
@ -263,7 +264,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
message: "Module removed from issue successfully", message: "Module removed from issue successfully",
}); });
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
updates: { updates: {
changed_property: "module_id", changed_property: "module_id",
@ -273,7 +274,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureIssueEvent({
eventName: "Issue updated", eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
updates: { updates: {
changed_property: "module_id", changed_property: "module_id",

View File

@ -11,6 +11,8 @@ import { Button } from "@plane/ui";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
// types // types
import type { IModule } from "@plane/types"; import type { IModule } from "@plane/types";
// constants
import { MODULE_DELETED } from "constants/event-tracker";
type Props = { type Props = {
data: IModule; data: IModule;
@ -51,7 +53,7 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
message: "Module deleted successfully.", message: "Module deleted successfully.",
}); });
captureModuleEvent({ captureModuleEvent({
eventName: "Module deleted", eventName: MODULE_DELETED,
payload: { ...data, state: "SUCCESS" }, payload: { ...data, state: "SUCCESS" },
}); });
}) })
@ -62,7 +64,7 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
message: "Module could not be deleted. Please try again.", message: "Module could not be deleted. Please try again.",
}); });
captureModuleEvent({ captureModuleEvent({
eventName: "Module deleted", eventName: MODULE_DELETED,
payload: { ...data, state: "FAILED" }, payload: { ...data, state: "FAILED" },
}); });
}) })

View File

@ -11,7 +11,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { IModule } from "@plane/types"; import { IModule } from "@plane/types";
type Props = { type Props = {
handleFormSubmit: (values: Partial<IModule>) => Promise<void>; handleFormSubmit: (values: Partial<IModule>, dirtyFields: any) => Promise<void>;
handleClose: () => void; handleClose: () => void;
status: boolean; status: boolean;
projectId: string; projectId: string;
@ -36,7 +36,7 @@ export const ModuleForm: React.FC<Props> = ({
data, data,
}) => { }) => {
const { const {
formState: { errors, isSubmitting }, formState: { errors, isSubmitting, dirtyFields },
handleSubmit, handleSubmit,
watch, watch,
control, control,
@ -53,7 +53,7 @@ export const ModuleForm: React.FC<Props> = ({
}); });
const handleCreateUpdateModule = async (formData: Partial<IModule>) => { const handleCreateUpdateModule = async (formData: Partial<IModule>) => {
await handleFormSubmit(formData); await handleFormSubmit(formData, dirtyFields);
reset({ reset({
...defaultValues, ...defaultValues,

View File

@ -9,6 +9,8 @@ import useToast from "hooks/use-toast";
import { ModuleForm } from "components/modules"; import { ModuleForm } from "components/modules";
// types // types
import type { IModule } from "@plane/types"; import type { IModule } from "@plane/types";
// constants
import { MODULE_CREATED, MODULE_UPDATED } from "constants/event-tracker";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -59,7 +61,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
message: "Module created successfully.", message: "Module created successfully.",
}); });
captureModuleEvent({ captureModuleEvent({
eventName: "Module created", eventName: MODULE_CREATED,
payload: { ...res, state: "SUCCESS" }, payload: { ...res, state: "SUCCESS" },
}); });
}) })
@ -70,13 +72,13 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
message: err.detail ?? "Module could not be created. Please try again.", message: err.detail ?? "Module could not be created. Please try again.",
}); });
captureModuleEvent({ captureModuleEvent({
eventName: "Module created", eventName: MODULE_CREATED,
payload: { ...data, state: "FAILED" }, payload: { ...data, state: "FAILED" },
}); });
}); });
}; };
const handleUpdateModule = async (payload: Partial<IModule>) => { const handleUpdateModule = async (payload: Partial<IModule>, dirtyFields: any) => {
if (!workspaceSlug || !projectId || !data) return; if (!workspaceSlug || !projectId || !data) return;
const selectedProjectId = payload.project ?? projectId.toString(); const selectedProjectId = payload.project ?? projectId.toString();
@ -90,8 +92,8 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
message: "Module updated successfully.", message: "Module updated successfully.",
}); });
captureModuleEvent({ captureModuleEvent({
eventName: "Module updated", eventName: MODULE_UPDATED,
payload: { ...res, state: "SUCCESS" }, payload: { ...res, changed_properties: Object.keys(dirtyFields), state: "SUCCESS" },
}); });
}) })
.catch((err) => { .catch((err) => {
@ -101,20 +103,20 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
message: err.detail ?? "Module could not be updated. Please try again.", message: err.detail ?? "Module could not be updated. Please try again.",
}); });
captureModuleEvent({ captureModuleEvent({
eventName: "Module updated", eventName: MODULE_UPDATED,
payload: { ...data, state: "FAILED" }, payload: { ...data, state: "FAILED" },
}); });
}); });
}; };
const handleFormSubmit = async (formData: Partial<IModule>) => { const handleFormSubmit = async (formData: Partial<IModule>, dirtyFields: any) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const payload: Partial<IModule> = { const payload: Partial<IModule> = {
...formData, ...formData,
}; };
if (!data) await handleCreateModule(payload); if (!data) await handleCreateModule(payload);
else await handleUpdateModule(payload); else await handleUpdateModule(payload, dirtyFields);
}; };
useEffect(() => { useEffect(() => {

View File

@ -16,6 +16,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
// constants // constants
import { MODULE_STATUS } from "constants/module"; import { MODULE_STATUS } from "constants/module";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker";
type Props = { type Props = {
moduleId: string; moduleId: string;
@ -36,7 +37,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule(); const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
const { setTrackElement } = useEventTracker(); const { setTrackElement, captureEvent } = useEventTracker();
// derived values // derived values
const moduleDetails = getModuleById(moduleId); const moduleDetails = getModuleById(moduleId);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
@ -46,7 +47,15 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => { addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
.then(() => {
captureEvent(MODULE_FAVORITED, {
module_id: moduleId,
element: "Grid layout",
state: "SUCCESS",
});
})
.catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -60,7 +69,15 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => { removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
.then(() => {
captureEvent(MODULE_UNFAVORITED, {
module_id: moduleId,
element: "Grid layout",
state: "SUCCESS",
});
})
.catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -84,14 +101,14 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
const handleEditModule = (e: React.MouseEvent<HTMLButtonElement>) => { const handleEditModule = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setTrackElement("Modules page board layout"); setTrackElement("Modules page grid layout");
setEditModal(true); setEditModal(true);
}; };
const handleDeleteModule = (e: React.MouseEvent<HTMLButtonElement>) => { const handleDeleteModule = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setTrackElement("Modules page board layout"); setTrackElement("Modules page grid layout");
setDeleteModal(true); setDeleteModal(true);
}; };

View File

@ -16,6 +16,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
// constants // constants
import { MODULE_STATUS } from "constants/module"; import { MODULE_STATUS } from "constants/module";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker";
type Props = { type Props = {
moduleId: string; moduleId: string;
@ -36,7 +37,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule(); const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
const { setTrackElement } = useEventTracker(); const { setTrackElement, captureEvent } = useEventTracker();
// derived values // derived values
const moduleDetails = getModuleById(moduleId); const moduleDetails = getModuleById(moduleId);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
@ -46,7 +47,15 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => { addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
.then(() => {
captureEvent(MODULE_FAVORITED, {
module_id: moduleId,
element: "Grid layout",
state: "SUCCESS",
});
})
.catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -60,7 +69,15 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => { removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
.then(() => {
captureEvent(MODULE_UNFAVORITED, {
module_id: moduleId,
element: "Grid layout",
state: "SUCCESS",
});
})
.catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",

View File

@ -34,6 +34,7 @@ import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
// constant // constant
import { MODULE_STATUS } from "constants/module"; import { MODULE_STATUS } from "constants/module";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker";
const defaultValues: Partial<IModule> = { const defaultValues: Partial<IModule> = {
lead: "", lead: "",
@ -66,7 +67,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule(); const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule();
const { setTrackElement } = useEventTracker(); const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker();
const moduleDetails = getModuleById(moduleId); const moduleDetails = getModuleById(moduleId);
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -77,7 +78,19 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const submitChanges = (data: Partial<IModule>) => { const submitChanges = (data: Partial<IModule>) => {
if (!workspaceSlug || !projectId || !moduleId) return; 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) => { const handleCreateLink = async (formData: ModuleLink) => {
@ -87,6 +100,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload) createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload)
.then(() => { .then(() => {
captureEvent(MODULE_LINK_CREATED, {
module_id: moduleId,
state: "SUCCESS",
});
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Module link created", title: "Module link created",
@ -109,6 +126,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload) updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload)
.then(() => { .then(() => {
captureEvent(MODULE_LINK_UPDATED, {
module_id: moduleId,
state: "SUCCESS",
});
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Module link updated", title: "Module link updated",
@ -129,6 +150,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId) deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId)
.then(() => { .then(() => {
captureEvent(MODULE_LINK_DELETED, {
module_id: moduleId,
state: "SUCCESS",
});
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Module link deleted", title: "Module link deleted",
@ -187,8 +212,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") {
submitChanges({ submitChanges({
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
target_date: renderFormattedPayloadDate(`${watch("target_date")}`), target_date: renderFormattedPayloadDate(`${watch("target_date")}`),
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
}); });
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -294,7 +319,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<Controller <Controller
control={control} control={control}
name="status" name="status"
render={({ field: { value } }) => ( render={({ field: { value, onChange } }) => (
<CustomSelect <CustomSelect
customButton={ customButton={
<span <span

View File

@ -1,10 +1,12 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Menu } from "@headlessui/react";
import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useEventTracker } from "hooks/store";
// icons // icons
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui"; import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
// constants // constants
@ -13,10 +15,12 @@ import { snoozeOptions } from "constants/notification";
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
// type // type
import type { IUserNotification } from "@plane/types"; import type { IUserNotification, NotificationType } from "@plane/types";
import { Menu } from "@headlessui/react"; // constants
import { ISSUE_OPENED, NOTIFICATIONS_READ, NOTIFICATION_ARCHIVED, NOTIFICATION_SNOOZED } from "constants/event-tracker";
type NotificationCardProps = { type NotificationCardProps = {
selectedTab: NotificationType;
notification: IUserNotification; notification: IUserNotification;
isSnoozedTabOpen: boolean; isSnoozedTabOpen: boolean;
closePopover: () => void; closePopover: () => void;
@ -29,6 +33,7 @@ type NotificationCardProps = {
export const NotificationCard: React.FC<NotificationCardProps> = (props) => { export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
const { const {
selectedTab,
notification, notification,
isSnoozedTabOpen, isSnoozedTabOpen,
closePopover, closePopover,
@ -38,6 +43,8 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
setSelectedNotificationForSnooze, setSelectedNotificationForSnooze,
markSnoozeNotification, markSnoozeNotification,
} = props; } = props;
// store hooks
const { captureEvent } = useEventTracker();
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -115,6 +122,10 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
<Link <Link
onClick={() => { onClick={() => {
markNotificationReadStatus(notification.id); markNotificationReadStatus(notification.id);
captureEvent(ISSUE_OPENED, {
issue_id: notification.data.issue.id,
element: "notification",
});
closePopover(); closePopover();
}} }}
href={`/${workspaceSlug}/projects/${notification.project}/${ href={`/${workspaceSlug}/projects/${notification.project}/${
@ -301,15 +312,55 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
)} )}
</div> </div>
</div> </div>
<div className="absolute right-3 top-3 hidden gap-x-3 py-1 md:group-hover:flex"> <div className="absolute right-3 top-3 hidden gap-x-3 py-1 group-hover:flex">
{moreOptions.map((item) => ( {[
{
id: 1,
name: notification.read_at ? "Mark as unread" : "Mark as read",
icon: <MessageSquare className="h-3.5 w-3.5 text-custom-text-300" />,
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",
});
});
},
},
{
id: 2,
name: notification.archived_at ? "Unarchive" : "Archive",
icon: notification.archived_at ? (
<ArchiveRestore className="h-3.5 w-3.5 text-custom-text-300" />
) : (
<ArchiveIcon className="h-3.5 w-3.5 text-custom-text-300" />
),
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",
});
});
},
},
].map((item) => (
<Tooltip tooltipContent={item.name}> <Tooltip tooltipContent={item.name}>
<button <button
type="button" type="button"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
item.onClick(); item.onClick();
}} }}
key={item.id} key={item.id}
@ -335,7 +386,23 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
snoozeOptionOnClick(item.value);
if (!item.value) {
setSelectedNotificationForSnooze(notification.id);
return;
}
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",
});
});
}} }}
> >
{item.label} {item.label}

View File

@ -4,10 +4,19 @@ import { ArrowLeft, CheckCheck, Clock, ListFilter, MoreVertical, RefreshCw, X }
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// ui // ui
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui"; import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
// hooks
import { useEventTracker } from "hooks/store";
// helpers // helpers
import { getNumberCount } from "helpers/string.helper"; import { getNumberCount } from "helpers/string.helper";
// type // type
import type { NotificationType, NotificationCount } from "@plane/types"; import type { NotificationType, NotificationCount } from "@plane/types";
// constants
import {
ARCHIVED_NOTIFICATIONS,
NOTIFICATIONS_READ,
SNOOZED_NOTIFICATIONS,
UNREAD_NOTIFICATIONS,
} from "constants/event-tracker";
type NotificationHeaderProps = { type NotificationHeaderProps = {
notificationCount?: NotificationCount | null; notificationCount?: NotificationCount | null;
@ -41,6 +50,8 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
setSelectedTab, setSelectedTab,
markAllNotificationsAsRead, markAllNotificationsAsRead,
} = props; } = props;
// store hooks
const { captureEvent } = useEventTracker();
const notificationTabs: Array<{ const notificationTabs: Array<{
label: string; label: string;
@ -90,6 +101,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
setSnoozed(false); setSnoozed(false);
setArchived(false); setArchived(false);
setReadNotification((prev) => !prev); setReadNotification((prev) => !prev);
captureEvent(UNREAD_NOTIFICATIONS);
}} }}
> >
<ListFilter className="h-3.5 w-3.5" /> <ListFilter className="h-3.5 w-3.5" />
@ -103,7 +115,12 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
} }
closeOnSelect closeOnSelect
> >
<CustomMenu.MenuItem onClick={markAllNotificationsAsRead}> <CustomMenu.MenuItem
onClick={() => {
markAllNotificationsAsRead();
captureEvent(NOTIFICATIONS_READ);
}}
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CheckCheck className="h-3.5 w-3.5" /> <CheckCheck className="h-3.5 w-3.5" />
Mark all as read Mark all as read
@ -114,6 +131,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
setArchived(false); setArchived(false);
setReadNotification(false); setReadNotification(false);
setSnoozed((prev) => !prev); setSnoozed((prev) => !prev);
captureEvent(SNOOZED_NOTIFICATIONS);
}} }}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -126,6 +144,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
setSnoozed(false); setSnoozed(false);
setReadNotification(false); setReadNotification(false);
setArchived((prev) => !prev); setArchived((prev) => !prev);
captureEvent(ARCHIVED_NOTIFICATIONS);
}} }}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@ -128,6 +128,7 @@ export const NotificationPopover = observer(() => {
<div className="divide-y divide-custom-border-100"> <div className="divide-y divide-custom-border-100">
{notifications.map((notification) => ( {notifications.map((notification) => (
<NotificationCard <NotificationCard
selectedTab={selectedTab}
key={notification.id} key={notification.id}
isSnoozedTabOpen={snoozed} isSnoozedTabOpen={snoozed}
closePopover={() => setIsActive(false)} closePopover={() => setIsActive(false)}

View File

@ -11,11 +11,13 @@ import { WorkspaceService } from "services/workspace.service";
// constants // constants
import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
import { MEMBER_ACCEPTED } from "constants/event-tracker";
// types // types
import { IWorkspaceMemberInvitation } from "@plane/types"; import { IWorkspaceMemberInvitation } from "@plane/types";
// icons // icons
import { CheckCircle2, Search } from "lucide-react"; import { CheckCircle2, Search } from "lucide-react";
import {} from "hooks/store/use-event-tracker"; import {} from "hooks/store/use-event-tracker";
import { getUserRole } from "helpers/user.helper";
type Props = { type Props = {
handleNextStep: () => void; handleNextStep: () => void;
@ -58,11 +60,19 @@ export const Invitations: React.FC<Props> = (props) => {
if (invitationsRespond.length <= 0) return; if (invitationsRespond.length <= 0) return;
setIsJoiningWorkspaces(true); setIsJoiningWorkspaces(true);
const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]);
await workspaceService await workspaceService
.joinWorkspaces({ invitations: invitationsRespond }) .joinWorkspaces({ invitations: invitationsRespond })
.then(async (res) => { .then(async () => {
captureEvent("Member accepted", { ...res, state: "SUCCESS", accepted_from: "App" }); 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 fetchWorkspaces();
await mutate(USER_WORKSPACES); await mutate(USER_WORKSPACES);
await updateLastWorkspace(); await updateLastWorkspace();
@ -71,7 +81,14 @@ export const Invitations: React.FC<Props> = (props) => {
}) })
.catch((error) => { .catch((error) => {
console.error(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)); .finally(() => setIsJoiningWorkspaces(false));
}; };

View File

@ -18,6 +18,7 @@ import { Check, ChevronDown, Plus, XCircle } from "lucide-react";
import { WorkspaceService } from "services/workspace.service"; import { WorkspaceService } from "services/workspace.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useEventTracker } from "hooks/store";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// components // components
@ -28,6 +29,9 @@ import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
// constants // constants
import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
import { MEMBER_INVITED } from "constants/event-tracker";
// helpers
import { getUserRole } from "helpers/user.helper";
// assets // assets
import user1 from "public/users/user-1.png"; import user1 from "public/users/user-1.png";
import user2 from "public/users/user-2.png"; import user2 from "public/users/user-2.png";
@ -267,6 +271,8 @@ export const InviteMembers: React.FC<Props> = (props) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
// store hooks
const { captureEvent } = useEventTracker();
const { const {
control, control,
@ -305,6 +311,17 @@ export const InviteMembers: React.FC<Props> = (props) => {
})), })),
}) })
.then(async () => { .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({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -313,13 +330,18 @@ export const InviteMembers: React.FC<Props> = (props) => {
await nextStep(); await nextStep();
}) })
.catch((err) => .catch((err) => {
captureEvent(MEMBER_INVITED, {
project_id: undefined,
state: "FAILED",
element: "Onboarding",
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: err?.error, message: err?.error,
}) });
); });
}; };
const appendField = () => { const appendField = () => {

View File

@ -15,6 +15,8 @@ import CyclesTour from "public/onboarding/cycles.webp";
import ModulesTour from "public/onboarding/modules.webp"; import ModulesTour from "public/onboarding/modules.webp";
import ViewsTour from "public/onboarding/views.webp"; import ViewsTour from "public/onboarding/views.webp";
import PagesTour from "public/onboarding/pages.webp"; import PagesTour from "public/onboarding/pages.webp";
// constants
import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker";
type Props = { type Props = {
onComplete: () => void; onComplete: () => void;
@ -79,7 +81,7 @@ export const TourRoot: React.FC<Props> = observer((props) => {
const [step, setStep] = useState<TTourSteps>("welcome"); const [step, setStep] = useState<TTourSteps>("welcome");
// store hooks // store hooks
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement, captureEvent } = useEventTracker();
const { currentUser } = useUser(); const { currentUser } = useUser();
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step); const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
@ -103,13 +105,22 @@ export const TourRoot: React.FC<Props> = observer((props) => {
</p> </p>
<div className="flex h-full items-end"> <div className="flex h-full items-end">
<div className="mt-8 flex items-center gap-6"> <div className="mt-8 flex items-center gap-6">
<Button variant="primary" onClick={() => setStep("issues")}> <Button
variant="primary"
onClick={() => {
captureEvent(PRODUCT_TOUR_STARTED);
setStep("issues");
}}
>
Take a Product Tour Take a Product Tour
</Button> </Button>
<button <button
type="button" type="button"
className="bg-transparent text-xs font-medium text-custom-primary-100 outline-custom-text-100" className="bg-transparent text-xs font-medium text-custom-primary-100 outline-custom-text-100"
onClick={onComplete} onClick={() => {
captureEvent(PRODUCT_TOUR_SKIPPED);
onComplete();
}}
> >
No thanks, I will explore it myself No thanks, I will explore it myself
</button> </button>
@ -156,8 +167,8 @@ export const TourRoot: React.FC<Props> = observer((props) => {
<Button <Button
variant="primary" variant="primary"
onClick={() => { onClick={() => {
setTrackElement("Product tour");
onComplete(); onComplete();
setTrackElement("Onboarding tour");
commandPaletteStore.toggleCreateProjectModal(true); commandPaletteStore.toggleCreateProjectModal(true);
}} }}
> >

View File

@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Camera, User2 } from "lucide-react"; import { Camera, User2 } from "lucide-react";
// hooks // hooks
import { useUser, useWorkspace } from "hooks/store"; import { useEventTracker, useUser, useWorkspace } from "hooks/store";
// components // components
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding"; import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding";
@ -15,6 +15,7 @@ import { IUser } from "@plane/types";
import { FileService } from "services/file.service"; import { FileService } from "services/file.service";
// assets // assets
import IssuesSvg from "public/onboarding/onboarding-issues.webp"; import IssuesSvg from "public/onboarding/onboarding-issues.webp";
import { USER_DETAILS } from "constants/event-tracker";
const defaultValues: Partial<IUser> = { const defaultValues: Partial<IUser> = {
first_name: "", first_name: "",
@ -48,6 +49,7 @@ export const UserDetails: React.FC<Props> = observer((props) => {
// store hooks // store hooks
const { updateCurrentUser } = useUser(); const { updateCurrentUser } = useUser();
const { workspaces } = useWorkspace(); const { workspaces } = useWorkspace();
const { captureEvent } = useEventTracker();
// derived values // derived values
const workspaceName = workspaces ? Object.values(workspaces)?.[0]?.name : "New Workspace"; const workspaceName = workspaces ? Object.values(workspaces)?.[0]?.name : "New Workspace";
// form info // form info
@ -76,7 +78,21 @@ export const UserDetails: React.FC<Props> = observer((props) => {
}, },
}; };
await updateCurrentUser(payload); await updateCurrentUser(payload)
.then(() => {
captureEvent(USER_DETAILS, {
use_case: formData.use_case,
state: "SUCCESS",
element: "Onboarding",
});
})
.catch(() => {
captureEvent(USER_DETAILS, {
use_case: formData.use_case,
state: "FAILED",
element: "Onboarding",
});
});
}; };
const handleDelete = (url: string | null | undefined) => { const handleDelete = (url: string | null | undefined) => {
if (!url) return; if (!url) return;

View File

@ -5,12 +5,13 @@ import { Button, Input } from "@plane/ui";
// types // types
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
// hooks // hooks
import { useUser, useWorkspace } from "hooks/store"; import { useEventTracker, useUser, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// services // services
import { WorkspaceService } from "services/workspace.service"; import { WorkspaceService } from "services/workspace.service";
// constants // constants
import { RESTRICTED_URLS } from "constants/workspace"; import { RESTRICTED_URLS } from "constants/workspace";
import { WORKSPACE_CREATED } from "constants/event-tracker";
type Props = { type Props = {
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>; stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
@ -33,6 +34,7 @@ export const Workspace: React.FC<Props> = (props) => {
// store hooks // store hooks
const { updateCurrentUser } = useUser(); const { updateCurrentUser } = useUser();
const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace(); const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace();
const { captureWorkspaceEvent } = useEventTracker();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -46,31 +48,48 @@ export const Workspace: React.FC<Props> = (props) => {
setSlugError(false); setSlugError(false);
await createWorkspace(formData) await createWorkspace(formData)
.then(async () => { .then(async (res) => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "Workspace created successfully.", message: "Workspace created successfully.",
}); });
captureWorkspaceEvent({
eventName: WORKSPACE_CREATED,
payload: {
...res,
state: "SUCCESS",
first_time: true,
element: "Onboarding",
},
});
await fetchWorkspaces(); await fetchWorkspaces();
await completeStep(); await completeStep();
}) })
.catch(() => {
captureWorkspaceEvent({
eventName: WORKSPACE_CREATED,
payload: {
state: "FAILED",
first_time: true,
element: "Onboarding",
},
});
setToastAlert({
type: "error",
title: "Error!",
message: "Workspace could not be created. Please try again.",
});
});
} else setSlugError(true);
})
.catch(() => .catch(() =>
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "Workspace could not be created. Please try again.", message: "Some error occurred while creating workspace. Please try again.",
}) })
); );
} else setSlugError(true);
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Some error occurred while creating workspace. Please try again.",
});
});
}; };
const completeStep = async () => { const completeStep = async () => {

View File

@ -13,6 +13,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// constants // constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker";
export const WorkspaceDashboardView = observer(() => { export const WorkspaceDashboardView = observer(() => {
// theme // theme
@ -37,9 +38,8 @@ export const WorkspaceDashboardView = observer(() => {
const handleTourCompleted = () => { const handleTourCompleted = () => {
updateTourCompleted() updateTourCompleted()
.then(() => { .then(() => {
captureEvent("User tour complete", { captureEvent(PRODUCT_TOUR_COMPLETED, {
user_id: currentUser?.id, user_id: currentUser?.id,
email: currentUser?.email,
state: "SUCCESS", state: "SUCCESS",
}); });
}) })
@ -84,7 +84,7 @@ export const WorkspaceDashboardView = observer(() => {
primaryButton={{ primaryButton={{
text: "Build your first project", text: "Build your first project",
onClick: () => { onClick: () => {
setTrackElement("Dashboard"); setTrackElement("Dashboard empty state");
toggleCreateProjectModal(true); toggleCreateProjectModal(true);
}, },
}} }}

View File

@ -3,10 +3,14 @@ import { useRouter } from "next/router";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// components // components
import { PageForm } from "./page-form"; import { PageForm } from "./page-form";
// hooks
import { useEventTracker } from "hooks/store";
// types // types
import { IPage } from "@plane/types"; import { IPage } from "@plane/types";
import { useProjectPages } from "hooks/store/use-project-page"; import { useProjectPages } from "hooks/store/use-project-page";
import { IPageStore } from "store/page.store"; import { IPageStore } from "store/page.store";
// constants
import { PAGE_CREATED, PAGE_UPDATED } from "constants/event-tracker";
type Props = { type Props = {
// data?: IPage | null; // data?: IPage | null;
@ -21,12 +25,30 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store hooks
const { createPage } = useProjectPages(); const { createPage } = useProjectPages();
const { capturePageEvent } = useEventTracker();
const createProjectPage = async (payload: IPage) => { const createProjectPage = async (payload: IPage) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
await createPage(workspaceSlug.toString(), projectId, payload); await createPage(workspaceSlug.toString(), projectId, payload)
.then((res) => {
capturePageEvent({
eventName: PAGE_CREATED,
payload: {
...res,
state: "SUCCESS",
},
});
})
.catch(() => {
capturePageEvent({
eventName: PAGE_CREATED,
payload: {
state: "FAILED",
},
});
});
}; };
const handleFormSubmit = async (formData: IPage) => { const handleFormSubmit = async (formData: IPage) => {
@ -39,6 +61,14 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
if (pageStore.access !== formData.access) { if (pageStore.access !== formData.access) {
formData.access === 1 ? await pageStore.makePrivate() : await pageStore.makePublic(); formData.access === 1 ? await pageStore.makePrivate() : await pageStore.makePublic();
} }
capturePageEvent({
eventName: PAGE_UPDATED,
payload: {
...pageStore,
state: "SUCCESS",
},
});
console.log("Page updated successfully", pageStore);
} else { } else {
await createProjectPage(formData); await createProjectPage(formData);
} }

View File

@ -4,12 +4,14 @@ import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
// hooks // hooks
import { usePage } from "hooks/store"; import { useEventTracker, usePage } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
import { useProjectPages } from "hooks/store/use-project-page"; import { useProjectPages } from "hooks/store/use-project-page";
// constants
import { PAGE_DELETED } from "constants/event-tracker";
type TConfirmPageDeletionProps = { type TConfirmPageDeletionProps = {
pageId: string; pageId: string;
@ -27,6 +29,7 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks // store hooks
const { deletePage } = useProjectPages(); const { deletePage } = useProjectPages();
const { capturePageEvent } = useEventTracker();
const pageStore = usePage(pageId); const pageStore = usePage(pageId);
// toast alert // toast alert
@ -49,6 +52,13 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
// Delete Page will only delete the page from the archive page map, at this point only archived pages can be deleted // Delete Page will only delete the page from the archive page map, at this point only archived pages can be deleted
await deletePage(workspaceSlug.toString(), projectId as string, pageId) await deletePage(workspaceSlug.toString(), projectId as string, pageId)
.then(() => { .then(() => {
capturePageEvent({
eventName: PAGE_DELETED,
payload: {
...pageStore,
state: "SUCCESS",
},
});
handleClose(); handleClose();
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -57,6 +67,13 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
}); });
}) })
.catch(() => { .catch(() => {
capturePageEvent({
eventName: PAGE_DELETED,
payload: {
...pageStore,
state: "FAILED",
},
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",

View File

@ -18,6 +18,7 @@ import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
// constants // constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { PROJECT_CREATED } from "constants/event-tracker";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -134,13 +135,8 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
state: "SUCCESS", state: "SUCCESS",
}; };
captureProjectEvent({ captureProjectEvent({
eventName: "Project created", eventName: PROJECT_CREATED,
payload: newPayload, payload: newPayload,
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: res.workspace,
},
}); });
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -160,16 +156,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
message: err.data[key], message: err.data[key],
}); });
captureProjectEvent({ captureProjectEvent({
eventName: "Project created", eventName: PROJECT_CREATED,
payload: { payload: {
...payload, ...payload,
state: "FAILED", state: "FAILED",
}, }
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
}); });
}); });
}); });

View File

@ -10,6 +10,8 @@ import useToast from "hooks/use-toast";
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// types // types
import type { IProject } from "@plane/types"; import type { IProject } from "@plane/types";
// constants
import { PROJECT_DELETED } from "constants/event-tracker";
type DeleteProjectModal = { type DeleteProjectModal = {
isOpen: boolean; isOpen: boolean;
@ -62,13 +64,8 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
handleClose(); handleClose();
captureProjectEvent({ captureProjectEvent({
eventName: "Project deleted", eventName: PROJECT_DELETED,
payload: { ...project, state: "SUCCESS", element: "Project general settings" }, payload: { ...project, state: "SUCCESS", element: "Project general settings" },
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
}); });
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -78,13 +75,8 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
}) })
.catch(() => { .catch(() => {
captureProjectEvent({ captureProjectEvent({
eventName: "Project deleted", eventName: PROJECT_DELETED,
payload: { ...project, state: "FAILED", element: "Project general settings" }, payload: { ...project, state: "FAILED", element: "Project general settings" },
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
}); });
setToastAlert({ setToastAlert({
type: "error", type: "error",

View File

@ -18,6 +18,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
import { NETWORK_CHOICES } from "constants/project"; import { NETWORK_CHOICES } from "constants/project";
// services // services
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
import { PROJECT_UPDATED } from "constants/event-tracker";
export interface IProjectDetailsForm { export interface IProjectDetailsForm {
project: IProject; project: IProject;
@ -45,7 +46,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
setValue, setValue,
setError, setError,
reset, reset,
formState: { errors }, formState: { errors, dirtyFields },
} = useForm<IProject>({ } = useForm<IProject>({
defaultValues: { defaultValues: {
...project, ...project,
@ -77,13 +78,15 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
return updateProject(workspaceSlug.toString(), project.id, payload) return updateProject(workspaceSlug.toString(), project.id, payload)
.then((res) => { .then((res) => {
const changed_properties = Object.keys(dirtyFields);
console.log(dirtyFields);
captureProjectEvent({ captureProjectEvent({
eventName: "Project updated", eventName: PROJECT_UPDATED,
payload: { ...res, state: "SUCCESS", element: "Project general settings" }, payload: {
group: { ...res,
isGrouping: true, changed_properties: changed_properties,
groupType: "Workspace_metrics", state: "SUCCESS",
groupId: res.workspace, element: "Project general settings",
}, },
}); });
setToastAlert({ setToastAlert({
@ -94,13 +97,8 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
}) })
.catch((error) => { .catch((error) => {
captureProjectEvent({ captureProjectEvent({
eventName: "Project updated", eventName: PROJECT_UPDATED,
payload: { ...payload, state: "FAILED", element: "Project general settings" }, payload: { ...payload, state: "FAILED", element: "Project general settings" },
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id,
},
}); });
setToastAlert({ setToastAlert({
type: "error", type: "error",
@ -153,7 +151,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
<img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" /> <img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" />
<div className="absolute bottom-4 z-5 flex w-full items-end justify-between gap-3 px-4"> <div className="z-5 absolute bottom-4 flex w-full items-end justify-between gap-3 px-4">
<div className="flex flex-grow gap-3 truncate"> <div className="flex flex-grow gap-3 truncate">
<div className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-custom-background-90"> <div className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-custom-background-90">
<div className="grid h-7 w-7 place-items-center"> <div className="grid h-7 w-7 place-items-center">

View File

@ -11,6 +11,8 @@ import useToast from "hooks/use-toast";
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// types // types
import { IProject } from "@plane/types"; import { IProject } from "@plane/types";
// constants
import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker";
type FormData = { type FormData = {
projectName: string; projectName: string;
@ -63,8 +65,9 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
.then(() => { .then(() => {
handleClose(); handleClose();
router.push(`/${workspaceSlug}/projects`); router.push(`/${workspaceSlug}/projects`);
captureEvent("Project member leave", { captureEvent(PROJECT_MEMBER_LEAVE, {
state: "SUCCESS", state: "SUCCESS",
element: "Project settings members page",
}); });
}) })
.catch(() => { .catch(() => {
@ -73,8 +76,9 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
title: "Error!", title: "Error!",
message: "Something went wrong please try again later.", message: "Something went wrong please try again later.",
}); });
captureEvent("Project member leave", { captureEvent(PROJECT_MEMBER_LEAVE, {
state: "FAILED", state: "FAILED",
element: "Project settings members page",
}); });
}); });
} else { } else {

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useMember, useProject, useUser } from "hooks/store"; import { useEventTracker, useMember, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { ConfirmProjectMemberRemove } from "components/project"; import { ConfirmProjectMemberRemove } from "components/project";
@ -14,6 +14,7 @@ import { ChevronDown, Dot, XCircle } from "lucide-react";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker";
type Props = { type Props = {
userId: string; userId: string;
@ -35,6 +36,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
const { const {
project: { removeMemberFromProject, getProjectMemberDetails, updateMember }, project: { removeMemberFromProject, getProjectMemberDetails, updateMember },
} = useMember(); } = useMember();
const { captureEvent } = useEventTracker();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -48,8 +50,11 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
if (userDetails.member.id === currentUser?.id) { if (userDetails.member.id === currentUser?.id) {
await leaveProject(workspaceSlug.toString(), projectId.toString()) await leaveProject(workspaceSlug.toString(), projectId.toString())
.then(async () => { .then(async () => {
captureEvent(PROJECT_MEMBER_LEAVE, {
state: "SUCCESS",
element: "Project settings members page",
});
await fetchProjects(workspaceSlug.toString()); await fetchProjects(workspaceSlug.toString());
router.push(`/${workspaceSlug}/projects`); router.push(`/${workspaceSlug}/projects`);
}) })
.catch((err) => .catch((err) =>

View File

@ -9,9 +9,12 @@ import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui"; import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui";
// helpers
import { getUserRole } from "helpers/user.helper";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { PROJECT_MEMBER_ADDED } from "constants/event-tracker";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -49,7 +52,6 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { currentWorkspace } = useWorkspace();
const { const {
project: { projectMemberIds, bulkAddMembersToProject }, project: { projectMemberIds, bulkAddMembersToProject },
workspace: { workspaceMemberIds, getWorkspaceMemberDetails }, workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
@ -79,7 +81,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
const payload = { ...formData }; const payload = { ...formData };
await bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload) await bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload)
.then((res) => { .then(() => {
if (onSuccess) onSuccess(); if (onSuccess) onSuccess();
onClose(); onClose();
setToastAlert({ setToastAlert({
@ -87,32 +89,23 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
type: "success", type: "success",
message: "Members added successfully.", message: "Members added successfully.",
}); });
captureEvent( captureEvent(PROJECT_MEMBER_ADDED, {
"Member added", members: [
{ ...payload.members.map((member) => ({
...res, member_id: member.member_id,
role: ROLE[member.role],
})),
],
state: "SUCCESS", state: "SUCCESS",
}, element: "Project settings members page",
{ });
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
}
);
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
captureEvent( captureEvent(PROJECT_MEMBER_ADDED, {
"Member added",
{
state: "FAILED", state: "FAILED",
}, element: "Project settings members page",
{ });
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
}
);
}) })
.finally(() => { .finally(() => {
reset(defaultValues); reset(defaultValues);

View File

@ -51,12 +51,11 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks // store hooks
const { setTrackElement, captureEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
const { const {
currentUser, currentUser,
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { currentWorkspace } = useWorkspace();
const { currentProjectDetails, updateProject } = useProject(); const { currentProjectDetails, updateProject } = useProject();
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
// toast alert // toast alert
@ -91,14 +90,9 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
<ToggleSwitch <ToggleSwitch
value={Boolean(currentProjectDetails?.[feature.property as keyof IProject])} value={Boolean(currentProjectDetails?.[feature.property as keyof IProject])}
onChange={() => { onChange={() => {
setTrackElement("PROJECT_SETTINGS_FEATURES_PAGE");
captureEvent(`Toggle ${feature.title.toLowerCase()}`, { captureEvent(`Toggle ${feature.title.toLowerCase()}`, {
workspace_id: currentWorkspace?.id,
workspace_slug: currentWorkspace?.slug,
project_id: currentProjectDetails?.id,
project_name: currentProjectDetails?.name,
project_identifier: currentProjectDetails?.identifier,
enabled: !currentProjectDetails?.[feature.property as keyof IProject], enabled: !currentProjectDetails?.[feature.property as keyof IProject],
element: "Project settings feature page",
}); });
handleSubmit({ handleSubmit({
[feature.property]: !currentProjectDetails?.[feature.property as keyof IProject], [feature.property]: !currentProjectDetails?.[feature.property as keyof IProject],

View File

@ -13,6 +13,7 @@ import { Button, CustomSelect, Input, Tooltip } from "@plane/ui";
import type { IState } from "@plane/types"; import type { IState } from "@plane/types";
// constants // constants
import { GROUP_CHOICES } from "constants/project"; import { GROUP_CHOICES } from "constants/project";
import { STATE_CREATED, STATE_UPDATED } from "constants/event-tracker";
type Props = { type Props = {
data: IState | null; data: IState | null;
@ -36,7 +37,7 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks // store hooks
const { captureEvent, setTrackElement } = useEventTracker(); const { captureProjectStateEvent, setTrackElement } = useEventTracker();
const { createState, updateState } = useProjectState(); const { createState, updateState } = useProjectState();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -86,9 +87,13 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
title: "Success!", title: "Success!",
message: "State created successfully.", message: "State created successfully.",
}); });
captureEvent("State created", { captureProjectStateEvent({
eventName: STATE_CREATED,
payload: {
...res, ...res,
state: "SUCCESS", state: "SUCCESS",
element: "Project settings states page",
},
}); });
}) })
.catch((error) => { .catch((error) => {
@ -104,8 +109,14 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
title: "Error!", title: "Error!",
message: "State could not be created. Please try again.", message: "State could not be created. Please try again.",
}); });
captureEvent("State created", {
captureProjectStateEvent({
eventName: STATE_CREATED,
payload: {
...formData,
state: "FAILED", state: "FAILED",
element: "Project settings states page",
},
}); });
}); });
}; };
@ -116,9 +127,13 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
await updateState(workspaceSlug.toString(), projectId.toString(), data.id, formData) await updateState(workspaceSlug.toString(), projectId.toString(), data.id, formData)
.then((res) => { .then((res) => {
handleClose(); handleClose();
captureEvent("State updated", { captureProjectStateEvent({
eventName: STATE_UPDATED,
payload: {
...res, ...res,
state: "SUCCESS", state: "SUCCESS",
element: "Project settings states page",
},
}); });
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -139,8 +154,13 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
title: "Error!", title: "Error!",
message: "State could not be updated. Please try again.", message: "State could not be updated. Please try again.",
}); });
captureEvent("State updated", { captureProjectStateEvent({
eventName: STATE_UPDATED,
payload: {
...formData,
state: "FAILED", state: "FAILED",
element: "Project settings states page",
},
}); });
}); });
}; };

View File

@ -10,6 +10,8 @@ import useToast from "hooks/use-toast";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
import type { IState } from "@plane/types"; import type { IState } from "@plane/types";
// constants
import { STATE_DELETED } from "constants/event-tracker";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -25,7 +27,7 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store hooks // store hooks
const { captureEvent } = useEventTracker(); const { captureProjectStateEvent } = useEventTracker();
const { deleteState } = useProjectState(); const { deleteState } = useProjectState();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -42,8 +44,12 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
await deleteState(workspaceSlug.toString(), data.project_id, data.id) await deleteState(workspaceSlug.toString(), data.project_id, data.id)
.then(() => { .then(() => {
captureEvent("State deleted", { captureProjectStateEvent({
eventName: STATE_DELETED,
payload: {
...data,
state: "SUCCESS", state: "SUCCESS",
},
}); });
handleClose(); handleClose();
}) })
@ -61,8 +67,12 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
title: "Error!", title: "Error!",
message: "State could not be deleted. Please try again.", message: "State could not be deleted. Please try again.",
}); });
captureEvent("State deleted", { captureProjectStateEvent({
eventName: STATE_DELETED,
payload: {
...data,
state: "FAILED", state: "FAILED",
},
}); });
}) })
.finally(() => { .finally(() => {

View File

@ -13,6 +13,7 @@ import { Button, CustomSelect, Input } from "@plane/ui";
import { IWorkspace } from "@plane/types"; import { IWorkspace } from "@plane/types";
// constants // constants
import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace"; import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace";
import { WORKSPACE_CREATED } from "constants/event-tracker";
type Props = { type Props = {
onSubmit?: (res: IWorkspace) => Promise<void>; onSubmit?: (res: IWorkspace) => Promise<void>;
@ -48,7 +49,7 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
// router // router
const router = useRouter(); const router = useRouter();
// store hooks // store hooks
const { captureEvent } = useEventTracker(); const { captureWorkspaceEvent } = useEventTracker();
const { createWorkspace } = useWorkspace(); const { createWorkspace } = useWorkspace();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -70,9 +71,13 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
await createWorkspace(formData) await createWorkspace(formData)
.then(async (res) => { .then(async (res) => {
captureEvent("Workspace created", { captureWorkspaceEvent({
eventName: WORKSPACE_CREATED,
payload: {
...res, ...res,
state: "SUCCESS", state: "SUCCESS",
element: "Create workspace page",
},
}); });
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -83,14 +88,18 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
if (onSubmit) await onSubmit(res); if (onSubmit) await onSubmit(res);
}) })
.catch(() => { .catch(() => {
captureWorkspaceEvent({
eventName: WORKSPACE_CREATED,
payload: {
state: "FAILED",
element: "Create workspace page",
},
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "Workspace could not be created. Please try again.", message: "Workspace could not be created. Please try again.",
}); });
captureEvent("Workspace created", {
state: "FAILED",
});
}); });
} else setSlugError(true); } else setSlugError(true);
}) })
@ -100,9 +109,6 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
title: "Error!", title: "Error!",
message: "Some error occurred while creating workspace. Please try again.", message: "Some error occurred while creating workspace. Please try again.",
}); });
captureEvent("Workspace created", {
state: "FAILED",
});
}); });
}; };

View File

@ -11,6 +11,8 @@ import useToast from "hooks/use-toast";
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// types // types
import type { IWorkspace } from "@plane/types"; import type { IWorkspace } from "@plane/types";
// constants
import { WORKSPACE_DELETED } from "constants/event-tracker";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -28,7 +30,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
// router // router
const router = useRouter(); const router = useRouter();
// store hooks // store hooks
const { captureEvent } = useEventTracker(); const { captureWorkspaceEvent } = useEventTracker();
const { deleteWorkspace } = useWorkspace(); const { deleteWorkspace } = useWorkspace();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -59,9 +61,13 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
.then((res) => { .then((res) => {
handleClose(); handleClose();
router.push("/"); router.push("/");
captureEvent("Workspace deleted", { captureWorkspaceEvent({
res, eventName: WORKSPACE_DELETED,
payload: {
...data,
state: "SUCCESS", state: "SUCCESS",
element: "Workspace general settings page",
},
}); });
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -75,8 +81,13 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
title: "Error!", title: "Error!",
message: "Something went wrong. Please try again later.", message: "Something went wrong. Please try again later.",
}); });
captureEvent("Workspace deleted", { captureWorkspaceEvent({
eventName: WORKSPACE_DELETED,
payload: {
...data,
state: "FAILED", state: "FAILED",
element: "Workspace general settings page",
},
}); });
}); });
}; };

View File

@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ChevronDown, Dot, XCircle } from "lucide-react"; import { ChevronDown, Dot, XCircle } from "lucide-react";
// hooks // hooks
import { useMember, useUser } from "hooks/store"; import { useEventTracker, useMember, useUser } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { ConfirmWorkspaceMemberRemove } from "components/workspace"; import { ConfirmWorkspaceMemberRemove } from "components/workspace";
@ -12,6 +12,7 @@ import { ConfirmWorkspaceMemberRemove } from "components/workspace";
import { CustomSelect, Tooltip } from "@plane/ui"; import { CustomSelect, Tooltip } from "@plane/ui";
// constants // constants
import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
import { WORKSPACE_MEMBER_lEAVE } from "constants/event-tracker";
type Props = { type Props = {
memberId: string; memberId: string;
@ -33,6 +34,7 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
const { const {
workspace: { updateMember, removeMemberFromWorkspace, getWorkspaceMemberDetails }, workspace: { updateMember, removeMemberFromWorkspace, getWorkspaceMemberDetails },
} = useMember(); } = useMember();
const { captureEvent } = useEventTracker();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// derived values // derived values
@ -42,7 +44,13 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
if (!workspaceSlug || !currentUserSettings) return; if (!workspaceSlug || !currentUserSettings) return;
await leaveWorkspace(workspaceSlug.toString()) await leaveWorkspace(workspaceSlug.toString())
.then(() => router.push("/profile")) .then(() => {
captureEvent(WORKSPACE_MEMBER_lEAVE, {
state: "SUCCESS",
element: "Workspace settings members page",
});
router.push("/profile");
})
.catch((err) => .catch((err) =>
setToastAlert({ setToastAlert({
type: "error", type: "error",

View File

@ -19,6 +19,7 @@ import { copyUrlToClipboard } from "helpers/string.helper";
import { IWorkspace } from "@plane/types"; import { IWorkspace } from "@plane/types";
// constants // constants
import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace"; import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace";
import { WORKSPACE_UPDATED } from "constants/event-tracker";
const defaultValues: Partial<IWorkspace> = { const defaultValues: Partial<IWorkspace> = {
name: "", name: "",
@ -37,7 +38,7 @@ export const WorkspaceDetails: FC = observer(() => {
const [isImageRemoving, setIsImageRemoving] = useState(false); const [isImageRemoving, setIsImageRemoving] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
// store hooks // store hooks
const { captureEvent } = useEventTracker(); const { captureWorkspaceEvent } = useEventTracker();
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
@ -68,9 +69,13 @@ export const WorkspaceDetails: FC = observer(() => {
await updateWorkspace(currentWorkspace.slug, payload) await updateWorkspace(currentWorkspace.slug, payload)
.then((res) => { .then((res) => {
captureEvent("Workspace updated", { captureWorkspaceEvent({
eventName: WORKSPACE_UPDATED,
payload: {
...res, ...res,
state: "SUCCESS", state: "SUCCESS",
element: "Workspace general settings page",
},
}); });
setToastAlert({ setToastAlert({
title: "Success", title: "Success",
@ -79,8 +84,12 @@ export const WorkspaceDetails: FC = observer(() => {
}); });
}) })
.catch((err) => { .catch((err) => {
captureEvent("Workspace updated", { captureWorkspaceEvent({
eventName: WORKSPACE_UPDATED,
payload: {
state: "FAILED", state: "FAILED",
element: "Workspace general settings page",
},
}); });
console.error(err); console.error(err);
}); });

View File

@ -222,7 +222,6 @@ export const WorkspaceSidebarDropdown = observer(() => {
<div className="flex w-full flex-col items-start justify-start gap-2 px-4 py-2 text-sm"> <div className="flex w-full flex-col items-start justify-start gap-2 px-4 py-2 text-sm">
<Link <Link
href="/create-workspace" href="/create-workspace"
onClick={() => setTrackElement("APP_SIDEBAR_WORKSPACE_DROPDOWN")}
className="w-full" className="w-full"
> >
<Menu.Item <Menu.Item

View File

@ -3,7 +3,7 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useApplication, useUser } from "hooks/store"; import { useApplication, useEventTracker, useUser } from "hooks/store";
// components // components
import { NotificationPopover } from "components/notifications"; import { NotificationPopover } from "components/notifications";
// ui // ui
@ -12,12 +12,14 @@ import { Crown } from "lucide-react";
// constants // constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { SIDEBAR_MENU_ITEMS } from "constants/dashboard"; import { SIDEBAR_MENU_ITEMS } from "constants/dashboard";
import { SIDEBAR_CLICKED } from "constants/event-tracker";
// helper // helper
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
export const WorkspaceSidebarMenu = observer(() => { export const WorkspaceSidebarMenu = observer(() => {
// store hooks // store hooks
const { theme: themeStore } = useApplication(); const { theme: themeStore } = useApplication();
const { captureEvent } = useEventTracker();
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
@ -27,10 +29,13 @@ export const WorkspaceSidebarMenu = observer(() => {
// computed // computed
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST; const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
const handleLinkClick = () => { const handleLinkClick = (itemKey: string) => {
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
themeStore.toggleSidebar(); themeStore.toggleSidebar();
} }
captureEvent(SIDEBAR_CLICKED, {
destination: itemKey,
});
}; };
return ( return (
@ -38,11 +43,8 @@ export const WorkspaceSidebarMenu = observer(() => {
{SIDEBAR_MENU_ITEMS.map( {SIDEBAR_MENU_ITEMS.map(
(link) => (link) =>
workspaceMemberInfo >= link.access && ( workspaceMemberInfo >= link.access && (
<Link key={link.key} <Link key={link.key} href={`/${workspaceSlug}${link.href}`} onClick={() => handleLinkClick(link.key)}>
href={`/${workspaceSlug}${link.href}`} <span className="my-1 block w-full">
onClick={handleLinkClick}
>
<span className="block w-full my-1">
<Tooltip <Tooltip
tooltipContent={link.label} tooltipContent={link.label}
position="right" position="right"
@ -50,7 +52,8 @@ export const WorkspaceSidebarMenu = observer(() => {
disabled={!themeStore?.sidebarCollapsed} disabled={!themeStore?.sidebarCollapsed}
> >
<div <div
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${link.highlight(router.asPath, `/${workspaceSlug}`) className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
link.highlight(router.asPath, `/${workspaceSlug}`)
? "bg-custom-primary-100/10 text-custom-primary-100" ? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80" : "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`} } ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`}

View File

@ -4,12 +4,14 @@ import { Dialog, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
// store hooks // store hooks
import { useGlobalView } from "hooks/store"; import { useGlobalView, useEventTracker } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
import { IWorkspaceView } from "@plane/types"; import { IWorkspaceView } from "@plane/types";
// constants
import { GLOBAL_VIEW_DELETED } from "constants/event-tracker";
type Props = { type Props = {
data: IWorkspaceView; data: IWorkspaceView;
@ -26,6 +28,7 @@ export const DeleteGlobalViewModal: React.FC<Props> = observer((props) => {
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store hooks // store hooks
const { deleteGlobalView } = useGlobalView(); const { deleteGlobalView } = useGlobalView();
const { captureEvent } = useEventTracker();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -39,13 +42,23 @@ export const DeleteGlobalViewModal: React.FC<Props> = observer((props) => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
await deleteGlobalView(workspaceSlug.toString(), data.id) await deleteGlobalView(workspaceSlug.toString(), data.id)
.catch(() => .then(() => {
captureEvent(GLOBAL_VIEW_DELETED, {
view_id: data.id,
state: "SUCCESS",
});
})
.catch(() => {
captureEvent(GLOBAL_VIEW_DELETED, {
view_id: data.id,
state: "FAILED",
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "Something went wrong while deleting the view. Please try again.", message: "Something went wrong while deleting the view. Please try again.",
});
}) })
)
.finally(() => { .finally(() => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
handleClose(); handleClose();

View File

@ -4,11 +4,12 @@ import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// store hooks // store hooks
import { useGlobalView, useUser } from "hooks/store"; import { useEventTracker, useGlobalView, useUser } from "hooks/store";
// components // components
import { CreateUpdateWorkspaceViewModal } from "components/workspace"; import { CreateUpdateWorkspaceViewModal } from "components/workspace";
// constants // constants
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
import { GLOBAL_VIEW_OPENED } from "constants/event-tracker";
const ViewTab = observer((props: { viewId: string }) => { const ViewTab = observer((props: { viewId: string }) => {
const { viewId } = props; const { viewId } = props;
@ -49,11 +50,19 @@ export const GlobalViewsHeader: React.FC = observer(() => {
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
const { captureEvent } = useEventTracker();
// bring the active view to the centre of the header // bring the active view to the centre of the header
useEffect(() => { useEffect(() => {
if (!globalViewId) return; if (!globalViewId) return;
captureEvent(GLOBAL_VIEW_OPENED, {
view_id: globalViewId,
view_type: ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString())
? "Default"
: "Custom",
});
const activeTabElement = document.querySelector(`#global-view-${globalViewId.toString()}`); const activeTabElement = document.querySelector(`#global-view-${globalViewId.toString()}`);
if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" }); if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" });

View File

@ -3,12 +3,14 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// store hooks // store hooks
import { useGlobalView } from "hooks/store"; import { useEventTracker, useGlobalView } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { WorkspaceViewForm } from "components/workspace"; import { WorkspaceViewForm } from "components/workspace";
// types // types
import { IWorkspaceView } from "@plane/types"; import { IWorkspaceView } from "@plane/types";
// constants
import { GLOBAL_VIEW_CREATED, GLOBAL_VIEW_UPDATED } from "constants/event-tracker";
type Props = { type Props = {
data?: IWorkspaceView; data?: IWorkspaceView;
@ -24,6 +26,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store hooks // store hooks
const { createGlobalView, updateGlobalView } = useGlobalView(); const { createGlobalView, updateGlobalView } = useGlobalView();
const { captureEvent } = useEventTracker();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -43,6 +46,11 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
await createGlobalView(workspaceSlug.toString(), payloadData) await createGlobalView(workspaceSlug.toString(), payloadData)
.then((res) => { .then((res) => {
captureEvent(GLOBAL_VIEW_CREATED, {
view_id: res.id,
applied_filters: res.filters,
state: "SUCCESS",
});
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -52,13 +60,17 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
router.push(`/${workspaceSlug}/workspace-views/${res.id}`); router.push(`/${workspaceSlug}/workspace-views/${res.id}`);
handleClose(); handleClose();
}) })
.catch(() => .catch(() => {
captureEvent(GLOBAL_VIEW_CREATED, {
applied_filters: payload?.filters,
state: "FAILED",
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "View could not be created. Please try again.", message: "View could not be created. Please try again.",
}) });
); });
}; };
const handleUpdateView = async (payload: Partial<IWorkspaceView>) => { const handleUpdateView = async (payload: Partial<IWorkspaceView>) => {
@ -72,7 +84,12 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
}; };
await updateGlobalView(workspaceSlug.toString(), data.id, payloadData) await updateGlobalView(workspaceSlug.toString(), data.id, payloadData)
.then(() => { .then((res) => {
captureEvent(GLOBAL_VIEW_UPDATED, {
view_id: res.id,
applied_filters: res.filters,
state: "SUCCESS",
});
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -80,13 +97,18 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
}); });
handleClose(); handleClose();
}) })
.catch(() => .catch(() => {
captureEvent(GLOBAL_VIEW_UPDATED, {
view_id: data.id,
applied_filters: data.filters,
state: "FAILED",
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "View could not be updated. Please try again.", message: "View could not be updated. Please try again.",
}) });
); });
}; };
const handleFormSubmit = async (formData: Partial<IWorkspaceView>) => { const handleFormSubmit = async (formData: Partial<IWorkspaceView>) => {

View File

@ -4,7 +4,7 @@ import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Pencil, Trash2 } from "lucide-react"; import { Pencil, Trash2 } from "lucide-react";
// store hooks // store hooks
import { useGlobalView } from "hooks/store"; import { useEventTracker, useGlobalView } from "hooks/store";
// components // components
import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace"; import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace";
// ui // ui
@ -25,6 +25,7 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store hooks // store hooks
const { getViewDetailsById } = useGlobalView(); const { getViewDetailsById } = useGlobalView();
const {setTrackElement} = useEventTracker();
// derived data // derived data
const view = getViewDetailsById(viewId); const view = getViewDetailsById(viewId);
@ -59,6 +60,7 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setTrackElement("List view");
setUpdateViewModal(true); setUpdateViewModal(true);
}} }}
> >

View File

@ -2,26 +2,31 @@ export type IssueEventProps = {
eventName: string; eventName: string;
payload: any; payload: any;
updates?: any; updates?: any;
group?: EventGroupProps;
path?: string; path?: string;
}; };
export type EventProps = { export type EventProps = {
eventName: string; eventName: string;
payload: any; payload: any;
group?: EventGroupProps;
}; };
export type EventGroupProps = { export const getWorkspaceEventPayload = (payload: any) => ({
isGrouping?: boolean; workspace_id: payload.id,
groupType?: string; created_at: payload.created_at,
groupId?: string; updated_at: payload.updated_at,
}; organization_size: payload.organization_size,
first_time: payload.first_time,
state: payload.state,
element: payload.element,
});
export const getProjectEventPayload = (payload: any) => ({ export const getProjectEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id, workspace_id: payload.workspace_id,
project_id: payload.id, project_id: payload.id,
identifier: payload.identifier, identifier: payload.identifier,
project_visibility: payload.network == 2 ? "Public" : "Private",
changed_properties: payload.changed_properties,
lead_id: payload.project_lead,
created_at: payload.created_at, created_at: payload.created_at,
updated_at: payload.updated_at, updated_at: payload.updated_at,
state: payload.state, state: payload.state,
@ -30,26 +35,43 @@ export const getProjectEventPayload = (payload: any) => ({
export const getCycleEventPayload = (payload: any) => ({ export const getCycleEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id, workspace_id: payload.workspace_id,
project_id: payload.id, project_id: payload.project,
cycle_id: payload.id, cycle_id: payload.id,
created_at: payload.created_at, created_at: payload.created_at,
updated_at: payload.updated_at, updated_at: payload.updated_at,
start_date: payload.start_date, start_date: payload.start_date,
target_date: payload.target_date, target_date: payload.target_date,
cycle_status: payload.status, cycle_status: payload.status,
changed_properties: payload.changed_properties,
state: payload.state, state: payload.state,
element: payload.element, element: payload.element,
}); });
export const getModuleEventPayload = (payload: any) => ({ export const getModuleEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id, workspace_id: payload.workspace_id,
project_id: payload.id, project_id: payload.project,
module_id: payload.id, module_id: payload.id,
created_at: payload.created_at, created_at: payload.created_at,
updated_at: payload.updated_at, updated_at: payload.updated_at,
start_date: payload.start_date, start_date: payload.start_date,
target_date: payload.target_date, target_date: payload.target_date,
module_status: payload.status, module_status: payload.status,
lead_id: payload.lead,
changed_properties: payload.changed_properties,
member_ids: payload.members,
state: payload.state,
element: payload.element,
});
export const getPageEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id,
project_id: payload.project,
created_at: payload.created_at,
updated_at: payload.updated_at,
access: payload.access === 0 ? "Public" : "Private",
is_locked: payload.is_locked,
archived_at: payload.archived_at,
created_by: payload.created_by,
state: payload.state, state: payload.state,
element: payload.element, element: payload.element,
}); });
@ -71,6 +93,7 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
sub_issues_count: payload.sub_issues_count, sub_issues_count: payload.sub_issues_count,
parent_id: payload.parent_id, parent_id: payload.parent_id,
project_id: payload.project_id, project_id: payload.project_id,
workspace_id: payload.workspace_id,
priority: payload.priority, priority: payload.priority,
state_id: payload.state_id, state_id: payload.state_id,
start_date: payload.start_date, start_date: payload.start_date,
@ -82,7 +105,7 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
view_id: path?.includes("workspace-views") || path?.includes("views") ? path.split("/").pop() : "", view_id: path?.includes("workspace-views") || path?.includes("views") ? path.split("/").pop() : "",
}; };
if (eventName === "Issue updated") { if (eventName === ISSUE_UPDATED) {
eventPayload = { eventPayload = {
...eventPayload, ...eventPayload,
...updates, ...updates,
@ -103,3 +126,99 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
} }
return eventPayload; return eventPayload;
}; };
export const getProjectStateEventPayload = (payload: any) => {
return {
workspace_id: payload.workspace_id,
project_id: payload.id,
state_id: payload.id,
created_at: payload.created_at,
updated_at: payload.updated_at,
group: payload.group,
color: payload.color,
default: payload.default,
state: payload.state,
element: payload.element,
};
};
// Workspace crud Events
export const WORKSPACE_CREATED = "Workspace created";
export const WORKSPACE_UPDATED = "Workspace updated";
export const WORKSPACE_DELETED = "Workspace deleted";
// Project Events
export const PROJECT_CREATED = "Project created";
export const PROJECT_UPDATED = "Project updated";
export const PROJECT_DELETED = "Project deleted";
// Cycle Events
export const CYCLE_CREATED = "Cycle created";
export const CYCLE_UPDATED = "Cycle updated";
export const CYCLE_DELETED = "Cycle deleted";
export const CYCLE_FAVORITED = "Cycle favorited";
export const CYCLE_UNFAVORITED = "Cycle unfavorited";
// Module Events
export const MODULE_CREATED = "Module created";
export const MODULE_UPDATED = "Module updated";
export const MODULE_DELETED = "Module deleted";
export const MODULE_FAVORITED = "Module favorited";
export const MODULE_UNFAVORITED = "Module unfavorited";
export const MODULE_LINK_CREATED = "Module link created";
export const MODULE_LINK_UPDATED = "Module link updated";
export const MODULE_LINK_DELETED = "Module link deleted";
// Issue Events
export const ISSUE_CREATED = "Issue created";
export const ISSUE_UPDATED = "Issue updated";
export const ISSUE_DELETED = "Issue deleted";
export const ISSUE_OPENED = "Issue opened";
// Project State Events
export const STATE_CREATED = "State created";
export const STATE_UPDATED = "State updated";
export const STATE_DELETED = "State deleted";
// Project Page Events
export const PAGE_CREATED = "Page created";
export const PAGE_UPDATED = "Page updated";
export const PAGE_DELETED = "Page deleted";
// Member Events
export const MEMBER_INVITED = "Member invited";
export const MEMBER_ACCEPTED = "Member accepted";
export const PROJECT_MEMBER_ADDED = "Project member added";
export const PROJECT_MEMBER_LEAVE = "Project member leave";
export const WORKSPACE_MEMBER_lEAVE = "Workspace member leave";
// Sign-in & Sign-up Events
export const NAVIGATE_TO_SIGNUP = "Navigate to sign-up page";
export const NAVIGATE_TO_SIGNIN = "Navigate to sign-in page";
export const CODE_VERIFIED = "Code verified";
export const SETUP_PASSWORD = "Password setup";
export const PASSWORD_CREATE_SELECTED = "Password created";
export const PASSWORD_CREATE_SKIPPED = "Skipped to setup";
export const SIGN_IN_WITH_PASSWORD = "Sign in with password";
export const FORGOT_PASSWORD = "Forgot password clicked";
export const FORGOT_PASS_LINK = "Forgot password link generated";
export const NEW_PASS_CREATED = "New password created";
// Onboarding Events
export const USER_DETAILS = "User details added";
export const USER_ONBOARDING_COMPLETED = "User onboarding completed";
// Product Tour Events
export const PRODUCT_TOUR_STARTED = "Product tour started";
export const PRODUCT_TOUR_COMPLETED = "Product tour completed";
export const PRODUCT_TOUR_SKIPPED = "Product tour skipped";
// Dashboard Events
export const CHANGELOG_REDIRECTED = "Changelog redirected";
export const GITHUB_REDIRECTED = "Github redirected";
// Sidebar Events
export const SIDEBAR_CLICKED = "Sidenav clicked";
// Global View Events
export const GLOBAL_VIEW_CREATED = "Global view created";
export const GLOBAL_VIEW_UPDATED = "Global view updated";
export const GLOBAL_VIEW_DELETED = "Global view deleted";
export const GLOBAL_VIEW_OPENED = "Global view opened";
// Notification Events
export const NOTIFICATION_ARCHIVED = "Notification archived";
export const NOTIFICATION_SNOOZED = "Notification snoozed";
export const NOTIFICATION_READ = "Notification marked read";
export const UNREAD_NOTIFICATIONS = "Unread notifications viewed";
export const NOTIFICATIONS_READ = "All notifications marked read";
export const SNOOZED_NOTIFICATIONS= "Snoozed notifications viewed";
export const ARCHIVED_NOTIFICATIONS = "Archived notifications viewed";
// Groups
export const GROUP_WORKSPACE = "Workspace_metrics";

View File

@ -5,7 +5,7 @@ import NProgress from "nprogress";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
// hooks // hooks
import { useApplication, useUser } from "hooks/store"; import { useApplication, useUser, useWorkspace } from "hooks/store";
// constants // constants
import { THEMES } from "constants/themes"; import { THEMES } from "constants/themes";
// layouts // layouts
@ -37,6 +37,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
currentUser, currentUser,
membership: { currentProjectRole, currentWorkspaceRole }, membership: { currentProjectRole, currentWorkspaceRole },
} = useUser(); } = useUser();
const { currentWorkspace } = useWorkspace();
const { const {
config: { envConfig }, config: { envConfig },
} = useApplication(); } = useApplication();
@ -49,6 +50,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
<CrispWrapper user={currentUser}> <CrispWrapper user={currentUser}>
<PostHogProvider <PostHogProvider
user={currentUser} user={currentUser}
currentWorkspaceId= {currentWorkspace?.id}
workspaceRole={currentWorkspaceRole} workspaceRole={currentWorkspaceRole}
projectRole={currentProjectRole} projectRole={currentProjectRole}
posthogAPIKey={envConfig?.posthog_api_key || null} posthogAPIKey={envConfig?.posthog_api_key || null}

View File

@ -1,4 +1,4 @@
import { FC, ReactNode, useEffect } from "react"; import { FC, ReactNode, useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import posthog from "posthog-js"; import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react"; import { PostHogProvider as PHProvider } from "posthog-js/react";
@ -6,10 +6,13 @@ import { PostHogProvider as PHProvider } from "posthog-js/react";
import { IUser } from "@plane/types"; import { IUser } from "@plane/types";
// helpers // helpers
import { getUserRole } from "helpers/user.helper"; import { getUserRole } from "helpers/user.helper";
// constants
import { GROUP_WORKSPACE } from "constants/event-tracker";
export interface IPosthogWrapper { export interface IPosthogWrapper {
children: ReactNode; children: ReactNode;
user: IUser | null; user: IUser | null;
currentWorkspaceId: string | undefined;
workspaceRole: number | undefined; workspaceRole: number | undefined;
projectRole: number | undefined; projectRole: number | undefined;
posthogAPIKey: string | null; posthogAPIKey: string | null;
@ -17,7 +20,9 @@ export interface IPosthogWrapper {
} }
const PostHogProvider: FC<IPosthogWrapper> = (props) => { const PostHogProvider: FC<IPosthogWrapper> = (props) => {
const { children, user, workspaceRole, projectRole, posthogAPIKey, posthogHost } = props; const { children, user, workspaceRole, currentWorkspaceId, projectRole, posthogAPIKey, posthogHost } = props;
// states
const [lastWorkspaceId, setLastWorkspaceId] = useState(currentWorkspaceId);
// router // router
const router = useRouter(); const router = useRouter();
@ -25,10 +30,11 @@ const PostHogProvider: FC<IPosthogWrapper> = (props) => {
if (user) { if (user) {
// Identify sends an event, so you want may want to limit how often you call it // Identify sends an event, so you want may want to limit how often you call it
posthog?.identify(user.email, { posthog?.identify(user.email, {
email: user.email, id: user.id,
first_name: user.first_name, first_name: user.first_name,
last_name: user.last_name, last_name: user.last_name,
id: user.id, email: user.email,
use_case: user.use_case,
workspace_role: workspaceRole ? getUserRole(workspaceRole) : undefined, workspace_role: workspaceRole ? getUserRole(workspaceRole) : undefined,
project_role: projectRole ? getUserRole(projectRole) : undefined, project_role: projectRole ? getUserRole(projectRole) : undefined,
}); });
@ -45,6 +51,15 @@ const PostHogProvider: FC<IPosthogWrapper> = (props) => {
} }
}, [posthogAPIKey, posthogHost]); }, [posthogAPIKey, posthogHost]);
useEffect(() => {
// Join workspace group on workspace change
if (lastWorkspaceId !== currentWorkspaceId && currentWorkspaceId && user) {
setLastWorkspaceId(currentWorkspaceId);
posthog?.identify(user.email);
posthog?.group(GROUP_WORKSPACE, currentWorkspaceId);
}
}, [currentWorkspaceId, user]);
useEffect(() => { useEffect(() => {
// Track page views // Track page views
const handleRouteChange = () => { const handleRouteChange = () => {

View File

@ -45,7 +45,7 @@
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"posthog-js": "^1.88.4", "posthog-js": "^1.105.0",
"react": "18.2.0", "react": "18.2.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-datepicker": "^4.8.0", "react-datepicker": "^4.8.0",

View File

@ -6,7 +6,7 @@ import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// hooks // hooks
import { useApplication, useUser } from "hooks/store"; import { useApplication, useEventTracker, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// layouts // layouts
@ -60,6 +60,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
const { const {
commandPalette: { toggleCreatePageModal }, commandPalette: { toggleCreatePageModal },
} = useApplication(); } = useApplication();
const { setTrackElement } = useEventTracker();
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } = const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
useProjectPages(); useProjectPages();
@ -194,7 +195,10 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
description="Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your projects context. To make short work of any doc, invoke Galileo, Planes AI, with a shortcut or the click of a button." description="Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your projects context. To make short work of any doc, invoke Galileo, Planes AI, with a shortcut or the click of a button."
primaryButton={{ primaryButton={{
text: "Create your first page", text: "Create your first page",
onClick: () => toggleCreatePageModal(true), onClick: () => {
setTrackElement("Pages empty state");
toggleCreatePageModal(true);
},
}} }}
comicBox={{ comicBox={{
title: "A page can be a doc or a doc of docs.", title: "A page can be a doc or a doc of docs.",

View File

@ -16,8 +16,11 @@ import { Button } from "@plane/ui";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { IWorkspaceBulkInviteFormData } from "@plane/types"; import { IWorkspaceBulkInviteFormData } from "@plane/types";
// helpers
import { getUserRole } from "helpers/user.helper";
// constants // constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { MEMBER_INVITED } from "constants/event-tracker";
const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
// states // states
@ -43,7 +46,17 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
return inviteMembersToWorkspace(workspaceSlug.toString(), data) return inviteMembersToWorkspace(workspaceSlug.toString(), data)
.then(() => { .then(() => {
setInviteModal(false); setInviteModal(false);
captureEvent("Member invited", { state: "SUCCESS" }); captureEvent(MEMBER_INVITED, {
emails: [
...data.emails.map((email) => ({
email: email.email,
role: getUserRole(email.role),
})),
],
project_id: undefined,
state: "SUCCESS",
element: "Workspace settings member page",
});
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -51,7 +64,17 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
}); });
}) })
.catch((err) => { .catch((err) => {
captureEvent("Member invited", { state: "FAILED" }); captureEvent(MEMBER_INVITED, {
emails: [
...data.emails.map((email) => ({
email: email.email,
role: getUserRole(email.role),
})),
],
project_id: undefined,
state: "FAILED",
element: "Workspace settings member page",
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -84,14 +107,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
/> />
</div> </div>
{hasAddMemberPermission && ( {hasAddMemberPermission && (
<Button <Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
variant="primary"
size="sm"
onClick={() => {
setTrackElement("WORKSPACE_SETTINGS_MEMBERS_PAGE_HEADER");
setInviteModal(true);
}}
>
Add member Add member
</Button> </Button>
)} )}

View File

@ -7,6 +7,7 @@ import { AuthService } from "services/auth.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useTimer from "hooks/use-timer"; import useTimer from "hooks/use-timer";
import { useEventTracker } from "hooks/store";
// layouts // layouts
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// components // components
@ -19,6 +20,7 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// type // type
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { FORGOT_PASS_LINK } from "constants/event-tracker";
type TForgotPasswordFormValues = { type TForgotPasswordFormValues = {
email: string; email: string;
@ -35,6 +37,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
// router // router
const router = useRouter(); const router = useRouter();
const { email } = router.query; const { email } = router.query;
// store hooks
const { captureEvent } = useEventTracker();
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// timer // timer
@ -57,6 +61,9 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
email: formData.email, email: formData.email,
}) })
.then(() => { .then(() => {
captureEvent(FORGOT_PASS_LINK, {
state: "SUCCESS",
});
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Email sent", title: "Email sent",
@ -65,13 +72,16 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
}); });
setResendCodeTimer(30); setResendCodeTimer(30);
}) })
.catch((err) => .catch((err) => {
captureEvent(FORGOT_PASS_LINK, {
state: "FAILED",
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.", message: err?.error ?? "Something went wrong. Please try again.",
}) });
); });
}; };
return ( return (

View File

@ -7,6 +7,7 @@ import { AuthService } from "services/auth.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useSignInRedirection from "hooks/use-sign-in-redirection"; import useSignInRedirection from "hooks/use-sign-in-redirection";
import { useEventTracker } from "hooks/store";
// layouts // layouts
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// components // components
@ -21,6 +22,8 @@ import { checkEmailValidity } from "helpers/string.helper";
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// icons // icons
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
// constants
import { NEW_PASS_CREATED } from "constants/event-tracker";
type TResetPasswordFormValues = { type TResetPasswordFormValues = {
email: string; email: string;
@ -41,6 +44,8 @@ const ResetPasswordPage: NextPageWithLayout = () => {
const { uidb64, token, email } = router.query; const { uidb64, token, email } = router.query;
// states // states
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// sign in redirection hook // sign in redirection hook
@ -66,14 +71,22 @@ const ResetPasswordPage: NextPageWithLayout = () => {
await authService await authService
.resetPassword(uidb64.toString(), token.toString(), payload) .resetPassword(uidb64.toString(), token.toString(), payload)
.then(() => handleRedirection()) .then(() => {
.catch((err) => captureEvent(NEW_PASS_CREATED, {
state: "SUCCESS",
});
handleRedirection();
})
.catch((err) => {
captureEvent(NEW_PASS_CREATED, {
state: "FAILED",
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.", message: err?.error ?? "Something went wrong. Please try again.",
}) });
); });
}; };
return ( return (

View File

@ -23,11 +23,13 @@ import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-l
import emptyInvitation from "public/empty-state/invitation.svg"; import emptyInvitation from "public/empty-state/invitation.svg";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { getUserRole } from "helpers/user.helper";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import type { IWorkspaceMemberInvitation } from "@plane/types"; import type { IWorkspaceMemberInvitation } from "@plane/types";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
import { MEMBER_ACCEPTED } from "constants/event-tracker";
// components // components
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
@ -40,7 +42,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]); const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
// store hooks // store hooks
const { captureEvent } = useEventTracker(); const { captureEvent, joinWorkspaceMetricGroup } = useEventTracker();
const { currentUser, currentUserSettings } = useUser(); const { currentUser, currentUserSettings } = useUser();
// router // router
const router = useRouter(); const router = useRouter();
@ -81,11 +83,16 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
.then((res) => { .then((res) => {
mutate("USER_WORKSPACES"); mutate("USER_WORKSPACES");
const firstInviteId = invitationsRespond[0]; const firstInviteId = invitationsRespond[0];
const invitation = invitations?.find((i) => i.id === firstInviteId);
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace; const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
captureEvent("Member accepted", { joinWorkspaceMetricGroup(redirectWorkspace?.id);
...res, captureEvent(MEMBER_ACCEPTED, {
state: "SUCCESS", member_id: invitation?.id,
role: getUserRole(invitation?.role!),
project_id: undefined,
accepted_from: "App", accepted_from: "App",
state: "SUCCESS",
element: "Workspace invitations page",
}); });
userService userService
.updateUser({ last_workspace_id: redirectWorkspace?.id }) .updateUser({ last_workspace_id: redirectWorkspace?.id })
@ -103,6 +110,12 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
}); });
}) })
.catch(() => { .catch(() => {
captureEvent(MEMBER_ACCEPTED, {
project_id: undefined,
accepted_from: "App",
state: "FAILED",
element: "Workspace invitations page",
});
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",

View File

@ -24,6 +24,8 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// types // types
import { IUser, TOnboardingSteps } from "@plane/types"; import { IUser, TOnboardingSteps } from "@plane/types";
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants
import { USER_ONBOARDING_COMPLETED } from "constants/event-tracker";
// services // services
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
@ -79,7 +81,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
await updateUserOnBoard() await updateUserOnBoard()
.then(() => { .then(() => {
captureEvent("User onboarding completed", { captureEvent(USER_ONBOARDING_COMPLETED, {
user_role: user.role, user_role: user.role,
email: user.email, email: user.email,
user_id: user.id, user_id: user.id,

View File

@ -3,39 +3,49 @@ import posthog from "posthog-js";
// stores // stores
import { RootStore } from "./root.store"; import { RootStore } from "./root.store";
import { import {
EventGroupProps, GROUP_WORKSPACE,
WORKSPACE_CREATED,
EventProps, EventProps,
IssueEventProps, IssueEventProps,
getCycleEventPayload, getCycleEventPayload,
getIssueEventPayload, getIssueEventPayload,
getModuleEventPayload, getModuleEventPayload,
getProjectEventPayload, getProjectEventPayload,
getProjectStateEventPayload,
getWorkspaceEventPayload,
getPageEventPayload,
} from "constants/event-tracker"; } from "constants/event-tracker";
export interface IEventTrackerStore { export interface IEventTrackerStore {
// properties // properties
trackElement: string; trackElement: string | undefined;
// computed // computed
getRequiredPayload: any; getRequiredProperties: any;
// actions // actions
resetSession: () => void;
setTrackElement: (element: string) => 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; captureProjectEvent: (props: EventProps) => void;
captureCycleEvent: (props: EventProps) => void; captureCycleEvent: (props: EventProps) => void;
captureModuleEvent: (props: EventProps) => void; captureModuleEvent: (props: EventProps) => void;
capturePageEvent: (props: EventProps) => void;
captureIssueEvent: (props: IssueEventProps) => void; captureIssueEvent: (props: IssueEventProps) => void;
captureProjectStateEvent: (props: EventProps) => void;
} }
export class EventTrackerStore implements IEventTrackerStore { export class EventTrackerStore implements IEventTrackerStore {
trackElement: string = ""; trackElement: string | undefined = undefined;
rootStore; rootStore;
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
// properties // properties
trackElement: observable, trackElement: observable,
// computed // computed
getRequiredPayload: computed, getRequiredProperties: computed,
// actions // actions
resetSession: action,
setTrackElement: action, setTrackElement: action,
captureEvent: action, captureEvent: action,
captureProjectEvent: action, captureProjectEvent: action,
@ -48,12 +58,12 @@ export class EventTrackerStore implements IEventTrackerStore {
/** /**
* @description: Returns the necessary property for the event tracking * @description: Returns the necessary property for the event tracking
*/ */
get getRequiredPayload() { get getRequiredProperties() {
const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace; const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails; const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails;
return { return {
workspace_id: currentWorkspaceDetails?.id ?? "", workspace_id: currentWorkspaceDetails?.id,
project_id: currentProjectDetails?.id ?? "", project_id: currentProjectDetails?.id,
}; };
} }
@ -61,42 +71,74 @@ export class EventTrackerStore implements IEventTrackerStore {
* @description: Set the trigger point of event. * @description: Set the trigger point of event.
* @param {string} element * @param {string} element
*/ */
setTrackElement = (element: string) => { setTrackElement = (element?: string) => {
this.trackElement = element; this.trackElement = element;
}; };
postHogGroup = (group: EventGroupProps) => { /**
if (group && group!.isGrouping === true) { * @description: Reset the session.
posthog?.group(group!.groupType!, group!.groupId!, { */
date: new Date(), resetSession = () => {
workspace_id: group!.groupId, posthog?.reset();
});
}
}; };
captureEvent = (eventName: string, payload: object | [] | null) => { /**
posthog?.capture(eventName, { * @description: Creates the workspace metric group.
...payload, * @param {string} userEmail
element: this.trackElement ?? "", * @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. * @description: Captures the project related events.
* @param {EventProps} props * @param {EventProps} props
*/ */
captureProjectEvent = (props: EventProps) => { captureProjectEvent = (props: EventProps) => {
const { eventName, payload, group } = props; const { eventName, payload } = props;
if (group) {
this.postHogGroup(group);
}
const eventPayload: any = getProjectEventPayload({ const eventPayload: any = getProjectEventPayload({
...this.getRequiredPayload, ...this.getRequiredProperties,
...payload, ...payload,
element: payload.element ?? this.trackElement, element: payload.element ?? this.trackElement,
}); });
posthog?.capture(eventName, eventPayload); posthog?.capture(eventName, eventPayload);
this.setTrackElement(""); this.setTrackElement(undefined);
}; };
/** /**
@ -104,17 +146,14 @@ export class EventTrackerStore implements IEventTrackerStore {
* @param {EventProps} props * @param {EventProps} props
*/ */
captureCycleEvent = (props: EventProps) => { captureCycleEvent = (props: EventProps) => {
const { eventName, payload, group } = props; const { eventName, payload } = props;
if (group) {
this.postHogGroup(group);
}
const eventPayload: any = getCycleEventPayload({ const eventPayload: any = getCycleEventPayload({
...this.getRequiredPayload, ...this.getRequiredProperties,
...payload, ...payload,
element: payload.element ?? this.trackElement, element: payload.element ?? this.trackElement,
}); });
posthog?.capture(eventName, eventPayload); posthog?.capture(eventName, eventPayload);
this.setTrackElement(""); this.setTrackElement(undefined);
}; };
/** /**
@ -122,17 +161,29 @@ export class EventTrackerStore implements IEventTrackerStore {
* @param {EventProps} props * @param {EventProps} props
*/ */
captureModuleEvent = (props: EventProps) => { captureModuleEvent = (props: EventProps) => {
const { eventName, payload, group } = props; const { eventName, payload } = props;
if (group) {
this.postHogGroup(group);
}
const eventPayload: any = getModuleEventPayload({ const eventPayload: any = getModuleEventPayload({
...this.getRequiredPayload, ...this.getRequiredProperties,
...payload, ...payload,
element: payload.element ?? this.trackElement, element: payload.element ?? this.trackElement,
}); });
posthog?.capture(eventName, eventPayload); 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 * @param {IssueEventProps} props
*/ */
captureIssueEvent = (props: IssueEventProps) => { captureIssueEvent = (props: IssueEventProps) => {
const { eventName, payload, group } = props; const { eventName, payload } = props;
if (group) {
this.postHogGroup(group);
}
const eventPayload: any = { const eventPayload: any = {
...getIssueEventPayload(props), ...getIssueEventPayload(props),
...this.getRequiredPayload, ...this.getRequiredProperties,
state_group: this.rootStore.state.getStateById(payload.state_id)?.group ?? "", state_group: this.rootStore.state.getStateById(payload.state_id)?.group ?? "",
element: payload.element ?? this.trackElement, element: payload.element ?? this.trackElement,
}; };
posthog?.capture(eventName, eventPayload); 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);
}; };
} }

View File

@ -1,7 +1,7 @@
import sortBy from "lodash/sortBy"; import orderBy from "lodash/orderBy";
import get from "lodash/get"; import get from "lodash/get";
import indexOf from "lodash/indexOf"; import indexOf from "lodash/indexOf";
import reverse from "lodash/reverse"; import isEmpty from "lodash/isEmpty";
import values from "lodash/values"; import values from "lodash/values";
// types // types
import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types"; import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
@ -144,98 +144,189 @@ export class IssueHelperStore implements TIssueHelperStore {
issueDisplayFiltersDefaultData = (groupBy: string | null): string[] => { issueDisplayFiltersDefaultData = (groupBy: string | null): string[] => {
switch (groupBy) { switch (groupBy) {
case "state": case "state":
return this.rootStore?.states || []; return Object.keys(this.rootStore?.stateMap || {});
case "state_detail.group": case "state_detail.group":
return Object.keys(STATE_GROUPS); return Object.keys(STATE_GROUPS);
case "priority": case "priority":
return ISSUE_PRIORITIES.map((i) => i.key); return ISSUE_PRIORITIES.map((i) => i.key);
case "labels": case "labels":
return this.rootStore?.labels || []; return Object.keys(this.rootStore?.labelMap || {});
case "created_by": case "created_by":
return this.rootStore?.members || []; return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {});
case "assignees": case "assignees":
return this.rootStore?.members || []; return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {});
case "project": case "project":
return this.rootStore?.projects || []; return Object.keys(this.rootStore?.projectMap || {});
default: default:
return []; 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<TIssueOrderByOptions>): TIssue[] => { issuesSortWithOrderBy = (issueObject: TIssueMap, key: Partial<TIssueOrderByOptions>): TIssue[] => {
let array = values(issueObject); let array = values(issueObject);
array = reverse(sortBy(array, "created_at")); array = orderBy(array, "created_at");
switch (key) { switch (key) {
case "sort_order": case "sort_order":
return sortBy(array, "sort_order"); return orderBy(array, "sort_order");
case "state__name": case "state__name":
return reverse(sortBy(array, "state")); return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]));
case "-state__name": case "-state__name":
return sortBy(array, "state"); return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]), ["desc"]);
// dates // dates
case "created_at": case "created_at":
return sortBy(array, "created_at"); return orderBy(array, "created_at");
case "-created_at": case "-created_at":
return reverse(sortBy(array, "created_at")); return orderBy(array, "created_at", ["desc"]);
case "updated_at": case "updated_at":
return sortBy(array, "updated_at"); return orderBy(array, "updated_at");
case "-updated_at": case "-updated_at":
return reverse(sortBy(array, "updated_at")); return orderBy(array, "updated_at", ["desc"]);
case "start_date": 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": 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": 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": 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 // custom
case "priority": { case "priority": {
const sortArray = ISSUE_PRIORITIES.map((i) => i.key); 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": { case "-priority": {
const sortArray = ISSUE_PRIORITIES.map((i) => i.key); 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 // number
case "attachment_count": case "attachment_count":
return sortBy(array, "attachment_count"); return orderBy(array, "attachment_count");
case "-attachment_count": case "-attachment_count":
return reverse(sortBy(array, "attachment_count")); return orderBy(array, "attachment_count", ["desc"]);
case "estimate_point": 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": 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": case "link_count":
return sortBy(array, "link_count"); return orderBy(array, "link_count");
case "-link_count": case "-link_count":
return reverse(sortBy(array, "link_count")); return orderBy(array, "link_count", ["desc"]);
case "sub_issues_count": case "sub_issues_count":
return sortBy(array, "sub_issues_count"); return orderBy(array, "sub_issues_count");
case "-sub_issues_count": case "-sub_issues_count":
return reverse(sortBy(array, "sub_issues_count")); return orderBy(array, "sub_issues_count", ["desc"]);
// Array // Array
case "labels__name": 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": 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": 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": 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: default:
return array; return array;

View File

@ -4,7 +4,7 @@ import isEmpty from "lodash/isEmpty";
import { RootStore } from "../root.store"; import { RootStore } from "../root.store";
import { IStateStore, StateStore } from "../state.store"; import { IStateStore, StateStore } from "../state.store";
// issues data store // issues data store
import { IState } from "@plane/types"; import { IIssueLabel, IProject, IState, IUserLite } from "@plane/types";
import { IIssueStore, IssueStore } from "./issue.store"; import { IIssueStore, IssueStore } from "./issue.store";
import { IIssueDetail, IssueDetail } from "./issue-details/root.store"; import { IIssueDetail, IssueDetail } from "./issue-details/root.store";
import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace"; import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace";
@ -22,6 +22,7 @@ import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedI
import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft"; import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft";
import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store"; import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store";
import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store"; import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store";
import { IWorkspaceMembership } from "store/member/workspace-member.store";
export interface IIssueRootStore { export interface IIssueRootStore {
currentUserId: string | undefined; currentUserId: string | undefined;
@ -32,11 +33,12 @@ export interface IIssueRootStore {
viewId: string | undefined; viewId: string | undefined;
globalViewId: string | undefined; // all issues view id globalViewId: string | undefined; // all issues view id
userId: string | undefined; // user profile detail Id userId: string | undefined; // user profile detail Id
states: string[] | undefined; stateMap: Record<string, IState> | undefined;
stateDetails: IState[] | undefined; stateDetails: IState[] | undefined;
labels: string[] | undefined; labelMap: Record<string, IIssueLabel> | undefined;
members: string[] | undefined; workSpaceMemberRolesMap: Record<string, IWorkspaceMembership> | undefined;
projects: string[] | undefined; memberMap: Record<string, IUserLite> | undefined;
projectMap: Record<string, IProject> | undefined;
rootStore: RootStore; rootStore: RootStore;
@ -83,11 +85,12 @@ export class IssueRootStore implements IIssueRootStore {
viewId: string | undefined = undefined; viewId: string | undefined = undefined;
globalViewId: string | undefined = undefined; globalViewId: string | undefined = undefined;
userId: string | undefined = undefined; userId: string | undefined = undefined;
states: string[] | undefined = undefined; stateMap: Record<string, IState> | undefined = undefined;
stateDetails: IState[] | undefined = undefined; stateDetails: IState[] | undefined = undefined;
labels: string[] | undefined = undefined; labelMap: Record<string, IIssueLabel> | undefined = undefined;
members: string[] | undefined = undefined; workSpaceMemberRolesMap: Record<string, IWorkspaceMembership> | undefined = undefined;
projects: string[] | undefined = undefined; memberMap: Record<string, IUserLite> | undefined = undefined;
projectMap: Record<string, IProject> | undefined = undefined;
rootStore: RootStore; rootStore: RootStore;
@ -133,11 +136,12 @@ export class IssueRootStore implements IIssueRootStore {
viewId: observable.ref, viewId: observable.ref,
userId: observable.ref, userId: observable.ref,
globalViewId: observable.ref, globalViewId: observable.ref,
states: observable, stateMap: observable,
stateDetails: observable, stateDetails: observable,
labels: observable, labelMap: observable,
members: observable, memberMap: observable,
projects: observable, workSpaceMemberRolesMap: observable,
projectMap: observable,
}); });
this.rootStore = rootStore; 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.viewId) this.viewId = rootStore.app.router.viewId;
if (rootStore.app.router.globalViewId) this.globalViewId = rootStore.app.router.globalViewId; if (rootStore.app.router.globalViewId) this.globalViewId = rootStore.app.router.globalViewId;
if (rootStore.app.router.userId) this.userId = rootStore.app.router.userId; 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?.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)) 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)) if (!isEmpty(rootStore?.projectRoot?.project?.projectMap))
this.projects = Object.keys(rootStore?.projectRoot?.project?.projectMap); this.projectMap = rootStore?.projectRoot?.project?.projectMap;
}); });
this.issues = new IssueStore(); this.issues = new IssueStore();

View File

@ -26,6 +26,7 @@ export interface IWorkspaceMemberStore {
// computed // computed
workspaceMemberIds: string[] | null; workspaceMemberIds: string[] | null;
workspaceMemberInvitationIds: string[] | null; workspaceMemberInvitationIds: string[] | null;
memberMap: Record<string, IWorkspaceMembership> | null;
// computed actions // computed actions
getSearchedWorkspaceMemberIds: (searchQuery: string) => string[] | null; getSearchedWorkspaceMemberIds: (searchQuery: string) => string[] | null;
getSearchedWorkspaceInvitationIds: (searchQuery: string) => string[] | null; getSearchedWorkspaceInvitationIds: (searchQuery: string) => string[] | null;
@ -68,6 +69,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
// computed // computed
workspaceMemberIds: computed, workspaceMemberIds: computed,
workspaceMemberInvitationIds: computed, workspaceMemberInvitationIds: computed,
memberMap: computed,
// actions // actions
fetchWorkspaceMembers: action, fetchWorkspaceMembers: action,
updateMember: action, updateMember: action,
@ -100,6 +102,12 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
return memberIds; return memberIds;
} }
get memberMap() {
const workspaceSlug = this.routerStore.workspaceSlug;
if (!workspaceSlug) return null;
return this.workspaceMemberMap?.[workspaceSlug] ?? {};
}
get workspaceMemberInvitationIds() { get workspaceMemberInvitationIds() {
const workspaceSlug = this.routerStore.workspaceSlug; const workspaceSlug = this.routerStore.workspaceSlug;
if (!workspaceSlug) return null; if (!workspaceSlug) return null;

View File

@ -250,6 +250,7 @@ export class UserRootStore implements IUserRootStore {
this.isUserLoggedIn = false; this.isUserLoggedIn = false;
}); });
this.membership = new UserMembershipStore(this.rootStore); this.membership = new UserMembershipStore(this.rootStore);
this.rootStore.eventTracker.resetSession();
this.rootStore.resetOnSignout(); this.rootStore.resetOnSignout();
}); });
@ -264,6 +265,7 @@ export class UserRootStore implements IUserRootStore {
this.isUserLoggedIn = false; this.isUserLoggedIn = false;
}); });
this.membership = new UserMembershipStore(this.rootStore); this.membership = new UserMembershipStore(this.rootStore);
this.rootStore.eventTracker.resetSession();
this.rootStore.resetOnSignout(); this.rootStore.resetOnSignout();
}); });
} }