mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: merge conflicts resolved
This commit is contained in:
commit
b86c6c906a
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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 (
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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) => {
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
|
@ -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: "",
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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 ? (
|
||||
|
@ -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" },
|
||||
});
|
||||
});
|
||||
|
@ -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 && (
|
||||
|
@ -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");
|
||||
|
@ -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!",
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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!,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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: {
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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!,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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" },
|
||||
});
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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(() => {
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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>) => {
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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)}
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -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 = () => {
|
||||
|
@ -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);
|
||||
}}
|
||||
>
|
||||
|
@ -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;
|
||||
|
@ -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 () => {
|
||||
|
@ -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);
|
||||
},
|
||||
}}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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!",
|
||||
|
@ -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!,
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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">
|
||||
|
@ -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 {
|
||||
|
@ -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) =>
|
||||
|
@ -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);
|
||||
|
@ -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],
|
||||
|
@ -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",
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -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(() => {
|
||||
|
@ -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",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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" });
|
||||
|
@ -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>) => {
|
||||
|
@ -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);
|
||||
}}
|
||||
>
|
||||
|
@ -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";
|
||||
|
@ -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}
|
||||
|
@ -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 = () => {
|
||||
|
@ -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",
|
||||
|
@ -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 project’s context. To make short work of any doc, invoke Galileo, Plane’s 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.",
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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!",
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user