mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: merge conflicts resolved from develop
This commit is contained in:
commit
e000e7eedd
@ -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-primary-text-subtle"
|
||||
>
|
||||
|
@ -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-primary-text-subtle font-medium underline">
|
||||
<Link
|
||||
href="/accounts/sign-up"
|
||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
|
||||
className="text-primary-text-subtle 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-primary-text-subtle font-medium underline">
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
|
||||
className="text-primary-text-subtle 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-neutral-border-subtle bg-neutral-component-surface-light px-5 py-6 text-sm hover:bg-neutral-component-surface-medium">
|
||||
<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-neutral-border-subtle bg-neutral-component-surface-light px-5 py-6 text-sm hover:bg-neutral-component-surface-medium 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-neutral-text-subtle" />
|
||||
</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-neutral-text-medium">
|
||||
{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 ? (
|
||||
@ -221,7 +238,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
))}
|
||||
</AvatarGroup>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-neutral-component-surface-dark">
|
||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-neutral-border-strong bg-neutral-component-surface-dark">
|
||||
<User2 className="h-4 w-4 text-neutral-text-subtle" />
|
||||
</span>
|
||||
)}
|
||||
|
@ -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,18 +4,23 @@ import { useTheme } from "next-themes";
|
||||
// images
|
||||
import githubBlackImage from "/public/logos/github-black.png";
|
||||
import githubWhiteImage from "/public/logos/github-white.png";
|
||||
// hooks
|
||||
import { useEventTracker } from "hooks/store";
|
||||
// components
|
||||
import { BreadcrumbLink } from "components/common";
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
// constants
|
||||
import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker";
|
||||
|
||||
export const WorkspaceDashboardHeader = () => {
|
||||
// hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative z-[15] flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-neutral-border-medium bg-custom-sidebar-background-100 p-4">
|
||||
<div className="relative z-[15] flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-neutral-border-medium bg-sidebar-neutral-component-surface-light p-4">
|
||||
<div className="flex items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<SidebarHamburgerToggle />
|
||||
<div>
|
||||
@ -34,16 +39,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-neutral-component-surface-dark px-3 py-1.5"
|
||||
>
|
||||
<Zap size={14} strokeWidth={2} fill="var(--color-neutral-120)" />
|
||||
<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-neutral-component-surface-dark px-3 py-1.5 "
|
||||
onClick={() =>
|
||||
captureEvent(GITHUB_REDIRECTED, {
|
||||
element: "navbar",
|
||||
})
|
||||
}
|
||||
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-neutral-component-surface-dark px-3 py-1.5"
|
||||
href="https://github.com/makeplane/plane"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@ -54,7 +69,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-neutral-border-medium">
|
||||
<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-neutral-border-medium">
|
||||
<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 from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { ArchiveRestore, Clock, MessageSquare, User2 } from "lucide-react";
|
||||
import 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,9 +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 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;
|
||||
@ -28,6 +33,7 @@ type NotificationCardProps = {
|
||||
|
||||
export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
const {
|
||||
selectedTab,
|
||||
notification,
|
||||
isSnoozedTabOpen,
|
||||
closePopover,
|
||||
@ -37,11 +43,78 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
setSelectedNotificationForSnooze,
|
||||
markSnoozeNotification,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
// states
|
||||
const [showSnoozeOptions, setshowSnoozeOptions] = React.useState(false);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// refs
|
||||
const snoozeRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const moreOptions = [
|
||||
{
|
||||
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(() => {
|
||||
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(() => {
|
||||
setToastAlert({
|
||||
title: notification.archived_at ? "Notification un-archived" : "Notification archived",
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const snoozeOptionOnClick = (date: Date | null) => {
|
||||
if (!date) {
|
||||
setSelectedNotificationForSnooze(notification.id);
|
||||
return;
|
||||
}
|
||||
markSnoozeNotification(notification.id, date).then(() => {
|
||||
setToastAlert({
|
||||
title: `Notification snoozed till ${renderFormattedDate(date)}`,
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// close snooze options on outside click
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: any) => {
|
||||
if (snoozeRef.current && !snoozeRef.current?.contains(event.target)) {
|
||||
setshowSnoozeOptions(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside, true);
|
||||
document.addEventListener("touchend", handleClickOutside, true);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside, true);
|
||||
document.removeEventListener("touchend", handleClickOutside, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
|
||||
|
||||
@ -49,6 +122,10 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
<Link
|
||||
onClick={() => {
|
||||
markNotificationReadStatus(notification.id);
|
||||
captureEvent(ISSUE_OPENED, {
|
||||
issue_id: notification.data.issue.id,
|
||||
element: "notification",
|
||||
});
|
||||
closePopover();
|
||||
}}
|
||||
href={`/${workspaceSlug}/projects/${notification.project}/${
|
||||
@ -87,57 +164,136 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full space-y-2.5 overflow-hidden">
|
||||
{!notification.message ? (
|
||||
<div className="w-full break-words text-sm">
|
||||
<span className="font-semibold">
|
||||
{notification.triggered_by_details.is_bot
|
||||
? notification.triggered_by_details.first_name
|
||||
: notification.triggered_by_details.display_name}{" "}
|
||||
</span>
|
||||
{notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.verb}{" "}
|
||||
{notification.data.issue_activity.field === "comment"
|
||||
? "commented"
|
||||
: notification.data.issue_activity.field === "None"
|
||||
? null
|
||||
: replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
|
||||
{notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.field !== "None"
|
||||
? "to"
|
||||
: ""}
|
||||
<span className="font-semibold">
|
||||
{" "}
|
||||
{notification.data.issue_activity.field !== "None" ? (
|
||||
notification.data.issue_activity.field !== "comment" ? (
|
||||
notification.data.issue_activity.field === "target_date" ? (
|
||||
renderFormattedDate(notification.data.issue_activity.new_value)
|
||||
) : notification.data.issue_activity.field === "attachment" ? (
|
||||
"the issue"
|
||||
) : notification.data.issue_activity.field === "description" ? (
|
||||
stripAndTruncateHTML(notification.data.issue_activity.new_value, 55)
|
||||
<div className="flex items-start">
|
||||
{!notification.message ? (
|
||||
<div className="w-full break-words text-sm">
|
||||
<span className="font-semibold">
|
||||
{notification.triggered_by_details.is_bot
|
||||
? notification.triggered_by_details.first_name
|
||||
: notification.triggered_by_details.display_name}{" "}
|
||||
</span>
|
||||
{notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.verb}{" "}
|
||||
{notification.data.issue_activity.field === "comment"
|
||||
? "commented"
|
||||
: notification.data.issue_activity.field === "None"
|
||||
? null
|
||||
: replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
|
||||
{notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.field !== "None"
|
||||
? "to"
|
||||
: ""}
|
||||
<span className="font-semibold">
|
||||
{" "}
|
||||
{notification.data.issue_activity.field !== "None" ? (
|
||||
notification.data.issue_activity.field !== "comment" ? (
|
||||
notification.data.issue_activity.field === "target_date" ? (
|
||||
renderFormattedDate(notification.data.issue_activity.new_value)
|
||||
) : notification.data.issue_activity.field === "attachment" ? (
|
||||
"the issue"
|
||||
) : notification.data.issue_activity.field === "description" ? (
|
||||
stripAndTruncateHTML(notification.data.issue_activity.new_value, 55)
|
||||
) : (
|
||||
notification.data.issue_activity.new_value
|
||||
)
|
||||
) : (
|
||||
notification.data.issue_activity.new_value
|
||||
<span>
|
||||
{`"`}
|
||||
{notification.data.issue_activity.new_value.length > 55
|
||||
? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
|
||||
: notification.data.issue_activity.issue_comment}
|
||||
{`"`}
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<span>
|
||||
{`"`}
|
||||
{notification.data.issue_activity.new_value.length > 55
|
||||
? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
|
||||
: notification.data.issue_activity.issue_comment}
|
||||
{`"`}
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
"the issue and assigned it to you."
|
||||
"the issue and assigned it to you."
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full break-words text-sm">
|
||||
<span className="semi-bold">{notification.message}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex md:hidden items-start">
|
||||
<Menu as="div" className={" w-min text-left"}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button as={React.Fragment}>
|
||||
<button
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex w-full items-center gap-x-2 rounded p-0.5 text-sm"
|
||||
>
|
||||
<MoreVertical className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
</button>
|
||||
</Menu.Button>
|
||||
{open && (
|
||||
<Menu.Items className={"absolute right-0 z-10"} static>
|
||||
<div
|
||||
className={
|
||||
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap"
|
||||
}
|
||||
>
|
||||
{moreOptions.map((item) => (
|
||||
<Menu.Item as="div">
|
||||
{({ close }) => (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
item.onClick();
|
||||
close();
|
||||
}}
|
||||
className="flex gap-x-2 items-center p-1.5"
|
||||
>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
<Menu.Item as="div">
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setshowSnoozeOptions(true);
|
||||
}}
|
||||
className="flex gap-x-2 items-center p-1.5"
|
||||
>
|
||||
<Clock className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
Snooze
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</Menu>
|
||||
{showSnoozeOptions && (
|
||||
<div
|
||||
ref={snoozeRef}
|
||||
className="absolute right-36 z-20 my-1 top-24 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap"
|
||||
>
|
||||
{snoozeOptions.map((item) => (
|
||||
<p
|
||||
className="p-1.5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setshowSnoozeOptions(false);
|
||||
snoozeOptionOnClick(item.value);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full break-words text-sm">
|
||||
<span className="semi-bold">{notification.message}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-2 text-xs">
|
||||
<p className="text-neutral-text-medium">
|
||||
<p className="text-neutral-text-medium line-clamp-1">
|
||||
{truncateText(
|
||||
`${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
|
||||
50
|
||||
@ -152,7 +308,9 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="flex-shrink-0 text-neutral-text-medium">{calculateTimeAgo(notification.created_at)}</p>
|
||||
<p className="flex-shrink-0 text-neutral-text-medium mt-auto">
|
||||
{calculateTimeAgo(notification.created_at)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -164,6 +322,11 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
icon: <MessageSquare className="h-3.5 w-3.5 text-neutral-text-medium" />,
|
||||
onClick: () => {
|
||||
markNotificationReadStatusToggle(notification.id).then(() => {
|
||||
captureEvent(NOTIFICATIONS_READ, {
|
||||
issue_id: notification.data.issue.id,
|
||||
tab: selectedTab,
|
||||
state: "SUCCESS",
|
||||
});
|
||||
setToastAlert({
|
||||
title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
|
||||
type: "success",
|
||||
@ -181,6 +344,11 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
),
|
||||
onClick: () => {
|
||||
markNotificationArchivedStatus(notification.id).then(() => {
|
||||
captureEvent(NOTIFICATION_ARCHIVED, {
|
||||
issue_id: notification.data.issue.id,
|
||||
tab: selectedTab,
|
||||
state: "SUCCESS",
|
||||
});
|
||||
setToastAlert({
|
||||
title: notification.archived_at ? "Notification un-archived" : "Notification archived",
|
||||
type: "success",
|
||||
@ -195,7 +363,6 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
item.onClick();
|
||||
}}
|
||||
key={item.id}
|
||||
@ -228,6 +395,11 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
}
|
||||
|
||||
markSnoozeNotification(notification.id, item.value).then(() => {
|
||||
captureEvent(NOTIFICATION_SNOOZED, {
|
||||
issue_id: notification.data.issue.id,
|
||||
tab: selectedTab,
|
||||
state: "SUCCESS",
|
||||
});
|
||||
setToastAlert({
|
||||
title: `Notification snoozed till ${renderFormattedDate(item.value)}`,
|
||||
type: "success",
|
||||
|
@ -1,11 +1,22 @@
|
||||
import React from "react";
|
||||
import { ArrowLeft, CheckCheck, Clock, ListFilter, MoreVertical, RefreshCw, X } from "lucide-react";
|
||||
// components
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
// ui
|
||||
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { useEventTracker } from "hooks/store";
|
||||
// helpers
|
||||
import { getNumberCount } from "helpers/string.helper";
|
||||
// type
|
||||
import type { NotificationType, NotificationCount } from "@plane/types";
|
||||
// constants
|
||||
import {
|
||||
ARCHIVED_NOTIFICATIONS,
|
||||
NOTIFICATIONS_READ,
|
||||
SNOOZED_NOTIFICATIONS,
|
||||
UNREAD_NOTIFICATIONS,
|
||||
} from "constants/event-tracker";
|
||||
|
||||
type NotificationHeaderProps = {
|
||||
notificationCount?: NotificationCount | null;
|
||||
@ -39,6 +50,8 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
||||
setSelectedTab,
|
||||
markAllNotificationsAsRead,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
|
||||
const notificationTabs: Array<{
|
||||
label: string;
|
||||
@ -65,7 +78,11 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-5 pt-5">
|
||||
<h2 className="mb-2 text-xl font-semibold">Notifications</h2>
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<SidebarHamburgerToggle />
|
||||
<h2 className="md:text-xl md:font-semibold">Notifications</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-x-4 text-neutral-text-medium">
|
||||
<Tooltip tooltipContent="Refresh">
|
||||
<button
|
||||
@ -84,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" />
|
||||
@ -97,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
|
||||
@ -108,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">
|
||||
@ -120,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,11 +153,13 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
<Tooltip tooltipContent="Close">
|
||||
<button type="button" onClick={() => closePopover()}>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className="hidden md:block">
|
||||
<Tooltip tooltipContent="Close">
|
||||
<button type="button" onClick={() => closePopover()}>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 w-full border-b border-neutral-border-medium px-5">
|
||||
@ -165,7 +192,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
||||
onClick={() => setSelectedTab(tab.value)}
|
||||
className={`whitespace-nowrap border-b-2 px-1 pb-4 text-sm font-medium outline-none ${
|
||||
tab.value === selectedTab
|
||||
? "border-custom-primary-100 text-primary-text-subtle"
|
||||
? "border-primary-border-subtle text-primary-text-subtle"
|
||||
: "border-transparent text-neutral-text-medium"
|
||||
}`}
|
||||
>
|
||||
@ -174,7 +201,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
||||
<span
|
||||
className={`ml-2 rounded-full px-2 py-0.5 text-xs ${
|
||||
tab.value === selectedTab
|
||||
? "bg-custom-primary-100 text-white"
|
||||
? "bg-primary-solid text-white"
|
||||
: "bg-neutral-component-surface-dark text-neutral-text-medium"
|
||||
}`}
|
||||
>
|
||||
|
@ -5,6 +5,7 @@ import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useApplication } from "hooks/store";
|
||||
import useUserNotification from "hooks/use-user-notifications";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
|
||||
@ -16,8 +17,12 @@ import { getNumberCount } from "helpers/string.helper";
|
||||
import { cn } from "helpers/common.helper";
|
||||
|
||||
export const NotificationPopover = observer(() => {
|
||||
// states
|
||||
const [isActive, setIsActive] = React.useState(false);
|
||||
// store hooks
|
||||
const { theme: themeStore } = useApplication();
|
||||
// refs
|
||||
const notificationPopoverRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const {
|
||||
notifications,
|
||||
@ -45,8 +50,11 @@ export const NotificationPopover = observer(() => {
|
||||
setFetchNotifications,
|
||||
markAllNotificationsAsRead,
|
||||
} = useUserNotification();
|
||||
|
||||
const isSidebarCollapsed = themeStore.sidebarCollapsed;
|
||||
useOutsideClickDetector(notificationPopoverRef, () => {
|
||||
// if snooze modal is open, then don't close the popover
|
||||
if (selectedNotificationForSnooze === null) setIsActive(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -55,144 +63,143 @@ export const NotificationPopover = observer(() => {
|
||||
onClose={() => setSelectedNotificationForSnooze(null)}
|
||||
onSubmit={markSnoozeNotification}
|
||||
notification={notifications?.find((notification) => notification.id === selectedNotificationForSnooze) || null}
|
||||
onSuccess={() => {
|
||||
setSelectedNotificationForSnooze(null);
|
||||
}}
|
||||
onSuccess={() => setSelectedNotificationForSnooze(null)}
|
||||
/>
|
||||
<Popover className="relative w-full">
|
||||
{({ open: isActive, close: closePopover }) => {
|
||||
if (isActive) setFetchNotifications(true);
|
||||
<Popover ref={notificationPopoverRef} className="md:relative w-full">
|
||||
<>
|
||||
<Tooltip tooltipContent="Notifications" position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
||||
<button
|
||||
className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||
isActive
|
||||
? "bg-primary-component-surface-light text-primary-text-subtle"
|
||||
: "text-sidebar-neutral-text-medium hover:bg-sidebar-neutral-component-surface-dark"
|
||||
} ${isSidebarCollapsed ? "justify-center" : ""}`}
|
||||
onClick={() => {
|
||||
if (window.innerWidth < 768) themeStore.toggleSidebar();
|
||||
if (!isActive) setFetchNotifications(true);
|
||||
setIsActive(!isActive);
|
||||
}}
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
{isSidebarCollapsed ? null : <span>Notifications</span>}
|
||||
{totalNotificationCount && totalNotificationCount > 0 ? (
|
||||
isSidebarCollapsed ? (
|
||||
<span className="absolute right-3.5 top-2 h-2 w-2 rounded-full bg-primary-solid" />
|
||||
) : (
|
||||
<span className="ml-auto rounded-full bg-primary-solid px-1.5 text-xs text-white">
|
||||
{getNumberCount(totalNotificationCount)}
|
||||
</span>
|
||||
)
|
||||
) : null}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Transition
|
||||
show={isActive}
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel
|
||||
className="absolute top-0 left-[280px] md:-top-36 md:ml-8 md:h-[50vh] z-10 flex h-full w-[100vw] flex-col rounded-xl md:border border-neutral-border-medium bg-neutral-component-surface-light shadow-lg md:left-full md:w-[36rem]"
|
||||
static
|
||||
>
|
||||
<NotificationHeader
|
||||
notificationCount={notificationCount}
|
||||
notificationMutate={notificationMutate}
|
||||
closePopover={() => setIsActive(false)}
|
||||
isRefreshing={isRefreshing}
|
||||
snoozed={snoozed}
|
||||
archived={archived}
|
||||
readNotification={readNotification}
|
||||
selectedTab={selectedTab}
|
||||
setSnoozed={setSnoozed}
|
||||
setArchived={setArchived}
|
||||
setReadNotification={setReadNotification}
|
||||
setSelectedTab={setSelectedTab}
|
||||
markAllNotificationsAsRead={markAllNotificationsAsRead}
|
||||
/>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip tooltipContent="Notifications" position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
||||
<Popover.Button
|
||||
className={cn(
|
||||
"group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none",
|
||||
{
|
||||
"bg-primary-component-surface-light text-primary-text-subtle": isActive,
|
||||
"text-sidebar-neutral-text-medium hover:bg-sidebar-neutral-component-surface-dark": !isActive,
|
||||
"justify-center": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
{isSidebarCollapsed ? null : <span>Notifications</span>}
|
||||
{totalNotificationCount && totalNotificationCount > 0 ? (
|
||||
isSidebarCollapsed ? (
|
||||
<span className="absolute right-3.5 top-2 h-2 w-2 rounded-full bg-primary-component-surface-medium" />
|
||||
) : (
|
||||
<span className="ml-auto rounded-full bg-primary-component-surface-medium px-1.5 text-xs text-white">
|
||||
{getNumberCount(totalNotificationCount)}
|
||||
</span>
|
||||
)
|
||||
) : null}
|
||||
</Popover.Button>
|
||||
</Tooltip>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute -top-36 left-0 z-10 ml-8 flex h-[50vh] w-[20rem] flex-col rounded-xl border border-neutral-border-medium bg-neutral-component-surface-light shadow-lg md:left-full md:w-[36rem]">
|
||||
<NotificationHeader
|
||||
notificationCount={notificationCount}
|
||||
notificationMutate={notificationMutate}
|
||||
closePopover={closePopover}
|
||||
isRefreshing={isRefreshing}
|
||||
snoozed={snoozed}
|
||||
archived={archived}
|
||||
readNotification={readNotification}
|
||||
selectedTab={selectedTab}
|
||||
setSnoozed={setSnoozed}
|
||||
setArchived={setArchived}
|
||||
setReadNotification={setReadNotification}
|
||||
setSelectedTab={setSelectedTab}
|
||||
markAllNotificationsAsRead={markAllNotificationsAsRead}
|
||||
/>
|
||||
|
||||
{notifications ? (
|
||||
notifications.length > 0 ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="divide-y divide-neutral-border-subtle">
|
||||
{notifications.map((notification) => (
|
||||
<NotificationCard
|
||||
key={notification.id}
|
||||
isSnoozedTabOpen={snoozed}
|
||||
closePopover={closePopover}
|
||||
notification={notification}
|
||||
markNotificationArchivedStatus={markNotificationArchivedStatus}
|
||||
markNotificationReadStatus={markNotificationAsRead}
|
||||
markNotificationReadStatusToggle={markNotificationReadStatus}
|
||||
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
|
||||
markSnoozeNotification={markSnoozeNotification}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{isLoadingMore && (
|
||||
<div className="my-6 flex items-center justify-center text-sm">
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="mr-2 h-6 w-6 animate-spin fill-blue-600 text-neutral-text-medium"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
<p>Loading notifications</p>
|
||||
</div>
|
||||
)}
|
||||
{hasMore && !isLoadingMore && (
|
||||
<button
|
||||
type="button"
|
||||
className="my-6 flex w-full items-center justify-center text-sm font-medium text-primary-text-subtle"
|
||||
disabled={isLoadingMore}
|
||||
onClick={() => {
|
||||
setSize((prev) => prev + 1);
|
||||
}}
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-full w-full scale-75 place-items-center overflow-hidden">
|
||||
<EmptyState
|
||||
title="You're updated with all the notifications"
|
||||
description="You have read all the notifications."
|
||||
image={emptyNotification}
|
||||
{notifications ? (
|
||||
notifications.length > 0 ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="divide-y divide-neutral-border-subtle">
|
||||
{notifications.map((notification) => (
|
||||
<NotificationCard
|
||||
selectedTab={selectedTab}
|
||||
key={notification.id}
|
||||
isSnoozedTabOpen={snoozed}
|
||||
closePopover={() => setIsActive(false)}
|
||||
notification={notification}
|
||||
markNotificationArchivedStatus={markNotificationArchivedStatus}
|
||||
markNotificationReadStatus={markNotificationAsRead}
|
||||
markNotificationReadStatusToggle={markNotificationReadStatus}
|
||||
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
|
||||
markSnoozeNotification={markSnoozeNotification}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{isLoadingMore && (
|
||||
<div className="my-6 flex items-center justify-center text-sm">
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="mr-2 h-6 w-6 animate-spin fill-blue-600 text-neutral-text-medium"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
<p>Loading notifications</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-4 overflow-y-auto p-5">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
)}
|
||||
{hasMore && !isLoadingMore && (
|
||||
<button
|
||||
type="button"
|
||||
className="my-6 flex w-full items-center justify-center text-sm font-medium text-primary-text-subtle"
|
||||
disabled={isLoadingMore}
|
||||
onClick={() => {
|
||||
setSize((prev) => prev + 1);
|
||||
}}
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-full w-full scale-75 place-items-center overflow-hidden">
|
||||
<EmptyState
|
||||
title="You're updated with all the notifications"
|
||||
description="You have read all the notifications."
|
||||
image={emptyNotification}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-4 overflow-y-auto p-5">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
|
@ -109,7 +109,12 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
// This is a workaround to fix the issue of the Notification popover modal close on closing this modal
|
||||
const closeTimeout = setTimeout(() => {
|
||||
onClose();
|
||||
clearTimeout(closeTimeout);
|
||||
}, 50);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reset({ ...defaultValues });
|
||||
clearTimeout(timeout);
|
||||
@ -142,7 +147,7 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-neutral-component-surface-light p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-neutral-component-surface-light p-5 text-left shadow-custom-shadow-md transition-all w-full sm:w-full sm:!max-w-2xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-neutral-text-strong">
|
||||
@ -156,8 +161,8 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center gap-3">
|
||||
<div className="flex-1">
|
||||
<div className="mt-5 flex flex-col md:!flex-row md:items-center gap-3">
|
||||
<div className="flex-1 pb-3 md:pb-0">
|
||||
<h6 className="mb-2 block text-sm font-medium text-neutral-text-subtle">Pick a date</h6>
|
||||
<Controller
|
||||
name="date"
|
||||
|
@ -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-primary-text-subtle 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-neutral-component-surface-medium">
|
||||
<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
|
||||
@ -93,14 +92,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,8 +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"
|
||||
|
@ -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