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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { Eye, EyeOff, XCircle } from "lucide-react";
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
import { useApplication } from "hooks/store";
import { useApplication, useEventTracker } from "hooks/store";
// components
import { ESignInSteps, ForgotPasswordPopover } from "components/account";
// ui
@ -16,6 +16,8 @@ import { Button, Input } from "@plane/ui";
import { checkEmailValidity } from "helpers/string.helper";
// types
import { IPasswordSignInData } from "@plane/types";
// constants
import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker";
type Props = {
email: string;
@ -46,6 +48,7 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
const {
config: { envConfig },
} = useApplication();
const { captureEvent } = useEventTracker();
// derived values
const isSmtpConfigured = envConfig?.is_smtp_configured;
// form info
@ -72,7 +75,13 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
await authService
.passwordSignIn(payload)
.then(async () => await onSubmit())
.then(async () => {
captureEvent(SIGN_IN_WITH_PASSWORD, {
state: "SUCCESS",
first_time: false,
});
await onSubmit();
})
.catch((err) =>
setToastAlert({
type: "error",
@ -182,9 +191,10 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
</div>
)}
/>
<div className="w-full text-right mt-2 pb-3">
<div className="mt-2 w-full pb-3 text-right">
{isSmtpConfigured ? (
<Link
onClick={() => captureEvent(FORGOT_PASSWORD)}
href={`/accounts/forgot-password?email=${email}`}
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 { observer } from "mobx-react-lite";
// hooks
import { useApplication } from "hooks/store";
import { useApplication, useEventTracker } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection";
// components
import { LatestFeatureBlock } from "components/common";
@ -13,6 +13,8 @@ import {
OAuthOptions,
SignInOptionalSetPasswordForm,
} from "components/account";
// constants
import { NAVIGATE_TO_SIGNUP } from "constants/event-tracker";
export enum ESignInSteps {
EMAIL = "EMAIL",
@ -32,6 +34,7 @@ export const SignInRoot = observer(() => {
const {
config: { envConfig },
} = useApplication();
const { captureEvent } = useEventTracker();
// derived values
const isSmtpConfigured = envConfig?.is_smtp_configured;
@ -110,7 +113,11 @@ export const SignInRoot = observer(() => {
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_in" />
<p className="text-xs text-onboarding-text-300 text-center mt-6">
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
</Link>
</p>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace";
// types
import { TCycleGroups } from "@plane/types";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker";
type TCyclesListItem = {
cycleId: string;
@ -37,7 +38,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
// router
const router = useRouter();
// store hooks
const { setTrackElement } = useEventTracker();
const { setTrackElement, captureEvent } = useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
@ -63,26 +64,42 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
captureEvent(CYCLE_FAVORITED, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
});
});
});
};
const handleRemoveFromFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
captureEvent(CYCLE_UNFAVORITED, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
});
});
});
};
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
@ -159,9 +176,9 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
projectId={projectId}
/>
<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="relative w-full flex items-center justify-between gap-3 overflow-hidden">
<div className="relative w-full flex items-center gap-3 overflow-hidden">
<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 flex w-full items-center justify-between gap-3 overflow-hidden">
<div className="relative flex w-full items-center gap-3 overflow-hidden">
<div className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}>
{isCompleted ? (
@ -181,20 +198,20 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
<div className="relative flex items-center gap-2.5 overflow-hidden">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
<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}
</span>
</Tooltip>
</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" />
</button>
</div>
{currentCycle && (
<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={{
color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`,
@ -206,12 +223,12 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</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">
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
</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`}>
<div className="flex w-10 cursor-default items-center justify-center">
{cycleDetails.assignees.length > 0 ? (

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { FileText, Plus } from "lucide-react";
// hooks
import { useApplication, useProject, useUser } from "hooks/store";
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// helpers
@ -25,6 +25,7 @@ export const PagesHeader = observer(() => {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const { setTrackElement } = useEventTracker();
const canUserCreatePage =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
@ -64,7 +65,15 @@ export const PagesHeader = observer(() => {
</div>
{canUserCreatePage && (
<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
</Button>
</div>

View File

@ -4,13 +4,18 @@ import { useTheme } from "next-themes";
// images
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
// hooks
import { useEventTracker } from "hooks/store";
// components
import { BreadcrumbLink } from "components/common";
import { Breadcrumbs } from "@plane/ui";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// constants
import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker";
export const WorkspaceDashboardHeader = () => {
// hooks
const { captureEvent } = useEventTracker();
const { resolvedTheme } = useTheme();
return (
@ -31,16 +36,26 @@ export const WorkspaceDashboardHeader = () => {
</div>
<div className="flex items-center gap-3 px-3">
<a
onClick={() =>
captureEvent(CHANGELOG_REDIRECTED, {
element: "navbar",
})
}
href="https://plane.so/changelog"
target="_blank"
rel="noopener noreferrer"
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))" />
<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
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"
target="_blank"
rel="noopener noreferrer"
@ -51,7 +66,7 @@ export const WorkspaceDashboardHeader = () => {
width={16}
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>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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">
<DateDropdown
value={issue.target_date}
minDate={issue.start_date ? new Date(issue.start_date) : undefined}
onChange={(data) => {
const targetDate = data ? renderFormattedPayloadDate(data) : null;
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">
<DateDropdown
value={issue.start_date}
maxDate={issue.target_date ? new Date(issue.target_date) : undefined}
onChange={(data) => {
const startDate = data ? renderFormattedPayloadDate(data) : null;
onChange(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,8 @@ import CyclesTour from "public/onboarding/cycles.webp";
import ModulesTour from "public/onboarding/modules.webp";
import ViewsTour from "public/onboarding/views.webp";
import PagesTour from "public/onboarding/pages.webp";
// constants
import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker";
type Props = {
onComplete: () => void;
@ -79,7 +81,7 @@ export const TourRoot: React.FC<Props> = observer((props) => {
const [step, setStep] = useState<TTourSteps>("welcome");
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const { setTrackElement } = useEventTracker();
const { setTrackElement, captureEvent } = useEventTracker();
const { currentUser } = useUser();
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
@ -103,13 +105,22 @@ export const TourRoot: React.FC<Props> = observer((props) => {
</p>
<div className="flex h-full items-end">
<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
</Button>
<button
type="button"
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
</button>
@ -156,8 +167,8 @@ export const TourRoot: React.FC<Props> = observer((props) => {
<Button
variant="primary"
onClick={() => {
setTrackElement("Product tour");
onComplete();
setTrackElement("Onboarding tour");
commandPaletteStore.toggleCreateProjectModal(true);
}}
>

View File

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

View File

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

View File

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

View File

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

View File

@ -4,12 +4,14 @@ import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react";
import { AlertTriangle } from "lucide-react";
// hooks
import { usePage } from "hooks/store";
import { useEventTracker, usePage } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { Button } from "@plane/ui";
// types
import { useProjectPages } from "hooks/store/use-project-page";
// constants
import { PAGE_DELETED } from "constants/event-tracker";
type TConfirmPageDeletionProps = {
pageId: string;
@ -27,6 +29,7 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
const { workspaceSlug, projectId } = router.query;
// store hooks
const { deletePage } = useProjectPages();
const { capturePageEvent } = useEventTracker();
const pageStore = usePage(pageId);
// 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
await deletePage(workspaceSlug.toString(), projectId as string, pageId)
.then(() => {
capturePageEvent({
eventName: PAGE_DELETED,
payload: {
...pageStore,
state: "SUCCESS",
},
});
handleClose();
setToastAlert({
type: "success",
@ -57,6 +67,13 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
});
})
.catch(() => {
capturePageEvent({
eventName: PAGE_DELETED,
payload: {
...pageStore,
state: "FAILED",
},
});
setToastAlert({
type: "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";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
import { PROJECT_CREATED } from "constants/event-tracker";
type Props = {
isOpen: boolean;
@ -134,13 +135,8 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
state: "SUCCESS",
};
captureProjectEvent({
eventName: "Project created",
eventName: PROJECT_CREATED,
payload: newPayload,
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: res.workspace,
},
});
setToastAlert({
type: "success",
@ -160,16 +156,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
message: err.data[key],
});
captureProjectEvent({
eventName: "Project created",
eventName: PROJECT_CREATED,
payload: {
...payload,
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";
// types
import type { IProject } from "@plane/types";
// constants
import { PROJECT_DELETED } from "constants/event-tracker";
type DeleteProjectModal = {
isOpen: boolean;
@ -62,13 +64,8 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
handleClose();
captureProjectEvent({
eventName: "Project deleted",
eventName: PROJECT_DELETED,
payload: { ...project, state: "SUCCESS", element: "Project general settings" },
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
});
setToastAlert({
type: "success",
@ -78,13 +75,8 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
})
.catch(() => {
captureProjectEvent({
eventName: "Project deleted",
eventName: PROJECT_DELETED,
payload: { ...project, state: "FAILED", element: "Project general settings" },
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
},
});
setToastAlert({
type: "error",

View File

@ -18,6 +18,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
import { NETWORK_CHOICES } from "constants/project";
// services
import { ProjectService } from "services/project";
import { PROJECT_UPDATED } from "constants/event-tracker";
export interface IProjectDetailsForm {
project: IProject;
@ -45,7 +46,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
setValue,
setError,
reset,
formState: { errors },
formState: { errors, dirtyFields },
} = useForm<IProject>({
defaultValues: {
...project,
@ -77,13 +78,15 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
return updateProject(workspaceSlug.toString(), project.id, payload)
.then((res) => {
const changed_properties = Object.keys(dirtyFields);
console.log(dirtyFields);
captureProjectEvent({
eventName: "Project updated",
payload: { ...res, state: "SUCCESS", element: "Project general settings" },
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: res.workspace,
eventName: PROJECT_UPDATED,
payload: {
...res,
changed_properties: changed_properties,
state: "SUCCESS",
element: "Project general settings",
},
});
setToastAlert({
@ -94,13 +97,8 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
})
.catch((error) => {
captureProjectEvent({
eventName: "Project updated",
eventName: PROJECT_UPDATED,
payload: { ...payload, state: "FAILED", element: "Project general settings" },
group: {
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id,
},
});
setToastAlert({
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" />
<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 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">

View File

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

View File

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

View File

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

View File

@ -51,12 +51,11 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { setTrackElement, captureEvent } = useEventTracker();
const { captureEvent } = useEventTracker();
const {
currentUser,
membership: { currentProjectRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
const { currentProjectDetails, updateProject } = useProject();
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
// toast alert
@ -91,14 +90,9 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
<ToggleSwitch
value={Boolean(currentProjectDetails?.[feature.property as keyof IProject])}
onChange={() => {
setTrackElement("PROJECT_SETTINGS_FEATURES_PAGE");
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],
element: "Project settings feature page",
});
handleSubmit({
[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";
// constants
import { GROUP_CHOICES } from "constants/project";
import { STATE_CREATED, STATE_UPDATED } from "constants/event-tracker";
type Props = {
data: IState | null;
@ -36,7 +37,7 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { captureEvent, setTrackElement } = useEventTracker();
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
const { createState, updateState } = useProjectState();
// toast alert
const { setToastAlert } = useToast();
@ -86,9 +87,13 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
title: "Success!",
message: "State created successfully.",
});
captureEvent("State created", {
...res,
state: "SUCCESS",
captureProjectStateEvent({
eventName: STATE_CREATED,
payload: {
...res,
state: "SUCCESS",
element: "Project settings states page",
},
});
})
.catch((error) => {
@ -104,8 +109,14 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
title: "Error!",
message: "State could not be created. Please try again.",
});
captureEvent("State created", {
state: "FAILED",
captureProjectStateEvent({
eventName: STATE_CREATED,
payload: {
...formData,
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)
.then((res) => {
handleClose();
captureEvent("State updated", {
...res,
state: "SUCCESS",
captureProjectStateEvent({
eventName: STATE_UPDATED,
payload: {
...res,
state: "SUCCESS",
element: "Project settings states page",
},
});
setToastAlert({
type: "success",
@ -139,8 +154,13 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
title: "Error!",
message: "State could not be updated. Please try again.",
});
captureEvent("State updated", {
state: "FAILED",
captureProjectStateEvent({
eventName: STATE_UPDATED,
payload: {
...formData,
state: "FAILED",
element: "Project settings states page",
},
});
});
};

View File

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

View File

@ -13,6 +13,7 @@ import { Button, CustomSelect, Input } from "@plane/ui";
import { IWorkspace } from "@plane/types";
// constants
import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace";
import { WORKSPACE_CREATED } from "constants/event-tracker";
type Props = {
onSubmit?: (res: IWorkspace) => Promise<void>;
@ -48,7 +49,7 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
// router
const router = useRouter();
// store hooks
const { captureEvent } = useEventTracker();
const { captureWorkspaceEvent } = useEventTracker();
const { createWorkspace } = useWorkspace();
// toast alert
const { setToastAlert } = useToast();
@ -70,9 +71,13 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
await createWorkspace(formData)
.then(async (res) => {
captureEvent("Workspace created", {
...res,
state: "SUCCESS",
captureWorkspaceEvent({
eventName: WORKSPACE_CREATED,
payload: {
...res,
state: "SUCCESS",
element: "Create workspace page",
},
});
setToastAlert({
type: "success",
@ -83,14 +88,18 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
if (onSubmit) await onSubmit(res);
})
.catch(() => {
captureWorkspaceEvent({
eventName: WORKSPACE_CREATED,
payload: {
state: "FAILED",
element: "Create workspace page",
},
});
setToastAlert({
type: "error",
title: "Error!",
message: "Workspace could not be created. Please try again.",
});
captureEvent("Workspace created", {
state: "FAILED",
});
});
} else setSlugError(true);
})
@ -100,9 +109,6 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
title: "Error!",
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";
// types
import type { IWorkspace } from "@plane/types";
// constants
import { WORKSPACE_DELETED } from "constants/event-tracker";
type Props = {
isOpen: boolean;
@ -28,7 +30,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
// router
const router = useRouter();
// store hooks
const { captureEvent } = useEventTracker();
const { captureWorkspaceEvent } = useEventTracker();
const { deleteWorkspace } = useWorkspace();
// toast alert
const { setToastAlert } = useToast();
@ -59,9 +61,13 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
.then((res) => {
handleClose();
router.push("/");
captureEvent("Workspace deleted", {
res,
state: "SUCCESS",
captureWorkspaceEvent({
eventName: WORKSPACE_DELETED,
payload: {
...data,
state: "SUCCESS",
element: "Workspace general settings page",
},
});
setToastAlert({
type: "success",
@ -75,8 +81,13 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
title: "Error!",
message: "Something went wrong. Please try again later.",
});
captureEvent("Workspace deleted", {
state: "FAILED",
captureWorkspaceEvent({
eventName: WORKSPACE_DELETED,
payload: {
...data,
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 { ChevronDown, Dot, XCircle } from "lucide-react";
// hooks
import { useMember, useUser } from "hooks/store";
import { useEventTracker, useMember, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { ConfirmWorkspaceMemberRemove } from "components/workspace";
@ -12,6 +12,7 @@ import { ConfirmWorkspaceMemberRemove } from "components/workspace";
import { CustomSelect, Tooltip } from "@plane/ui";
// constants
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
import { WORKSPACE_MEMBER_lEAVE } from "constants/event-tracker";
type Props = {
memberId: string;
@ -33,6 +34,7 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
const {
workspace: { updateMember, removeMemberFromWorkspace, getWorkspaceMemberDetails },
} = useMember();
const { captureEvent } = useEventTracker();
// toast alert
const { setToastAlert } = useToast();
// derived values
@ -42,7 +44,13 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
if (!workspaceSlug || !currentUserSettings) return;
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) =>
setToastAlert({
type: "error",

View File

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

View File

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

View File

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

View File

@ -4,11 +4,12 @@ import Link from "next/link";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// store hooks
import { useGlobalView, useUser } from "hooks/store";
import { useEventTracker, useGlobalView, useUser } from "hooks/store";
// components
import { CreateUpdateWorkspaceViewModal } from "components/workspace";
// constants
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
import { GLOBAL_VIEW_OPENED } from "constants/event-tracker";
const ViewTab = observer((props: { viewId: string }) => {
const { viewId } = props;
@ -49,11 +50,19 @@ export const GlobalViewsHeader: React.FC = observer(() => {
const {
membership: { currentWorkspaceRole },
} = useUser();
const { captureEvent } = useEventTracker();
// bring the active view to the centre of the header
useEffect(() => {
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()}`);
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 { Dialog, Transition } from "@headlessui/react";
// store hooks
import { useGlobalView } from "hooks/store";
import { useEventTracker, useGlobalView } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { WorkspaceViewForm } from "components/workspace";
// types
import { IWorkspaceView } from "@plane/types";
// constants
import { GLOBAL_VIEW_CREATED, GLOBAL_VIEW_UPDATED } from "constants/event-tracker";
type Props = {
data?: IWorkspaceView;
@ -24,6 +26,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
const { workspaceSlug } = router.query;
// store hooks
const { createGlobalView, updateGlobalView } = useGlobalView();
const { captureEvent } = useEventTracker();
// toast alert
const { setToastAlert } = useToast();
@ -43,6 +46,11 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
await createGlobalView(workspaceSlug.toString(), payloadData)
.then((res) => {
captureEvent(GLOBAL_VIEW_CREATED, {
view_id: res.id,
applied_filters: res.filters,
state: "SUCCESS",
});
setToastAlert({
type: "success",
title: "Success!",
@ -52,13 +60,17 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
router.push(`/${workspaceSlug}/workspace-views/${res.id}`);
handleClose();
})
.catch(() =>
.catch(() => {
captureEvent(GLOBAL_VIEW_CREATED, {
applied_filters: payload?.filters,
state: "FAILED",
});
setToastAlert({
type: "error",
title: "Error!",
message: "View could not be created. Please try again.",
})
);
});
});
};
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)
.then(() => {
.then((res) => {
captureEvent(GLOBAL_VIEW_UPDATED, {
view_id: res.id,
applied_filters: res.filters,
state: "SUCCESS",
});
setToastAlert({
type: "success",
title: "Success!",
@ -80,13 +97,18 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
});
handleClose();
})
.catch(() =>
.catch(() => {
captureEvent(GLOBAL_VIEW_UPDATED, {
view_id: data.id,
applied_filters: data.filters,
state: "FAILED",
});
setToastAlert({
type: "error",
title: "Error!",
message: "View could not be updated. Please try again.",
})
);
});
});
};
const handleFormSubmit = async (formData: Partial<IWorkspaceView>) => {

View File

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

View File

@ -2,26 +2,31 @@ export type IssueEventProps = {
eventName: string;
payload: any;
updates?: any;
group?: EventGroupProps;
path?: string;
};
export type EventProps = {
eventName: string;
payload: any;
group?: EventGroupProps;
};
export type EventGroupProps = {
isGrouping?: boolean;
groupType?: string;
groupId?: string;
};
export const getWorkspaceEventPayload = (payload: any) => ({
workspace_id: payload.id,
created_at: payload.created_at,
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) => ({
workspace_id: payload.workspace_id,
project_id: payload.id,
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,
updated_at: payload.updated_at,
state: payload.state,
@ -30,26 +35,43 @@ export const getProjectEventPayload = (payload: any) => ({
export const getCycleEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id,
project_id: payload.id,
project_id: payload.project,
cycle_id: payload.id,
created_at: payload.created_at,
updated_at: payload.updated_at,
start_date: payload.start_date,
target_date: payload.target_date,
cycle_status: payload.status,
changed_properties: payload.changed_properties,
state: payload.state,
element: payload.element,
});
export const getModuleEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id,
project_id: payload.id,
project_id: payload.project,
module_id: payload.id,
created_at: payload.created_at,
updated_at: payload.updated_at,
start_date: payload.start_date,
target_date: payload.target_date,
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,
element: payload.element,
});
@ -71,6 +93,7 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
sub_issues_count: payload.sub_issues_count,
parent_id: payload.parent_id,
project_id: payload.project_id,
workspace_id: payload.workspace_id,
priority: payload.priority,
state_id: payload.state_id,
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() : "",
};
if (eventName === "Issue updated") {
if (eventName === ISSUE_UPDATED) {
eventPayload = {
...eventPayload,
...updates,
@ -103,3 +126,99 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
}
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 { ThemeProvider } from "next-themes";
// hooks
import { useApplication, useUser } from "hooks/store";
import { useApplication, useUser, useWorkspace } from "hooks/store";
// constants
import { THEMES } from "constants/themes";
// layouts
@ -37,6 +37,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
currentUser,
membership: { currentProjectRole, currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
const {
config: { envConfig },
} = useApplication();
@ -49,6 +50,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
<CrispWrapper user={currentUser}>
<PostHogProvider
user={currentUser}
currentWorkspaceId= {currentWorkspace?.id}
workspaceRole={currentWorkspaceRole}
projectRole={currentProjectRole}
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 posthog from "posthog-js";
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";
// helpers
import { getUserRole } from "helpers/user.helper";
// constants
import { GROUP_WORKSPACE } from "constants/event-tracker";
export interface IPosthogWrapper {
children: ReactNode;
user: IUser | null;
currentWorkspaceId: string | undefined;
workspaceRole: number | undefined;
projectRole: number | undefined;
posthogAPIKey: string | null;
@ -17,7 +20,9 @@ export interface IPosthogWrapper {
}
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
const router = useRouter();
@ -25,10 +30,11 @@ const PostHogProvider: FC<IPosthogWrapper> = (props) => {
if (user) {
// Identify sends an event, so you want may want to limit how often you call it
posthog?.identify(user.email, {
email: user.email,
id: user.id,
first_name: user.first_name,
last_name: user.last_name,
id: user.id,
email: user.email,
use_case: user.use_case,
workspace_role: workspaceRole ? getUserRole(workspaceRole) : undefined,
project_role: projectRole ? getUserRole(projectRole) : undefined,
});
@ -45,6 +51,15 @@ const PostHogProvider: FC<IPosthogWrapper> = (props) => {
}
}, [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(() => {
// Track page views
const handleRouteChange = () => {

View File

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

View File

@ -6,7 +6,7 @@ import useSWR from "swr";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useUser } from "hooks/store";
import { useApplication, useEventTracker, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth";
// layouts
@ -60,6 +60,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
const {
commandPalette: { toggleCreatePageModal },
} = useApplication();
const { setTrackElement } = useEventTracker();
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
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."
primaryButton={{
text: "Create your first page",
onClick: () => toggleCreatePageModal(true),
onClick: () => {
setTrackElement("Pages empty state");
toggleCreatePageModal(true);
},
}}
comicBox={{
title: "A page can be a doc or a doc of docs.",

View File

@ -16,8 +16,11 @@ import { Button } from "@plane/ui";
// types
import { NextPageWithLayout } from "lib/types";
import { IWorkspaceBulkInviteFormData } from "@plane/types";
// helpers
import { getUserRole } from "helpers/user.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
import { MEMBER_INVITED } from "constants/event-tracker";
const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
// states
@ -43,7 +46,17 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
return inviteMembersToWorkspace(workspaceSlug.toString(), data)
.then(() => {
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({
type: "success",
title: "Success!",
@ -51,7 +64,17 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
});
})
.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({
type: "error",
title: "Error!",
@ -84,14 +107,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
/>
</div>
{hasAddMemberPermission && (
<Button
variant="primary"
size="sm"
onClick={() => {
setTrackElement("WORKSPACE_SETTINGS_MEMBERS_PAGE_HEADER");
setInviteModal(true);
}}
>
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
Add member
</Button>
)}

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import sortBy from "lodash/sortBy";
import orderBy from "lodash/orderBy";
import get from "lodash/get";
import indexOf from "lodash/indexOf";
import reverse from "lodash/reverse";
import isEmpty from "lodash/isEmpty";
import values from "lodash/values";
// types
import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
@ -144,98 +144,189 @@ export class IssueHelperStore implements TIssueHelperStore {
issueDisplayFiltersDefaultData = (groupBy: string | null): string[] => {
switch (groupBy) {
case "state":
return this.rootStore?.states || [];
return Object.keys(this.rootStore?.stateMap || {});
case "state_detail.group":
return Object.keys(STATE_GROUPS);
case "priority":
return ISSUE_PRIORITIES.map((i) => i.key);
case "labels":
return this.rootStore?.labels || [];
return Object.keys(this.rootStore?.labelMap || {});
case "created_by":
return this.rootStore?.members || [];
return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {});
case "assignees":
return this.rootStore?.members || [];
return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {});
case "project":
return this.rootStore?.projects || [];
return Object.keys(this.rootStore?.projectMap || {});
default:
return [];
}
};
/**
* This Method is used to get data of the issue based on the ids of the data for states, labels adn assignees
* @param dataType what type of data is being sent
* @param dataIds id/ids of the data that is to be populated
* @param order ascending or descending for arrays of data
* @returns string | string[] of sortable fields to be used for sorting
*/
populateIssueDataForSorting(
dataType: "state_id" | "label_ids" | "assignee_ids",
dataIds: string | string[] | null | undefined,
order?: "asc" | "desc"
) {
if (!dataIds) return;
const dataValues: string[] = [];
const isDataIdsArray = Array.isArray(dataIds);
const dataIdsArray = isDataIdsArray ? dataIds : [dataIds];
switch (dataType) {
case "state_id":
const stateMap = this.rootStore?.stateMap;
if (!stateMap) break;
for (const dataId of dataIdsArray) {
const state = stateMap[dataId];
if (state && state.name) dataValues.push(state.name.toLocaleLowerCase());
}
break;
case "label_ids":
const labelMap = this.rootStore?.labelMap;
if (!labelMap) break;
for (const dataId of dataIdsArray) {
const label = labelMap[dataId];
if (label && label.name) dataValues.push(label.name.toLocaleLowerCase());
}
break;
case "assignee_ids":
const memberMap = this.rootStore?.memberMap;
if (!memberMap) break;
for (const dataId of dataIdsArray) {
const member = memberMap[dataId];
if (memberMap && member.first_name) dataValues.push(member.first_name.toLocaleLowerCase());
}
break;
}
return isDataIdsArray ? (order ? orderBy(dataValues, undefined, [order]) : dataValues) : dataValues[0];
}
/**
* This Method is mainly used to filter out empty values in the begining
* @param key key of the value that is to be checked if empty
* @param object any object in which the key's value is to be checked
* @returns 1 if emoty, 0 if not empty
*/
getSortOrderToFilterEmptyValues(key: string, object: any) {
const value = object?.[key];
if (typeof value !== "number" && isEmpty(value)) return 1;
return 0;
}
issuesSortWithOrderBy = (issueObject: TIssueMap, key: Partial<TIssueOrderByOptions>): TIssue[] => {
let array = values(issueObject);
array = reverse(sortBy(array, "created_at"));
array = orderBy(array, "created_at");
switch (key) {
case "sort_order":
return sortBy(array, "sort_order");
return orderBy(array, "sort_order");
case "state__name":
return reverse(sortBy(array, "state"));
return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]));
case "-state__name":
return sortBy(array, "state");
return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]), ["desc"]);
// dates
case "created_at":
return sortBy(array, "created_at");
return orderBy(array, "created_at");
case "-created_at":
return reverse(sortBy(array, "created_at"));
return orderBy(array, "created_at", ["desc"]);
case "updated_at":
return sortBy(array, "updated_at");
return orderBy(array, "updated_at");
case "-updated_at":
return reverse(sortBy(array, "updated_at"));
return orderBy(array, "updated_at", ["desc"]);
case "start_date":
return sortBy(array, "start_date");
return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"]); //preferring sorting based on empty values to always keep the empty values below
case "-start_date":
return reverse(sortBy(array, "start_date"));
return orderBy(
array,
[this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"], //preferring sorting based on empty values to always keep the empty values below
["asc", "desc"]
);
case "target_date":
return sortBy(array, "target_date");
return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"]); //preferring sorting based on empty values to always keep the empty values below
case "-target_date":
return reverse(sortBy(array, "target_date"));
return orderBy(
array,
[this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"], //preferring sorting based on empty values to always keep the empty values below
["asc", "desc"]
);
// custom
case "priority": {
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
return reverse(sortBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority)));
return orderBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority), ["desc"]);
}
case "-priority": {
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
return sortBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority));
return orderBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority));
}
// number
case "attachment_count":
return sortBy(array, "attachment_count");
return orderBy(array, "attachment_count");
case "-attachment_count":
return reverse(sortBy(array, "attachment_count"));
return orderBy(array, "attachment_count", ["desc"]);
case "estimate_point":
return sortBy(array, "estimate_point");
return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"]); //preferring sorting based on empty values to always keep the empty values below
case "-estimate_point":
return reverse(sortBy(array, "estimate_point"));
return orderBy(
array,
[this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"], //preferring sorting based on empty values to always keep the empty values below
["asc", "desc"]
);
case "link_count":
return sortBy(array, "link_count");
return orderBy(array, "link_count");
case "-link_count":
return reverse(sortBy(array, "link_count"));
return orderBy(array, "link_count", ["desc"]);
case "sub_issues_count":
return sortBy(array, "sub_issues_count");
return orderBy(array, "sub_issues_count");
case "-sub_issues_count":
return reverse(sortBy(array, "sub_issues_count"));
return orderBy(array, "sub_issues_count", ["desc"]);
// Array
case "labels__name":
return reverse(sortBy(array, "labels"));
return orderBy(array, [
this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "asc"),
]);
case "-labels__name":
return sortBy(array, "labels");
return orderBy(
array,
[
this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "desc"),
],
["asc", "desc"]
);
case "assignees__first_name":
return reverse(sortBy(array, "assignees"));
return orderBy(array, [
this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "asc"),
]);
case "-assignees__first_name":
return sortBy(array, "assignees");
return orderBy(
array,
[
this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "desc"),
],
["asc", "desc"]
);
default:
return array;

View File

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

View File

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

View File

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