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,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign up",
|
||||||
medium="MAGIC_LINK",
|
medium="Magic link",
|
||||||
first_time=True,
|
first_time=True,
|
||||||
)
|
)
|
||||||
key, token, current_attempt = generate_magic_token(email=email)
|
key, token, current_attempt = generate_magic_token(email=email)
|
||||||
@ -438,8 +438,8 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign in",
|
||||||
medium="MAGIC_LINK",
|
medium="Magic link",
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -468,8 +468,8 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign in",
|
||||||
medium="EMAIL",
|
medium="Email",
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -274,8 +274,8 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign in",
|
||||||
medium="EMAIL",
|
medium="Email",
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -349,8 +349,8 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign in",
|
||||||
medium="MAGIC_LINK",
|
medium="Magic link",
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -296,7 +296,7 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign in",
|
||||||
medium=medium.upper(),
|
medium=medium.upper(),
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
@ -427,7 +427,7 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign up",
|
||||||
medium=medium.upper(),
|
medium=medium.upper(),
|
||||||
first_time=True,
|
first_time=True,
|
||||||
)
|
)
|
||||||
|
@ -4,12 +4,14 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import { AuthService } from "services/auth.service";
|
import { AuthService } from "services/auth.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// icons
|
// icons
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -34,6 +36,8 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
|||||||
// states
|
// states
|
||||||
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// form info
|
// form info
|
||||||
@ -63,21 +67,34 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
|||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Password created successfully.",
|
message: "Password created successfully.",
|
||||||
});
|
});
|
||||||
|
captureEvent(PASSWORD_CREATE_SELECTED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: false,
|
||||||
|
});
|
||||||
await handleSignInRedirection();
|
await handleSignInRedirection();
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(PASSWORD_CREATE_SELECTED, {
|
||||||
|
state: "FAILED",
|
||||||
|
first_time: false,
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoToWorkspace = async () => {
|
const handleGoToWorkspace = async () => {
|
||||||
setIsGoingToWorkspace(true);
|
setIsGoingToWorkspace(true);
|
||||||
|
await handleSignInRedirection().finally(() => {
|
||||||
await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false));
|
captureEvent(PASSWORD_CREATE_SKIPPED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: false,
|
||||||
|
});
|
||||||
|
setIsGoingToWorkspace(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,7 +7,7 @@ import { Eye, EyeOff, XCircle } from "lucide-react";
|
|||||||
import { AuthService } from "services/auth.service";
|
import { AuthService } from "services/auth.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication, useEventTracker } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { ESignInSteps, ForgotPasswordPopover } from "components/account";
|
import { ESignInSteps, ForgotPasswordPopover } from "components/account";
|
||||||
// ui
|
// ui
|
||||||
@ -16,6 +16,8 @@ import { Button, Input } from "@plane/ui";
|
|||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IPasswordSignInData } from "@plane/types";
|
import { IPasswordSignInData } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -46,6 +48,7 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const isSmtpConfigured = envConfig?.is_smtp_configured;
|
const isSmtpConfigured = envConfig?.is_smtp_configured;
|
||||||
// form info
|
// form info
|
||||||
@ -72,7 +75,13 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
await authService
|
await authService
|
||||||
.passwordSignIn(payload)
|
.passwordSignIn(payload)
|
||||||
.then(async () => await onSubmit())
|
.then(async () => {
|
||||||
|
captureEvent(SIGN_IN_WITH_PASSWORD, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: false,
|
||||||
|
});
|
||||||
|
await onSubmit();
|
||||||
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -182,9 +191,10 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="w-full text-right mt-2 pb-3">
|
<div className="mt-2 w-full pb-3 text-right">
|
||||||
{isSmtpConfigured ? (
|
{isSmtpConfigured ? (
|
||||||
<Link
|
<Link
|
||||||
|
onClick={() => captureEvent(FORGOT_PASSWORD)}
|
||||||
href={`/accounts/forgot-password?email=${email}`}
|
href={`/accounts/forgot-password?email=${email}`}
|
||||||
className="text-xs font-medium text-primary-text-subtle"
|
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 Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication, useEventTracker } from "hooks/store";
|
||||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||||
// components
|
// components
|
||||||
import { LatestFeatureBlock } from "components/common";
|
import { LatestFeatureBlock } from "components/common";
|
||||||
@ -13,6 +13,8 @@ import {
|
|||||||
OAuthOptions,
|
OAuthOptions,
|
||||||
SignInOptionalSetPasswordForm,
|
SignInOptionalSetPasswordForm,
|
||||||
} from "components/account";
|
} from "components/account";
|
||||||
|
// constants
|
||||||
|
import { NAVIGATE_TO_SIGNUP } from "constants/event-tracker";
|
||||||
|
|
||||||
export enum ESignInSteps {
|
export enum ESignInSteps {
|
||||||
EMAIL = "EMAIL",
|
EMAIL = "EMAIL",
|
||||||
@ -32,6 +34,7 @@ export const SignInRoot = observer(() => {
|
|||||||
const {
|
const {
|
||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const isSmtpConfigured = envConfig?.is_smtp_configured;
|
const isSmtpConfigured = envConfig?.is_smtp_configured;
|
||||||
|
|
||||||
@ -110,7 +113,11 @@ export const SignInRoot = observer(() => {
|
|||||||
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_in" />
|
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_in" />
|
||||||
<p className="text-xs text-onboarding-text-300 text-center mt-6">
|
<p className="text-xs text-onboarding-text-300 text-center mt-6">
|
||||||
Don{"'"}t have an account?{" "}
|
Don{"'"}t have an account?{" "}
|
||||||
<Link href="/accounts/sign-up" className="text-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
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
@ -7,12 +7,15 @@ import { UserService } from "services/user.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useTimer from "hooks/use-timer";
|
import useTimer from "hooks/use-timer";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
|
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { CODE_VERIFIED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -41,6 +44,8 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// timer
|
// timer
|
||||||
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
|
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
|
||||||
// form info
|
// form info
|
||||||
@ -69,17 +74,22 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
await authService
|
await authService
|
||||||
.magicSignIn(payload)
|
.magicSignIn(payload)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
captureEvent(CODE_VERIFIED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
const currentUser = await userService.currentUser();
|
const currentUser = await userService.currentUser();
|
||||||
|
|
||||||
await onSubmit(currentUser.is_password_autoset);
|
await onSubmit(currentUser.is_password_autoset);
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(CODE_VERIFIED, {
|
||||||
|
state: "FAILED",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
|
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
|
||||||
|
@ -4,12 +4,14 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import { AuthService } from "services/auth.service";
|
import { AuthService } from "services/auth.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// constants
|
// constants
|
||||||
import { ESignUpSteps } from "components/account";
|
import { ESignUpSteps } from "components/account";
|
||||||
|
import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker";
|
||||||
// icons
|
// icons
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
|
||||||
@ -37,6 +39,8 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
|||||||
// states
|
// states
|
||||||
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// form info
|
// form info
|
||||||
@ -66,21 +70,34 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
|||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Password created successfully.",
|
message: "Password created successfully.",
|
||||||
});
|
});
|
||||||
|
captureEvent(SETUP_PASSWORD, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: true,
|
||||||
|
});
|
||||||
await handleSignInRedirection();
|
await handleSignInRedirection();
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(SETUP_PASSWORD, {
|
||||||
|
state: "FAILED",
|
||||||
|
first_time: true,
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoToWorkspace = async () => {
|
const handleGoToWorkspace = async () => {
|
||||||
setIsGoingToWorkspace(true);
|
setIsGoingToWorkspace(true);
|
||||||
|
await handleSignInRedirection().finally(() => {
|
||||||
await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false));
|
captureEvent(PASSWORD_CREATE_SKIPPED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: true,
|
||||||
|
});
|
||||||
|
setIsGoingToWorkspace(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication, useEventTracker } from "hooks/store";
|
||||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
@ -12,6 +12,8 @@ import {
|
|||||||
SignUpUniqueCodeForm,
|
SignUpUniqueCodeForm,
|
||||||
} from "components/account";
|
} from "components/account";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
// constants
|
||||||
|
import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker";
|
||||||
|
|
||||||
export enum ESignUpSteps {
|
export enum ESignUpSteps {
|
||||||
EMAIL = "EMAIL",
|
EMAIL = "EMAIL",
|
||||||
@ -32,6 +34,7 @@ export const SignUpRoot = observer(() => {
|
|||||||
const {
|
const {
|
||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
||||||
// step 1 submit handler- email verification
|
// step 1 submit handler- email verification
|
||||||
const handleEmailVerification = () => setSignInStep(ESignUpSteps.UNIQUE_CODE);
|
const handleEmailVerification = () => setSignInStep(ESignUpSteps.UNIQUE_CODE);
|
||||||
@ -86,7 +89,11 @@ export const SignUpRoot = observer(() => {
|
|||||||
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_up" />
|
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_up" />
|
||||||
<p className="text-xs text-onboarding-text-300 text-center mt-6">
|
<p className="text-xs text-onboarding-text-300 text-center mt-6">
|
||||||
Already using Plane?{" "}
|
Already using Plane?{" "}
|
||||||
<Link href="/" className="text-primary-text-subtle font-medium underline">
|
<Link
|
||||||
|
href="/"
|
||||||
|
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
|
||||||
|
className="text-primary-text-subtle font-medium underline"
|
||||||
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
@ -8,12 +8,15 @@ import { UserService } from "services/user.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useTimer from "hooks/use-timer";
|
import useTimer from "hooks/use-timer";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
|
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { CODE_VERIFIED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -39,6 +42,8 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
const { email, handleEmailClear, onSubmit } = props;
|
const { email, handleEmailClear, onSubmit } = props;
|
||||||
// states
|
// states
|
||||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// timer
|
// timer
|
||||||
@ -69,17 +74,22 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
await authService
|
await authService
|
||||||
.magicSignIn(payload)
|
.magicSignIn(payload)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
captureEvent(CODE_VERIFIED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
const currentUser = await userService.currentUser();
|
const currentUser = await userService.currentUser();
|
||||||
|
|
||||||
await onSubmit(currentUser.is_password_autoset);
|
await onSubmit(currentUser.is_password_autoset);
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(CODE_VERIFIED, {
|
||||||
|
state: "FAILED",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
|
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
|
||||||
@ -96,7 +106,6 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "A new unique code has been sent to your email.",
|
message: "A new unique code has been sent to your email.",
|
||||||
});
|
});
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
token: "",
|
token: "",
|
||||||
|
@ -16,6 +16,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
|
|||||||
// constants
|
// constants
|
||||||
import { CYCLE_STATUS } from "constants/cycle";
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker";
|
||||||
//.types
|
//.types
|
||||||
import { TCycleGroups } from "@plane/types";
|
import { TCycleGroups } from "@plane/types";
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store
|
// store
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -90,39 +91,55 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
|
||||||
setToastAlert({
|
.then(() => {
|
||||||
type: "error",
|
captureEvent(CYCLE_FAVORITED, {
|
||||||
title: "Error!",
|
cycle_id: cycleId,
|
||||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
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>) => {
|
const handleRemoveFromFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
|
||||||
setToastAlert({
|
.then(() => {
|
||||||
type: "error",
|
captureEvent(CYCLE_UNFAVORITED, {
|
||||||
title: "Error!",
|
cycle_id: cycleId,
|
||||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
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>) => {
|
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setTrackElement("Cycles page board layout");
|
setTrackElement("Cycles page grid layout");
|
||||||
setUpdateModal(true);
|
setUpdateModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setTrackElement("Cycles page board layout");
|
setTrackElement("Cycles page grid layout");
|
||||||
setDeleteModal(true);
|
setDeleteModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import { CYCLE_STATUS } from "constants/cycle";
|
|||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
// types
|
// types
|
||||||
import { TCycleGroups } from "@plane/types";
|
import { TCycleGroups } from "@plane/types";
|
||||||
|
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker";
|
||||||
|
|
||||||
type TCyclesListItem = {
|
type TCyclesListItem = {
|
||||||
cycleId: string;
|
cycleId: string;
|
||||||
@ -37,7 +38,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -63,26 +64,42 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
|
||||||
setToastAlert({
|
.then(() => {
|
||||||
type: "error",
|
captureEvent(CYCLE_FAVORITED, {
|
||||||
title: "Error!",
|
cycle_id: cycleId,
|
||||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
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>) => {
|
const handleRemoveFromFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
|
||||||
setToastAlert({
|
.then(() => {
|
||||||
type: "error",
|
captureEvent(CYCLE_UNFAVORITED, {
|
||||||
title: "Error!",
|
cycle_id: cycleId,
|
||||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
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>) => {
|
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -159,9 +176,9 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
|
||||||
<div className="group flex flex-col md:flex-row w-full items-center justify-between gap-5 border-b border-neutral-border-subtle bg-neutral-component-surface-light px-5 py-6 text-sm hover:bg-neutral-component-surface-medium">
|
<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 w-full flex items-center justify-between gap-3 overflow-hidden">
|
<div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
|
||||||
<div className="relative w-full flex items-center gap-3 overflow-hidden">
|
<div className="relative flex w-full items-center gap-3 overflow-hidden">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<CircularProgressIndicator size={38} percentage={progress}>
|
<CircularProgressIndicator size={38} percentage={progress}>
|
||||||
{isCompleted ? (
|
{isCompleted ? (
|
||||||
@ -181,20 +198,20 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
<div className="relative flex items-center gap-2.5 overflow-hidden">
|
<div className="relative flex items-center gap-2.5 overflow-hidden">
|
||||||
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
|
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<Tooltip tooltipContent={cycleDetails.name} position="top">
|
<Tooltip tooltipContent={cycleDetails.name} position="top">
|
||||||
<span className="truncate line-clamp-1 inline-block overflow-hidden text-base font-medium">
|
<span className="line-clamp-1 inline-block overflow-hidden truncate text-base font-medium">
|
||||||
{cycleDetails.name}
|
{cycleDetails.name}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onClick={openCycleOverview} className="flex-shrink-0 z-10 invisible group-hover:visible">
|
<button onClick={openCycleOverview} className="invisible z-10 flex-shrink-0 group-hover:visible">
|
||||||
<Info className="h-4 w-4 text-neutral-text-subtle" />
|
<Info className="h-4 w-4 text-neutral-text-subtle" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentCycle && (
|
{currentCycle && (
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 relative flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs"
|
className="relative flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
|
||||||
style={{
|
style={{
|
||||||
color: currentCycle.color,
|
color: currentCycle.color,
|
||||||
backgroundColor: `${currentCycle.color}20`,
|
backgroundColor: `${currentCycle.color}20`,
|
||||||
@ -206,12 +223,12 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 relative overflow-hidden flex w-full items-center justify-between md:justify-end gap-2.5 md:w-auto md:flex-shrink-0 ">
|
<div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 overflow-hidden md:w-auto md:flex-shrink-0 md:justify-end ">
|
||||||
<div className="text-xs text-neutral-text-medium">
|
<div className="text-xs text-neutral-text-medium">
|
||||||
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
|
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0 relative flex items-center gap-3">
|
<div className="relative flex flex-shrink-0 items-center gap-3">
|
||||||
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
|
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
|
||||||
<div className="flex w-10 cursor-default items-center justify-center">
|
<div className="flex w-10 cursor-default items-center justify-center">
|
||||||
{cycleDetails.assignees.length > 0 ? (
|
{cycleDetails.assignees.length > 0 ? (
|
||||||
@ -221,7 +238,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
))}
|
))}
|
||||||
</AvatarGroup>
|
</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" />
|
<User2 className="h-4 w-4 text-neutral-text-subtle" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
@ -10,6 +10,8 @@ import useToast from "hooks/use-toast";
|
|||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { ICycle } from "@plane/types";
|
import { ICycle } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { CYCLE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
interface ICycleDelete {
|
interface ICycleDelete {
|
||||||
cycle: ICycle;
|
cycle: ICycle;
|
||||||
@ -45,13 +47,13 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
|||||||
message: "Cycle deleted successfully.",
|
message: "Cycle deleted successfully.",
|
||||||
});
|
});
|
||||||
captureCycleEvent({
|
captureCycleEvent({
|
||||||
eventName: "Cycle deleted",
|
eventName: CYCLE_DELETED,
|
||||||
payload: { ...cycle, state: "SUCCESS" },
|
payload: { ...cycle, state: "SUCCESS" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
captureCycleEvent({
|
captureCycleEvent({
|
||||||
eventName: "Cycle deleted",
|
eventName: CYCLE_DELETED,
|
||||||
payload: { ...cycle, state: "FAILED" },
|
payload: { ...cycle, state: "FAILED" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|||||||
import { ICycle } from "@plane/types";
|
import { ICycle } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
|
handleFormSubmit: (values: Partial<ICycle>, dirtyFields: any) => Promise<void>;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -29,7 +29,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||||||
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
|
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
|
||||||
// form data
|
// form data
|
||||||
const {
|
const {
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting, dirtyFields },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
@ -61,7 +61,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||||||
maxDate?.setDate(maxDate.getDate() - 1);
|
maxDate?.setDate(maxDate.getDate() - 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit((formData)=>handleFormSubmit(formData,dirtyFields))}>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
{!status && (
|
{!status && (
|
||||||
|
@ -10,6 +10,8 @@ import useLocalStorage from "hooks/use-local-storage";
|
|||||||
import { CycleForm } from "components/cycles";
|
import { CycleForm } from "components/cycles";
|
||||||
// types
|
// types
|
||||||
import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types";
|
import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type CycleModalProps = {
|
type CycleModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -47,7 +49,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
message: "Cycle created successfully.",
|
message: "Cycle created successfully.",
|
||||||
});
|
});
|
||||||
captureCycleEvent({
|
captureCycleEvent({
|
||||||
eventName: "Cycle created",
|
eventName: CYCLE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS" },
|
payload: { ...res, state: "SUCCESS" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -58,18 +60,23 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
message: err.detail ?? "Error in creating cycle. Please try again.",
|
message: err.detail ?? "Error in creating cycle. Please try again.",
|
||||||
});
|
});
|
||||||
captureCycleEvent({
|
captureCycleEvent({
|
||||||
eventName: "Cycle created",
|
eventName: CYCLE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED" },
|
payload: { ...payload, state: "FAILED" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
|
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>, dirtyFields: any) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const selectedProjectId = payload.project ?? projectId.toString();
|
const selectedProjectId = payload.project ?? projectId.toString();
|
||||||
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
|
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
|
||||||
.then(() => {
|
.then((res) => {
|
||||||
|
const changed_properties = Object.keys(dirtyFields);
|
||||||
|
captureCycleEvent({
|
||||||
|
eventName: CYCLE_UPDATED,
|
||||||
|
payload: { ...res, changed_properties: changed_properties, state: "SUCCESS" },
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -77,6 +84,10 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
captureCycleEvent({
|
||||||
|
eventName: CYCLE_UPDATED,
|
||||||
|
payload: { ...payload, state: "FAILED" },
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -95,7 +106,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
return status;
|
return status;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: Partial<ICycle>) => {
|
const handleFormSubmit = async (formData: Partial<ICycle>, dirtyFields: any) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const payload: Partial<ICycle> = {
|
const payload: Partial<ICycle> = {
|
||||||
@ -119,7 +130,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDateValid) {
|
if (isDateValid) {
|
||||||
if (data) await handleUpdateCycle(data.id, payload);
|
if (data) await handleUpdateCycle(data.id, payload, dirtyFields);
|
||||||
else {
|
else {
|
||||||
await handleCreateCycle(payload).then(() => {
|
await handleCreateCycle(payload).then(() => {
|
||||||
setCycleTab("all");
|
setCycleTab("all");
|
||||||
|
@ -39,6 +39,7 @@ import {
|
|||||||
import { ICycle } from "@plane/types";
|
import { ICycle } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { CYCLE_UPDATED } from "constants/event-tracker";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CYCLE_STATUS } from "constants/cycle";
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
|
|
||||||
@ -67,7 +68,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, peekCycle } = router.query;
|
const { workspaceSlug, projectId, peekCycle } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureCycleEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -83,10 +84,32 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitChanges = (data: Partial<ICycle>) => {
|
const submitChanges = (data: Partial<ICycle>, changedProperty: string) => {
|
||||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||||
|
|
||||||
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
|
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data)
|
||||||
|
.then((res) => {
|
||||||
|
captureCycleEvent({
|
||||||
|
eventName: CYCLE_UPDATED,
|
||||||
|
payload: {
|
||||||
|
...res,
|
||||||
|
changed_properties: [changedProperty],
|
||||||
|
element: "Right side-peek",
|
||||||
|
state: "SUCCESS",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
.catch((_) => {
|
||||||
|
captureCycleEvent({
|
||||||
|
eventName: CYCLE_UPDATED,
|
||||||
|
payload: {
|
||||||
|
...data,
|
||||||
|
element: "Right side-peek",
|
||||||
|
state: "FAILED",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
@ -146,10 +169,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDateValidForExistingCycle) {
|
if (isDateValidForExistingCycle) {
|
||||||
submitChanges({
|
submitChanges(
|
||||||
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
{
|
||||||
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
||||||
});
|
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
||||||
|
},
|
||||||
|
"start_date"
|
||||||
|
);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -174,10 +200,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDateValid) {
|
if (isDateValid) {
|
||||||
submitChanges({
|
submitChanges(
|
||||||
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
{
|
||||||
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
||||||
});
|
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
||||||
|
},
|
||||||
|
"start_date"
|
||||||
|
);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -219,10 +248,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDateValidForExistingCycle) {
|
if (isDateValidForExistingCycle) {
|
||||||
submitChanges({
|
submitChanges(
|
||||||
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
{
|
||||||
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
||||||
});
|
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
||||||
|
},
|
||||||
|
"end_date"
|
||||||
|
);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -246,10 +278,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDateValid) {
|
if (isDateValid) {
|
||||||
submitChanges({
|
submitChanges(
|
||||||
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
{
|
||||||
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
||||||
});
|
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
||||||
|
},
|
||||||
|
"end_date"
|
||||||
|
);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
|
@ -2,7 +2,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { FileText, Plus } from "lucide-react";
|
import { FileText, Plus } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useProject, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import { Breadcrumbs, Button } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -25,6 +25,7 @@ export const PagesHeader = observer(() => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
|
const { setTrackElement } = useEventTracker();
|
||||||
|
|
||||||
const canUserCreatePage =
|
const canUserCreatePage =
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
@ -64,7 +65,15 @@ export const PagesHeader = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
{canUserCreatePage && (
|
{canUserCreatePage && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => toggleCreatePageModal(true)}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
prependIcon={<Plus />}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setTrackElement("Project pages page");
|
||||||
|
toggleCreatePageModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Create Page
|
Create Page
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,18 +4,23 @@ import { useTheme } from "next-themes";
|
|||||||
// images
|
// images
|
||||||
import githubBlackImage from "/public/logos/github-black.png";
|
import githubBlackImage from "/public/logos/github-black.png";
|
||||||
import githubWhiteImage from "/public/logos/github-white.png";
|
import githubWhiteImage from "/public/logos/github-white.png";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs } from "@plane/ui";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
// constants
|
||||||
|
import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker";
|
||||||
|
|
||||||
export const WorkspaceDashboardHeader = () => {
|
export const WorkspaceDashboardHeader = () => {
|
||||||
// hooks
|
// hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<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">
|
<div className="flex items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
<SidebarHamburgerToggle />
|
||||||
<div>
|
<div>
|
||||||
@ -34,16 +39,26 @@ export const WorkspaceDashboardHeader = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 px-3">
|
<div className="flex items-center gap-3 px-3">
|
||||||
<a
|
<a
|
||||||
|
onClick={() =>
|
||||||
|
captureEvent(CHANGELOG_REDIRECTED, {
|
||||||
|
element: "navbar",
|
||||||
|
})
|
||||||
|
}
|
||||||
href="https://plane.so/changelog"
|
href="https://plane.so/changelog"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-neutral-component-surface-dark px-3 py-1.5"
|
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)" />
|
<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>
|
||||||
<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"
|
href="https://github.com/makeplane/plane"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@ -54,7 +69,7 @@ export const WorkspaceDashboardHeader = () => {
|
|||||||
width={16}
|
width={16}
|
||||||
alt="GitHub Logo"
|
alt="GitHub Logo"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs font-medium hidden sm:hidden md:block">Star us on GitHub</span>
|
<span className="hidden text-xs font-medium sm:hidden md:block">Star us on GitHub</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,6 +20,7 @@ import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle
|
|||||||
// types
|
// types
|
||||||
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
|
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { ISSUE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type TInboxIssueActionsHeader = {
|
type TInboxIssueActionsHeader = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -86,17 +87,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
|||||||
throw new Error("Missing required parameters");
|
throw new Error("Missing required parameters");
|
||||||
await removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
await removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: {
|
payload: {
|
||||||
id: inboxIssueId,
|
id: inboxIssueId,
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
element: "Inbox page",
|
element: "Inbox page",
|
||||||
},
|
}
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
router.push({
|
router.push({
|
||||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||||
@ -108,17 +104,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
|||||||
message: "Something went wrong while deleting inbox issue. Please try again.",
|
message: "Something went wrong while deleting inbox issue. Please try again.",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: {
|
payload: {
|
||||||
id: inboxIssueId,
|
id: inboxIssueId,
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
element: "Inbox page",
|
element: "Inbox page",
|
||||||
},
|
},
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -18,6 +18,8 @@ import { GptAssistantPopover } from "components/core";
|
|||||||
import { Button, Input, ToggleSwitch } from "@plane/ui";
|
import { Button, Input, ToggleSwitch } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -65,7 +67,6 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
|||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { captureIssueEvent } = useEventTracker();
|
const { captureIssueEvent } = useEventTracker();
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -94,34 +95,24 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
|||||||
handleClose();
|
handleClose();
|
||||||
} else reset(defaultValues);
|
} else reset(defaultValues);
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: {
|
payload: {
|
||||||
...formData,
|
...formData,
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
element: "Inbox page",
|
element: "Inbox page",
|
||||||
},
|
},
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
path: router.pathname,
|
path: router.pathname,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: {
|
payload: {
|
||||||
...formData,
|
...formData,
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
element: "Inbox page",
|
element: "Inbox page",
|
||||||
},
|
},
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
path: router.pathname,
|
path: router.pathname,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -38,7 +38,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
title: "Attachment uploaded",
|
title: "Attachment uploaded",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: "Issue attachment added",
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "attachment",
|
changed_property: "attachment",
|
||||||
@ -47,7 +47,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: "Issue attachment added",
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -67,7 +67,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
title: "Attachment removed",
|
title: "Attachment removed",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: "Issue attachment deleted",
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "attachment",
|
changed_property: "attachment",
|
||||||
@ -76,7 +76,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: "Issue attachment deleted",
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "attachment",
|
changed_property: "attachment",
|
||||||
|
@ -16,6 +16,7 @@ import { TIssue } from "@plane/types";
|
|||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
export type TIssueOperations = {
|
export type TIssueOperations = {
|
||||||
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
@ -102,7 +103,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: Object.keys(data).join(","),
|
changed_property: Object.keys(data).join(","),
|
||||||
@ -112,7 +113,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue detail page" },
|
payload: { state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: Object.keys(data).join(","),
|
changed_property: Object.keys(data).join(","),
|
||||||
@ -138,7 +139,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Issue deleted successfully",
|
message: "Issue deleted successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -149,7 +150,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Issue delete failed",
|
message: "Issue delete failed",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -164,7 +165,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Issue added to issue successfully",
|
message: "Issue added to issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -174,7 +175,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue detail page" },
|
payload: { state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -198,7 +199,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Cycle removed from issue successfully",
|
message: "Cycle removed from issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -208,7 +209,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue detail page" },
|
payload: { state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -232,7 +233,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Module added to issue successfully",
|
message: "Module added to issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -242,7 +243,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -266,7 +267,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Module removed from issue successfully",
|
message: "Module removed from issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -276,7 +277,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
|
@ -13,6 +13,8 @@ import { createIssuePayload } from "helpers/issue.helper";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
formKey: keyof TIssue;
|
formKey: keyof TIssue;
|
||||||
@ -129,7 +131,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
|||||||
viewId
|
viewId
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "Calendar quick add" },
|
payload: { ...res, state: "SUCCESS", element: "Calendar quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -142,7 +144,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "Calendar quick add" },
|
payload: { ...payload, state: "FAILED", element: "Calendar quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
// hooks
|
// hooks
|
||||||
import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
|
import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
|
||||||
//ui
|
//ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
@ -11,6 +11,8 @@ import { AppliedFiltersList } from "components/issues";
|
|||||||
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
|
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||||
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
|
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
// constants
|
||||||
|
import { GLOBAL_VIEW_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
globalViewId: string;
|
globalViewId: string;
|
||||||
@ -27,6 +29,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
|||||||
} = useIssues(EIssuesStoreType.GLOBAL);
|
} = useIssues(EIssuesStoreType.GLOBAL);
|
||||||
const { workspaceLabels } = useLabel();
|
const { workspaceLabels } = useLabel();
|
||||||
const { globalViewMap, updateGlobalView } = useGlobalView();
|
const { globalViewMap, updateGlobalView } = useGlobalView();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -91,6 +94,13 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
|||||||
filters: {
|
filters: {
|
||||||
...(appliedFilters ?? {}),
|
...(appliedFilters ?? {}),
|
||||||
},
|
},
|
||||||
|
}).then((res) => {
|
||||||
|
captureEvent(GLOBAL_VIEW_UPDATED, {
|
||||||
|
view_id: res.id,
|
||||||
|
applied_filters: res.filters,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Spreadsheet view",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|||||||
import { createIssuePayload } from "helpers/issue.helper";
|
import { createIssuePayload } from "helpers/issue.helper";
|
||||||
// types
|
// types
|
||||||
import { IProject, TIssue } from "@plane/types";
|
import { IProject, TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
interface IInputProps {
|
interface IInputProps {
|
||||||
formKey: string;
|
formKey: string;
|
||||||
@ -111,7 +113,7 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
|
|||||||
quickAddCallback &&
|
quickAddCallback &&
|
||||||
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
|
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "Gantt quick add" },
|
payload: { ...res, state: "SUCCESS", element: "Gantt quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -123,7 +125,7 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
|
|||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "Gantt quick add" },
|
payload: { ...payload, state: "FAILED", element: "Gantt quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -25,6 +25,7 @@ import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile";
|
|||||||
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
|
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
|
||||||
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
|
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
|
||||||
import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue";
|
import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue";
|
||||||
|
import { ISSUE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IBaseKanBanLayout {
|
export interface IBaseKanBanLayout {
|
||||||
issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues;
|
issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues;
|
||||||
@ -212,7 +213,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
setDeleteIssueModal(false);
|
setDeleteIssueModal(false);
|
||||||
setDragState({});
|
setDragState({});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: { id: dragState.draggedIssueId!, state: "FAILED", element: "Kanban layout drag & drop" },
|
payload: { id: dragState.draggedIssueId!, state: "FAILED", element: "Kanban layout drag & drop" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|||||||
import { createIssuePayload } from "helpers/issue.helper";
|
import { createIssuePayload } from "helpers/issue.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
const Inputs = (props: any) => {
|
const Inputs = (props: any) => {
|
||||||
const { register, setFocus, projectDetail } = props;
|
const { register, setFocus, projectDetail } = props;
|
||||||
@ -106,7 +108,7 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
|
|||||||
viewId
|
viewId
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "Kanban quick add" },
|
payload: { ...res, state: "SUCCESS", element: "Kanban quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -118,7 +120,7 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
|
|||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "Kanban quick add" },
|
payload: { ...payload, state: "FAILED", element: "Kanban quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|||||||
import { TIssue, IProject } from "@plane/types";
|
import { TIssue, IProject } from "@plane/types";
|
||||||
// types
|
// types
|
||||||
import { createIssuePayload } from "helpers/issue.helper";
|
import { createIssuePayload } from "helpers/issue.helper";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
interface IInputProps {
|
interface IInputProps {
|
||||||
formKey: string;
|
formKey: string;
|
||||||
@ -103,7 +105,7 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
|
|||||||
quickAddCallback &&
|
quickAddCallback &&
|
||||||
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
|
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "List quick add" },
|
payload: { ...res, state: "SUCCESS", element: "List quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -115,7 +117,7 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
|
|||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "List quick add" },
|
payload: { ...payload, state: "FAILED", element: "List quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,8 @@ import {
|
|||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
|
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IIssueProperties {
|
export interface IIssueProperties {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -40,7 +42,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleState = (stateId: string) => {
|
const handleState = (stateId: string) => {
|
||||||
handleIssues({ ...issue, state_id: stateId }).then(() => {
|
handleIssues({ ...issue, state_id: stateId }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -54,7 +56,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handlePriority = (value: TIssuePriorities) => {
|
const handlePriority = (value: TIssuePriorities) => {
|
||||||
handleIssues({ ...issue, priority: value }).then(() => {
|
handleIssues({ ...issue, priority: value }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -68,7 +70,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleLabel = (ids: string[]) => {
|
const handleLabel = (ids: string[]) => {
|
||||||
handleIssues({ ...issue, label_ids: ids }).then(() => {
|
handleIssues({ ...issue, label_ids: ids }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -82,7 +84,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleAssignee = (ids: string[]) => {
|
const handleAssignee = (ids: string[]) => {
|
||||||
handleIssues({ ...issue, assignee_ids: ids }).then(() => {
|
handleIssues({ ...issue, assignee_ids: ids }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -96,7 +98,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleStartDate = (date: Date | null) => {
|
const handleStartDate = (date: Date | null) => {
|
||||||
handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
|
handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -110,7 +112,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleTargetDate = (date: Date | null) => {
|
const handleTargetDate = (date: Date | null) => {
|
||||||
handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
|
handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -124,7 +126,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleEstimate = (value: number | null) => {
|
const handleEstimate = (value: number | null) => {
|
||||||
handleIssues({ ...issue, estimate_point: value }).then(() => {
|
handleIssues({ ...issue, estimate_point: value }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
|
@ -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">
|
<div className="h-11 border-b-[0.5px] border-neutral-border-medium">
|
||||||
<DateDropdown
|
<DateDropdown
|
||||||
value={issue.target_date}
|
value={issue.target_date}
|
||||||
|
minDate={issue.start_date ? new Date(issue.start_date) : undefined}
|
||||||
onChange={(data) => {
|
onChange={(data) => {
|
||||||
const targetDate = data ? renderFormattedPayloadDate(data) : null;
|
const targetDate = data ? renderFormattedPayloadDate(data) : null;
|
||||||
onChange(
|
onChange(
|
||||||
|
@ -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">
|
<div className="h-11 border-b-[0.5px] border-neutral-border-medium">
|
||||||
<DateDropdown
|
<DateDropdown
|
||||||
value={issue.start_date}
|
value={issue.start_date}
|
||||||
|
maxDate={issue.target_date ? new Date(issue.target_date) : undefined}
|
||||||
onChange={(data) => {
|
onChange={(data) => {
|
||||||
const startDate = data ? renderFormattedPayloadDate(data) : null;
|
const startDate = data ? renderFormattedPayloadDate(data) : null;
|
||||||
onChange(
|
onChange(
|
||||||
|
@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|||||||
import { createIssuePayload } from "helpers/issue.helper";
|
import { createIssuePayload } from "helpers/issue.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
formKey: keyof TIssue;
|
formKey: keyof TIssue;
|
||||||
@ -162,7 +164,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) =>
|
|||||||
(await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then(
|
(await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then(
|
||||||
(res) => {
|
(res) => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" },
|
payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -175,7 +177,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) =>
|
|||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" },
|
payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,8 @@ import { IssueFormRoot } from "./form";
|
|||||||
import type { TIssue } from "@plane/types";
|
import type { TIssue } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue";
|
import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue";
|
||||||
|
import { ISSUE_CREATED, ISSUE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IssuesModalProps {
|
export interface IssuesModalProps {
|
||||||
data?: Partial<TIssue>;
|
data?: Partial<TIssue>;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -157,14 +159,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
message: "Issue created successfully.",
|
message: "Issue created successfully.",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...response, state: "SUCCESS" },
|
payload: { ...response, state: "SUCCESS" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
!createMore && handleClose();
|
!createMore && handleClose();
|
||||||
return response;
|
return response;
|
||||||
@ -175,14 +172,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
message: "Issue could not be created. Please try again.",
|
message: "Issue could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED" },
|
payload: { ...payload, state: "FAILED" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -198,14 +190,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
message: "Issue updated successfully.",
|
message: "Issue updated successfully.",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS" },
|
payload: { ...response, state: "SUCCESS" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
handleClose();
|
handleClose();
|
||||||
return response;
|
return response;
|
||||||
@ -216,14 +203,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
message: "Issue could not be created. Please try again.",
|
message: "Issue could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...payload, state: "FAILED" },
|
payload: { ...payload, state: "FAILED" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ import { TIssue } from "@plane/types";
|
|||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
interface IIssuePeekOverview {
|
interface IIssuePeekOverview {
|
||||||
is_archived?: boolean;
|
is_archived?: boolean;
|
||||||
@ -103,7 +104,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Issue updated successfully",
|
message: "Issue updated successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: Object.keys(data).join(","),
|
changed_property: Object.keys(data).join(","),
|
||||||
@ -113,7 +114,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue peek-overview" },
|
payload: { state: "FAILED", element: "Issue peek-overview" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -135,7 +136,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Issue deleted successfully",
|
message: "Issue deleted successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -146,7 +147,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Issue delete failed",
|
message: "Issue delete failed",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -161,7 +162,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Issue added to issue successfully",
|
message: "Issue added to issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -171,7 +172,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue peek-overview" },
|
payload: { state: "FAILED", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -195,7 +196,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Cycle removed from issue successfully",
|
message: "Cycle removed from issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -210,7 +211,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Cycle remove from issue failed",
|
message: "Cycle remove from issue failed",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue peek-overview" },
|
payload: { state: "FAILED", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -229,7 +230,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Module added to issue successfully",
|
message: "Module added to issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -239,7 +240,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -263,7 +264,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Module removed from issue successfully",
|
message: "Module removed from issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -273,7 +274,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
|
@ -11,6 +11,8 @@ import { Button } from "@plane/ui";
|
|||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import type { IModule } from "@plane/types";
|
import type { IModule } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { MODULE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: IModule;
|
data: IModule;
|
||||||
@ -51,7 +53,7 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: "Module deleted successfully.",
|
message: "Module deleted successfully.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module deleted",
|
eventName: MODULE_DELETED,
|
||||||
payload: { ...data, state: "SUCCESS" },
|
payload: { ...data, state: "SUCCESS" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -62,7 +64,7 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: "Module could not be deleted. Please try again.",
|
message: "Module could not be deleted. Please try again.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module deleted",
|
eventName: MODULE_DELETED,
|
||||||
payload: { ...data, state: "FAILED" },
|
payload: { ...data, state: "FAILED" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -11,7 +11,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|||||||
import { IModule } from "@plane/types";
|
import { IModule } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleFormSubmit: (values: Partial<IModule>) => Promise<void>;
|
handleFormSubmit: (values: Partial<IModule>, dirtyFields: any) => Promise<void>;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -36,7 +36,7 @@ export const ModuleForm: React.FC<Props> = ({
|
|||||||
data,
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting, dirtyFields },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
control,
|
control,
|
||||||
@ -53,7 +53,7 @@ export const ModuleForm: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateUpdateModule = async (formData: Partial<IModule>) => {
|
const handleCreateUpdateModule = async (formData: Partial<IModule>) => {
|
||||||
await handleFormSubmit(formData);
|
await handleFormSubmit(formData, dirtyFields);
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
|
@ -9,6 +9,8 @@ import useToast from "hooks/use-toast";
|
|||||||
import { ModuleForm } from "components/modules";
|
import { ModuleForm } from "components/modules";
|
||||||
// types
|
// types
|
||||||
import type { IModule } from "@plane/types";
|
import type { IModule } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { MODULE_CREATED, MODULE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -59,7 +61,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: "Module created successfully.",
|
message: "Module created successfully.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module created",
|
eventName: MODULE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS" },
|
payload: { ...res, state: "SUCCESS" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -70,13 +72,13 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: err.detail ?? "Module could not be created. Please try again.",
|
message: err.detail ?? "Module could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module created",
|
eventName: MODULE_CREATED,
|
||||||
payload: { ...data, state: "FAILED" },
|
payload: { ...data, state: "FAILED" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateModule = async (payload: Partial<IModule>) => {
|
const handleUpdateModule = async (payload: Partial<IModule>, dirtyFields: any) => {
|
||||||
if (!workspaceSlug || !projectId || !data) return;
|
if (!workspaceSlug || !projectId || !data) return;
|
||||||
|
|
||||||
const selectedProjectId = payload.project ?? projectId.toString();
|
const selectedProjectId = payload.project ?? projectId.toString();
|
||||||
@ -90,8 +92,8 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: "Module updated successfully.",
|
message: "Module updated successfully.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module updated",
|
eventName: MODULE_UPDATED,
|
||||||
payload: { ...res, state: "SUCCESS" },
|
payload: { ...res, changed_properties: Object.keys(dirtyFields), state: "SUCCESS" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -101,20 +103,20 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: err.detail ?? "Module could not be updated. Please try again.",
|
message: err.detail ?? "Module could not be updated. Please try again.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module updated",
|
eventName: MODULE_UPDATED,
|
||||||
payload: { ...data, state: "FAILED" },
|
payload: { ...data, state: "FAILED" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: Partial<IModule>) => {
|
const handleFormSubmit = async (formData: Partial<IModule>, dirtyFields: any) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const payload: Partial<IModule> = {
|
const payload: Partial<IModule> = {
|
||||||
...formData,
|
...formData,
|
||||||
};
|
};
|
||||||
if (!data) await handleCreateModule(payload);
|
if (!data) await handleCreateModule(payload);
|
||||||
else await handleUpdateModule(payload);
|
else await handleUpdateModule(payload, dirtyFields);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -16,6 +16,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
// constants
|
// constants
|
||||||
import { MODULE_STATUS } from "constants/module";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
moduleId: string;
|
moduleId: string;
|
||||||
@ -36,7 +37,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
@ -46,13 +47,21 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => {
|
addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
|
||||||
setToastAlert({
|
.then(() => {
|
||||||
type: "error",
|
captureEvent(MODULE_FAVORITED, {
|
||||||
title: "Error!",
|
module_id: moduleId,
|
||||||
message: "Couldn't add the module to favorites. Please try again.",
|
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>) => {
|
const handleRemoveFromFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -60,13 +69,21 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => {
|
removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
|
||||||
setToastAlert({
|
.then(() => {
|
||||||
type: "error",
|
captureEvent(MODULE_UNFAVORITED, {
|
||||||
title: "Error!",
|
module_id: moduleId,
|
||||||
message: "Couldn't remove the module from favorites. Please try again.",
|
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>) => {
|
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -84,14 +101,14 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
const handleEditModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleEditModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setTrackElement("Modules page board layout");
|
setTrackElement("Modules page grid layout");
|
||||||
setEditModal(true);
|
setEditModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleDeleteModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setTrackElement("Modules page board layout");
|
setTrackElement("Modules page grid layout");
|
||||||
setDeleteModal(true);
|
setDeleteModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
// constants
|
// constants
|
||||||
import { MODULE_STATUS } from "constants/module";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
moduleId: string;
|
moduleId: string;
|
||||||
@ -36,7 +37,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
@ -46,13 +47,21 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => {
|
addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
|
||||||
setToastAlert({
|
.then(() => {
|
||||||
type: "error",
|
captureEvent(MODULE_FAVORITED, {
|
||||||
title: "Error!",
|
module_id: moduleId,
|
||||||
message: "Couldn't add the module to favorites. Please try again.",
|
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>) => {
|
const handleRemoveFromFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -60,13 +69,21 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => {
|
removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
|
||||||
setToastAlert({
|
.then(() => {
|
||||||
type: "error",
|
captureEvent(MODULE_UNFAVORITED, {
|
||||||
title: "Error!",
|
module_id: moduleId,
|
||||||
message: "Couldn't remove the module from favorites. Please try again.",
|
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>) => {
|
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
@ -34,6 +34,7 @@ import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
|
|||||||
// constant
|
// constant
|
||||||
import { MODULE_STATUS } from "constants/module";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
const defaultValues: Partial<IModule> = {
|
const defaultValues: Partial<IModule> = {
|
||||||
lead: "",
|
lead: "",
|
||||||
@ -66,7 +67,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule();
|
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker();
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -77,7 +78,19 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const submitChanges = (data: Partial<IModule>) => {
|
const submitChanges = (data: Partial<IModule>) => {
|
||||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||||
updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data);
|
updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data)
|
||||||
|
.then((res) => {
|
||||||
|
captureModuleEvent({
|
||||||
|
eventName: MODULE_UPDATED,
|
||||||
|
payload: { ...res, changed_properties: Object.keys(data)[0], element: "Right side-peek", state: "SUCCESS" },
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((_) => {
|
||||||
|
captureModuleEvent({
|
||||||
|
eventName: MODULE_UPDATED,
|
||||||
|
payload: { ...data, state: "FAILED" },
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateLink = async (formData: ModuleLink) => {
|
const handleCreateLink = async (formData: ModuleLink) => {
|
||||||
@ -87,6 +100,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload)
|
createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
captureEvent(MODULE_LINK_CREATED, {
|
||||||
|
module_id: moduleId,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Module link created",
|
title: "Module link created",
|
||||||
@ -109,6 +126,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload)
|
updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
captureEvent(MODULE_LINK_UPDATED, {
|
||||||
|
module_id: moduleId,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Module link updated",
|
title: "Module link updated",
|
||||||
@ -129,6 +150,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId)
|
deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
captureEvent(MODULE_LINK_DELETED, {
|
||||||
|
module_id: moduleId,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Module link deleted",
|
title: "Module link deleted",
|
||||||
@ -187,8 +212,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") {
|
if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") {
|
||||||
submitChanges({
|
submitChanges({
|
||||||
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
|
||||||
target_date: renderFormattedPayloadDate(`${watch("target_date")}`),
|
target_date: renderFormattedPayloadDate(`${watch("target_date")}`),
|
||||||
|
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -294,7 +319,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="status"
|
name="status"
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
customButton={
|
customButton={
|
||||||
<span
|
<span
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ArchiveRestore, Clock, MessageSquare, User2 } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Menu } from "@headlessui/react";
|
||||||
|
import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// icons
|
// icons
|
||||||
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
|
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
@ -13,9 +15,12 @@ import { snoozeOptions } from "constants/notification";
|
|||||||
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
|
||||||
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
|
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
|
||||||
// type
|
// type
|
||||||
import type { IUserNotification } from "@plane/types";
|
import type { IUserNotification, NotificationType } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_OPENED, NOTIFICATIONS_READ, NOTIFICATION_ARCHIVED, NOTIFICATION_SNOOZED } from "constants/event-tracker";
|
||||||
|
|
||||||
type NotificationCardProps = {
|
type NotificationCardProps = {
|
||||||
|
selectedTab: NotificationType;
|
||||||
notification: IUserNotification;
|
notification: IUserNotification;
|
||||||
isSnoozedTabOpen: boolean;
|
isSnoozedTabOpen: boolean;
|
||||||
closePopover: () => void;
|
closePopover: () => void;
|
||||||
@ -28,6 +33,7 @@ type NotificationCardProps = {
|
|||||||
|
|
||||||
export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
|
selectedTab,
|
||||||
notification,
|
notification,
|
||||||
isSnoozedTabOpen,
|
isSnoozedTabOpen,
|
||||||
closePopover,
|
closePopover,
|
||||||
@ -37,11 +43,78 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
setSelectedNotificationForSnooze,
|
setSelectedNotificationForSnooze,
|
||||||
markSnoozeNotification,
|
markSnoozeNotification,
|
||||||
} = props;
|
} = props;
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
// states
|
||||||
|
const [showSnoozeOptions, setshowSnoozeOptions] = React.useState(false);
|
||||||
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
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;
|
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
|
||||||
|
|
||||||
@ -49,6 +122,10 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
<Link
|
<Link
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
markNotificationReadStatus(notification.id);
|
markNotificationReadStatus(notification.id);
|
||||||
|
captureEvent(ISSUE_OPENED, {
|
||||||
|
issue_id: notification.data.issue.id,
|
||||||
|
element: "notification",
|
||||||
|
});
|
||||||
closePopover();
|
closePopover();
|
||||||
}}
|
}}
|
||||||
href={`/${workspaceSlug}/projects/${notification.project}/${
|
href={`/${workspaceSlug}/projects/${notification.project}/${
|
||||||
@ -87,57 +164,136 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-2.5 overflow-hidden">
|
<div className="w-full space-y-2.5 overflow-hidden">
|
||||||
{!notification.message ? (
|
<div className="flex items-start">
|
||||||
<div className="w-full break-words text-sm">
|
{!notification.message ? (
|
||||||
<span className="font-semibold">
|
<div className="w-full break-words text-sm">
|
||||||
{notification.triggered_by_details.is_bot
|
<span className="font-semibold">
|
||||||
? notification.triggered_by_details.first_name
|
{notification.triggered_by_details.is_bot
|
||||||
: notification.triggered_by_details.display_name}{" "}
|
? notification.triggered_by_details.first_name
|
||||||
</span>
|
: notification.triggered_by_details.display_name}{" "}
|
||||||
{notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.verb}{" "}
|
</span>
|
||||||
{notification.data.issue_activity.field === "comment"
|
{notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.verb}{" "}
|
||||||
? "commented"
|
{notification.data.issue_activity.field === "comment"
|
||||||
: notification.data.issue_activity.field === "None"
|
? "commented"
|
||||||
? null
|
: notification.data.issue_activity.field === "None"
|
||||||
: replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
|
? null
|
||||||
{notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.field !== "None"
|
: replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
|
||||||
? "to"
|
{notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.field !== "None"
|
||||||
: ""}
|
? "to"
|
||||||
<span className="font-semibold">
|
: ""}
|
||||||
{" "}
|
<span className="font-semibold">
|
||||||
{notification.data.issue_activity.field !== "None" ? (
|
{" "}
|
||||||
notification.data.issue_activity.field !== "comment" ? (
|
{notification.data.issue_activity.field !== "None" ? (
|
||||||
notification.data.issue_activity.field === "target_date" ? (
|
notification.data.issue_activity.field !== "comment" ? (
|
||||||
renderFormattedDate(notification.data.issue_activity.new_value)
|
notification.data.issue_activity.field === "target_date" ? (
|
||||||
) : notification.data.issue_activity.field === "attachment" ? (
|
renderFormattedDate(notification.data.issue_activity.new_value)
|
||||||
"the issue"
|
) : notification.data.issue_activity.field === "attachment" ? (
|
||||||
) : notification.data.issue_activity.field === "description" ? (
|
"the issue"
|
||||||
stripAndTruncateHTML(notification.data.issue_activity.new_value, 55)
|
) : 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>
|
"the issue and assigned it to you."
|
||||||
{`"`}
|
)}
|
||||||
{notification.data.issue_activity.new_value.length > 55
|
</span>
|
||||||
? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
|
</div>
|
||||||
: notification.data.issue_activity.issue_comment}
|
) : (
|
||||||
{`"`}
|
<div className="w-full break-words text-sm">
|
||||||
</span>
|
<span className="semi-bold">{notification.message}</span>
|
||||||
)
|
</div>
|
||||||
) : (
|
)}
|
||||||
"the issue and assigned it to you."
|
<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>
|
||||||
) : (
|
</div>
|
||||||
<div className="w-full break-words text-sm">
|
|
||||||
<span className="semi-bold">{notification.message}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-between gap-2 text-xs">
|
<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(
|
{truncateText(
|
||||||
`${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
|
`${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
|
||||||
50
|
50
|
||||||
@ -152,7 +308,9 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</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>
|
||||||
</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" />,
|
icon: <MessageSquare className="h-3.5 w-3.5 text-neutral-text-medium" />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
markNotificationReadStatusToggle(notification.id).then(() => {
|
markNotificationReadStatusToggle(notification.id).then(() => {
|
||||||
|
captureEvent(NOTIFICATIONS_READ, {
|
||||||
|
issue_id: notification.data.issue.id,
|
||||||
|
tab: selectedTab,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
|
title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -181,6 +344,11 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
markNotificationArchivedStatus(notification.id).then(() => {
|
markNotificationArchivedStatus(notification.id).then(() => {
|
||||||
|
captureEvent(NOTIFICATION_ARCHIVED, {
|
||||||
|
issue_id: notification.data.issue.id,
|
||||||
|
tab: selectedTab,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: notification.archived_at ? "Notification un-archived" : "Notification archived",
|
title: notification.archived_at ? "Notification un-archived" : "Notification archived",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -195,7 +363,6 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
item.onClick();
|
item.onClick();
|
||||||
}}
|
}}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@ -228,6 +395,11 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markSnoozeNotification(notification.id, item.value).then(() => {
|
markSnoozeNotification(notification.id, item.value).then(() => {
|
||||||
|
captureEvent(NOTIFICATION_SNOOZED, {
|
||||||
|
issue_id: notification.data.issue.id,
|
||||||
|
tab: selectedTab,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: `Notification snoozed till ${renderFormattedDate(item.value)}`,
|
title: `Notification snoozed till ${renderFormattedDate(item.value)}`,
|
||||||
type: "success",
|
type: "success",
|
||||||
|
@ -1,11 +1,22 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ArrowLeft, CheckCheck, Clock, ListFilter, MoreVertical, RefreshCw, X } from "lucide-react";
|
import { ArrowLeft, CheckCheck, Clock, ListFilter, MoreVertical, RefreshCw, X } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
// ui
|
// ui
|
||||||
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
|
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// helpers
|
// helpers
|
||||||
import { getNumberCount } from "helpers/string.helper";
|
import { getNumberCount } from "helpers/string.helper";
|
||||||
// type
|
// type
|
||||||
import type { NotificationType, NotificationCount } from "@plane/types";
|
import type { NotificationType, NotificationCount } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import {
|
||||||
|
ARCHIVED_NOTIFICATIONS,
|
||||||
|
NOTIFICATIONS_READ,
|
||||||
|
SNOOZED_NOTIFICATIONS,
|
||||||
|
UNREAD_NOTIFICATIONS,
|
||||||
|
} from "constants/event-tracker";
|
||||||
|
|
||||||
type NotificationHeaderProps = {
|
type NotificationHeaderProps = {
|
||||||
notificationCount?: NotificationCount | null;
|
notificationCount?: NotificationCount | null;
|
||||||
@ -39,6 +50,8 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
setSelectedTab,
|
setSelectedTab,
|
||||||
markAllNotificationsAsRead,
|
markAllNotificationsAsRead,
|
||||||
} = props;
|
} = props;
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
||||||
const notificationTabs: Array<{
|
const notificationTabs: Array<{
|
||||||
label: string;
|
label: string;
|
||||||
@ -65,7 +78,11 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between px-5 pt-5">
|
<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">
|
<div className="flex items-center justify-center gap-x-4 text-neutral-text-medium">
|
||||||
<Tooltip tooltipContent="Refresh">
|
<Tooltip tooltipContent="Refresh">
|
||||||
<button
|
<button
|
||||||
@ -84,6 +101,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
setSnoozed(false);
|
setSnoozed(false);
|
||||||
setArchived(false);
|
setArchived(false);
|
||||||
setReadNotification((prev) => !prev);
|
setReadNotification((prev) => !prev);
|
||||||
|
captureEvent(UNREAD_NOTIFICATIONS);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListFilter className="h-3.5 w-3.5" />
|
<ListFilter className="h-3.5 w-3.5" />
|
||||||
@ -97,7 +115,12 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
}
|
}
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={markAllNotificationsAsRead}>
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
markAllNotificationsAsRead();
|
||||||
|
captureEvent(NOTIFICATIONS_READ);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CheckCheck className="h-3.5 w-3.5" />
|
<CheckCheck className="h-3.5 w-3.5" />
|
||||||
Mark all as read
|
Mark all as read
|
||||||
@ -108,6 +131,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
setArchived(false);
|
setArchived(false);
|
||||||
setReadNotification(false);
|
setReadNotification(false);
|
||||||
setSnoozed((prev) => !prev);
|
setSnoozed((prev) => !prev);
|
||||||
|
captureEvent(SNOOZED_NOTIFICATIONS);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -120,6 +144,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
setSnoozed(false);
|
setSnoozed(false);
|
||||||
setReadNotification(false);
|
setReadNotification(false);
|
||||||
setArchived((prev) => !prev);
|
setArchived((prev) => !prev);
|
||||||
|
captureEvent(ARCHIVED_NOTIFICATIONS);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -128,11 +153,13 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
<Tooltip tooltipContent="Close">
|
<div className="hidden md:block">
|
||||||
<button type="button" onClick={() => closePopover()}>
|
<Tooltip tooltipContent="Close">
|
||||||
<X className="h-3.5 w-3.5" />
|
<button type="button" onClick={() => closePopover()}>
|
||||||
</button>
|
<X className="h-3.5 w-3.5" />
|
||||||
</Tooltip>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 w-full border-b border-neutral-border-medium px-5">
|
<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)}
|
onClick={() => setSelectedTab(tab.value)}
|
||||||
className={`whitespace-nowrap border-b-2 px-1 pb-4 text-sm font-medium outline-none ${
|
className={`whitespace-nowrap border-b-2 px-1 pb-4 text-sm font-medium outline-none ${
|
||||||
tab.value === selectedTab
|
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"
|
: "border-transparent text-neutral-text-medium"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -174,7 +201,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
<span
|
<span
|
||||||
className={`ml-2 rounded-full px-2 py-0.5 text-xs ${
|
className={`ml-2 rounded-full px-2 py-0.5 text-xs ${
|
||||||
tab.value === selectedTab
|
tab.value === selectedTab
|
||||||
? "bg-custom-primary-100 text-white"
|
? "bg-primary-solid text-white"
|
||||||
: "bg-neutral-component-surface-dark text-neutral-text-medium"
|
: "bg-neutral-component-surface-dark text-neutral-text-medium"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -5,6 +5,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication } from "hooks/store";
|
||||||
import useUserNotification from "hooks/use-user-notifications";
|
import useUserNotification from "hooks/use-user-notifications";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
|
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
|
||||||
@ -16,8 +17,12 @@ import { getNumberCount } from "helpers/string.helper";
|
|||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
|
|
||||||
export const NotificationPopover = observer(() => {
|
export const NotificationPopover = observer(() => {
|
||||||
|
// states
|
||||||
|
const [isActive, setIsActive] = React.useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { theme: themeStore } = useApplication();
|
const { theme: themeStore } = useApplication();
|
||||||
|
// refs
|
||||||
|
const notificationPopoverRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
notifications,
|
notifications,
|
||||||
@ -45,8 +50,11 @@ export const NotificationPopover = observer(() => {
|
|||||||
setFetchNotifications,
|
setFetchNotifications,
|
||||||
markAllNotificationsAsRead,
|
markAllNotificationsAsRead,
|
||||||
} = useUserNotification();
|
} = useUserNotification();
|
||||||
|
|
||||||
const isSidebarCollapsed = themeStore.sidebarCollapsed;
|
const isSidebarCollapsed = themeStore.sidebarCollapsed;
|
||||||
|
useOutsideClickDetector(notificationPopoverRef, () => {
|
||||||
|
// if snooze modal is open, then don't close the popover
|
||||||
|
if (selectedNotificationForSnooze === null) setIsActive(false);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -55,144 +63,143 @@ export const NotificationPopover = observer(() => {
|
|||||||
onClose={() => setSelectedNotificationForSnooze(null)}
|
onClose={() => setSelectedNotificationForSnooze(null)}
|
||||||
onSubmit={markSnoozeNotification}
|
onSubmit={markSnoozeNotification}
|
||||||
notification={notifications?.find((notification) => notification.id === selectedNotificationForSnooze) || null}
|
notification={notifications?.find((notification) => notification.id === selectedNotificationForSnooze) || null}
|
||||||
onSuccess={() => {
|
onSuccess={() => setSelectedNotificationForSnooze(null)}
|
||||||
setSelectedNotificationForSnooze(null);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Popover className="relative w-full">
|
<Popover ref={notificationPopoverRef} className="md:relative w-full">
|
||||||
{({ open: isActive, close: closePopover }) => {
|
<>
|
||||||
if (isActive) setFetchNotifications(true);
|
<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 (
|
{notifications ? (
|
||||||
<>
|
notifications.length > 0 ? (
|
||||||
<Tooltip tooltipContent="Notifications" position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
<div className="h-full overflow-y-auto">
|
||||||
<Popover.Button
|
<div className="divide-y divide-neutral-border-subtle">
|
||||||
className={cn(
|
{notifications.map((notification) => (
|
||||||
"group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none",
|
<NotificationCard
|
||||||
{
|
selectedTab={selectedTab}
|
||||||
"bg-primary-component-surface-light text-primary-text-subtle": isActive,
|
key={notification.id}
|
||||||
"text-sidebar-neutral-text-medium hover:bg-sidebar-neutral-component-surface-dark": !isActive,
|
isSnoozedTabOpen={snoozed}
|
||||||
"justify-center": isSidebarCollapsed,
|
closePopover={() => setIsActive(false)}
|
||||||
}
|
notification={notification}
|
||||||
)}
|
markNotificationArchivedStatus={markNotificationArchivedStatus}
|
||||||
>
|
markNotificationReadStatus={markNotificationAsRead}
|
||||||
<Bell className="h-4 w-4" />
|
markNotificationReadStatusToggle={markNotificationReadStatus}
|
||||||
{isSidebarCollapsed ? null : <span>Notifications</span>}
|
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
|
||||||
{totalNotificationCount && totalNotificationCount > 0 ? (
|
markSnoozeNotification={markSnoozeNotification}
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
|
</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>
|
</div>
|
||||||
)
|
)}
|
||||||
) : (
|
{hasMore && !isLoadingMore && (
|
||||||
<Loader className="space-y-4 overflow-y-auto p-5">
|
<button
|
||||||
<Loader.Item height="50px" />
|
type="button"
|
||||||
<Loader.Item height="50px" />
|
className="my-6 flex w-full items-center justify-center text-sm font-medium text-primary-text-subtle"
|
||||||
<Loader.Item height="50px" />
|
disabled={isLoadingMore}
|
||||||
<Loader.Item height="50px" />
|
onClick={() => {
|
||||||
<Loader.Item height="50px" />
|
setSize((prev) => prev + 1);
|
||||||
</Loader>
|
}}
|
||||||
)}
|
>
|
||||||
</Popover.Panel>
|
Load More
|
||||||
</Transition>
|
</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>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -109,7 +109,12 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
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(() => {
|
const timeout = setTimeout(() => {
|
||||||
reset({ ...defaultValues });
|
reset({ ...defaultValues });
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
@ -142,7 +147,7 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
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)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-neutral-text-strong">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex items-center gap-3">
|
<div className="mt-5 flex flex-col md:!flex-row md:items-center gap-3">
|
||||||
<div className="flex-1">
|
<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>
|
<h6 className="mb-2 block text-sm font-medium text-neutral-text-subtle">Pick a date</h6>
|
||||||
<Controller
|
<Controller
|
||||||
name="date"
|
name="date"
|
||||||
|
@ -11,11 +11,13 @@ import { WorkspaceService } from "services/workspace.service";
|
|||||||
// constants
|
// constants
|
||||||
import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
|
import { MEMBER_ACCEPTED } from "constants/event-tracker";
|
||||||
// types
|
// types
|
||||||
import { IWorkspaceMemberInvitation } from "@plane/types";
|
import { IWorkspaceMemberInvitation } from "@plane/types";
|
||||||
// icons
|
// icons
|
||||||
import { CheckCircle2, Search } from "lucide-react";
|
import { CheckCircle2, Search } from "lucide-react";
|
||||||
import {} from "hooks/store/use-event-tracker";
|
import {} from "hooks/store/use-event-tracker";
|
||||||
|
import { getUserRole } from "helpers/user.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleNextStep: () => void;
|
handleNextStep: () => void;
|
||||||
@ -58,11 +60,19 @@ export const Invitations: React.FC<Props> = (props) => {
|
|||||||
if (invitationsRespond.length <= 0) return;
|
if (invitationsRespond.length <= 0) return;
|
||||||
|
|
||||||
setIsJoiningWorkspaces(true);
|
setIsJoiningWorkspaces(true);
|
||||||
|
const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]);
|
||||||
|
|
||||||
await workspaceService
|
await workspaceService
|
||||||
.joinWorkspaces({ invitations: invitationsRespond })
|
.joinWorkspaces({ invitations: invitationsRespond })
|
||||||
.then(async (res) => {
|
.then(async () => {
|
||||||
captureEvent("Member accepted", { ...res, state: "SUCCESS", accepted_from: "App" });
|
captureEvent(MEMBER_ACCEPTED, {
|
||||||
|
member_id: invitation?.id,
|
||||||
|
role: getUserRole(invitation?.role!),
|
||||||
|
project_id: undefined,
|
||||||
|
accepted_from: "App",
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Workspace invitations page",
|
||||||
|
});
|
||||||
await fetchWorkspaces();
|
await fetchWorkspaces();
|
||||||
await mutate(USER_WORKSPACES);
|
await mutate(USER_WORKSPACES);
|
||||||
await updateLastWorkspace();
|
await updateLastWorkspace();
|
||||||
@ -71,7 +81,14 @@ export const Invitations: React.FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
captureEvent("Member accepted", { state: "FAILED", accepted_from: "App" });
|
captureEvent(MEMBER_ACCEPTED, {
|
||||||
|
member_id: invitation?.id,
|
||||||
|
role: getUserRole(invitation?.role!),
|
||||||
|
project_id: undefined,
|
||||||
|
accepted_from: "App",
|
||||||
|
state: "FAILED",
|
||||||
|
element: "Workspace invitations page",
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.finally(() => setIsJoiningWorkspaces(false));
|
.finally(() => setIsJoiningWorkspaces(false));
|
||||||
};
|
};
|
||||||
|
@ -18,6 +18,7 @@ import { Check, ChevronDown, Plus, XCircle } from "lucide-react";
|
|||||||
import { WorkspaceService } from "services/workspace.service";
|
import { WorkspaceService } from "services/workspace.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
@ -28,6 +29,9 @@ import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
|||||||
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
|
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
|
||||||
|
import { MEMBER_INVITED } from "constants/event-tracker";
|
||||||
|
// helpers
|
||||||
|
import { getUserRole } from "helpers/user.helper";
|
||||||
// assets
|
// assets
|
||||||
import user1 from "public/users/user-1.png";
|
import user1 from "public/users/user-1.png";
|
||||||
import user2 from "public/users/user-2.png";
|
import user2 from "public/users/user-2.png";
|
||||||
@ -267,6 +271,8 @@ export const InviteMembers: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -305,6 +311,17 @@ export const InviteMembers: React.FC<Props> = (props) => {
|
|||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
captureEvent(MEMBER_INVITED, {
|
||||||
|
emails: [
|
||||||
|
...payload.emails.map((email) => ({
|
||||||
|
email: email.email,
|
||||||
|
role: getUserRole(email.role),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
project_id: undefined,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Onboarding",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -313,13 +330,18 @@ export const InviteMembers: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
await nextStep();
|
await nextStep();
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(MEMBER_INVITED, {
|
||||||
|
project_id: undefined,
|
||||||
|
state: "FAILED",
|
||||||
|
element: "Onboarding",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error,
|
message: err?.error,
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const appendField = () => {
|
const appendField = () => {
|
||||||
|
@ -15,6 +15,8 @@ import CyclesTour from "public/onboarding/cycles.webp";
|
|||||||
import ModulesTour from "public/onboarding/modules.webp";
|
import ModulesTour from "public/onboarding/modules.webp";
|
||||||
import ViewsTour from "public/onboarding/views.webp";
|
import ViewsTour from "public/onboarding/views.webp";
|
||||||
import PagesTour from "public/onboarding/pages.webp";
|
import PagesTour from "public/onboarding/pages.webp";
|
||||||
|
// constants
|
||||||
|
import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
@ -79,7 +81,7 @@ export const TourRoot: React.FC<Props> = observer((props) => {
|
|||||||
const [step, setStep] = useState<TTourSteps>("welcome");
|
const [step, setStep] = useState<TTourSteps>("welcome");
|
||||||
// store hooks
|
// store hooks
|
||||||
const { commandPalette: commandPaletteStore } = useApplication();
|
const { commandPalette: commandPaletteStore } = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
|
|
||||||
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
|
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
|
||||||
@ -103,13 +105,22 @@ export const TourRoot: React.FC<Props> = observer((props) => {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex h-full items-end">
|
<div className="flex h-full items-end">
|
||||||
<div className="mt-8 flex items-center gap-6">
|
<div className="mt-8 flex items-center gap-6">
|
||||||
<Button variant="primary" onClick={() => setStep("issues")}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
captureEvent(PRODUCT_TOUR_STARTED);
|
||||||
|
setStep("issues");
|
||||||
|
}}
|
||||||
|
>
|
||||||
Take a Product Tour
|
Take a Product Tour
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-transparent text-xs font-medium text-primary-text-subtle outline-custom-text-100"
|
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
|
No thanks, I will explore it myself
|
||||||
</button>
|
</button>
|
||||||
@ -156,8 +167,8 @@ export const TourRoot: React.FC<Props> = observer((props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setTrackElement("Product tour");
|
||||||
onComplete();
|
onComplete();
|
||||||
setTrackElement("Onboarding tour");
|
|
||||||
commandPaletteStore.toggleCreateProjectModal(true);
|
commandPaletteStore.toggleCreateProjectModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Camera, User2 } from "lucide-react";
|
import { Camera, User2 } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useWorkspace } from "hooks/store";
|
import { useEventTracker, useUser, useWorkspace } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding";
|
import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding";
|
||||||
@ -15,6 +15,7 @@ import { IUser } from "@plane/types";
|
|||||||
import { FileService } from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
// assets
|
// assets
|
||||||
import IssuesSvg from "public/onboarding/onboarding-issues.webp";
|
import IssuesSvg from "public/onboarding/onboarding-issues.webp";
|
||||||
|
import { USER_DETAILS } from "constants/event-tracker";
|
||||||
|
|
||||||
const defaultValues: Partial<IUser> = {
|
const defaultValues: Partial<IUser> = {
|
||||||
first_name: "",
|
first_name: "",
|
||||||
@ -48,6 +49,7 @@ export const UserDetails: React.FC<Props> = observer((props) => {
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { updateCurrentUser } = useUser();
|
const { updateCurrentUser } = useUser();
|
||||||
const { workspaces } = useWorkspace();
|
const { workspaces } = useWorkspace();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const workspaceName = workspaces ? Object.values(workspaces)?.[0]?.name : "New Workspace";
|
const workspaceName = workspaces ? Object.values(workspaces)?.[0]?.name : "New Workspace";
|
||||||
// form info
|
// form info
|
||||||
@ -76,7 +78,21 @@ export const UserDetails: React.FC<Props> = observer((props) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await updateCurrentUser(payload);
|
await updateCurrentUser(payload)
|
||||||
|
.then(() => {
|
||||||
|
captureEvent(USER_DETAILS, {
|
||||||
|
use_case: formData.use_case,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Onboarding",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
captureEvent(USER_DETAILS, {
|
||||||
|
use_case: formData.use_case,
|
||||||
|
state: "FAILED",
|
||||||
|
element: "Onboarding",
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const handleDelete = (url: string | null | undefined) => {
|
const handleDelete = (url: string | null | undefined) => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
@ -5,12 +5,13 @@ import { Button, Input } from "@plane/ui";
|
|||||||
// types
|
// types
|
||||||
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useWorkspace } from "hooks/store";
|
import { useEventTracker, useUser, useWorkspace } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
import { WorkspaceService } from "services/workspace.service";
|
import { WorkspaceService } from "services/workspace.service";
|
||||||
// constants
|
// constants
|
||||||
import { RESTRICTED_URLS } from "constants/workspace";
|
import { RESTRICTED_URLS } from "constants/workspace";
|
||||||
|
import { WORKSPACE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
|
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
|
||||||
@ -33,6 +34,7 @@ export const Workspace: React.FC<Props> = (props) => {
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { updateCurrentUser } = useUser();
|
const { updateCurrentUser } = useUser();
|
||||||
const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace();
|
const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace();
|
||||||
|
const { captureWorkspaceEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -46,31 +48,48 @@ export const Workspace: React.FC<Props> = (props) => {
|
|||||||
setSlugError(false);
|
setSlugError(false);
|
||||||
|
|
||||||
await createWorkspace(formData)
|
await createWorkspace(formData)
|
||||||
.then(async () => {
|
.then(async (res) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Workspace created successfully.",
|
message: "Workspace created successfully.",
|
||||||
});
|
});
|
||||||
|
captureWorkspaceEvent({
|
||||||
|
eventName: WORKSPACE_CREATED,
|
||||||
|
payload: {
|
||||||
|
...res,
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: true,
|
||||||
|
element: "Onboarding",
|
||||||
|
},
|
||||||
|
});
|
||||||
await fetchWorkspaces();
|
await fetchWorkspaces();
|
||||||
await completeStep();
|
await completeStep();
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() => {
|
||||||
|
captureWorkspaceEvent({
|
||||||
|
eventName: WORKSPACE_CREATED,
|
||||||
|
payload: {
|
||||||
|
state: "FAILED",
|
||||||
|
first_time: true,
|
||||||
|
element: "Onboarding",
|
||||||
|
},
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Workspace could not be created. Please try again.",
|
message: "Workspace could not be created. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
} else setSlugError(true);
|
} else setSlugError(true);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Some error occurred while creating workspace. Please try again.",
|
message: "Some error occurred while creating workspace. Please try again.",
|
||||||
});
|
})
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeStep = async () => {
|
const completeStep = async () => {
|
||||||
|
@ -13,6 +13,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
|||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker";
|
||||||
|
|
||||||
export const WorkspaceDashboardView = observer(() => {
|
export const WorkspaceDashboardView = observer(() => {
|
||||||
// theme
|
// theme
|
||||||
@ -37,9 +38,8 @@ export const WorkspaceDashboardView = observer(() => {
|
|||||||
const handleTourCompleted = () => {
|
const handleTourCompleted = () => {
|
||||||
updateTourCompleted()
|
updateTourCompleted()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
captureEvent("User tour complete", {
|
captureEvent(PRODUCT_TOUR_COMPLETED, {
|
||||||
user_id: currentUser?.id,
|
user_id: currentUser?.id,
|
||||||
email: currentUser?.email,
|
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -84,7 +84,7 @@ export const WorkspaceDashboardView = observer(() => {
|
|||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Build your first project",
|
text: "Build your first project",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Dashboard");
|
setTrackElement("Dashboard empty state");
|
||||||
toggleCreateProjectModal(true);
|
toggleCreateProjectModal(true);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
@ -3,10 +3,14 @@ import { useRouter } from "next/router";
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// components
|
// components
|
||||||
import { PageForm } from "./page-form";
|
import { PageForm } from "./page-form";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// types
|
// types
|
||||||
import { IPage } from "@plane/types";
|
import { IPage } from "@plane/types";
|
||||||
import { useProjectPages } from "hooks/store/use-project-page";
|
import { useProjectPages } from "hooks/store/use-project-page";
|
||||||
import { IPageStore } from "store/page.store";
|
import { IPageStore } from "store/page.store";
|
||||||
|
// constants
|
||||||
|
import { PAGE_CREATED, PAGE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
// data?: IPage | null;
|
// data?: IPage | null;
|
||||||
@ -21,12 +25,30 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
// store hooks
|
||||||
const { createPage } = useProjectPages();
|
const { createPage } = useProjectPages();
|
||||||
|
const { capturePageEvent } = useEventTracker();
|
||||||
|
|
||||||
const createProjectPage = async (payload: IPage) => {
|
const createProjectPage = async (payload: IPage) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
await createPage(workspaceSlug.toString(), projectId, payload);
|
await createPage(workspaceSlug.toString(), projectId, payload)
|
||||||
|
.then((res) => {
|
||||||
|
capturePageEvent({
|
||||||
|
eventName: PAGE_CREATED,
|
||||||
|
payload: {
|
||||||
|
...res,
|
||||||
|
state: "SUCCESS",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
capturePageEvent({
|
||||||
|
eventName: PAGE_CREATED,
|
||||||
|
payload: {
|
||||||
|
state: "FAILED",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: IPage) => {
|
const handleFormSubmit = async (formData: IPage) => {
|
||||||
@ -39,6 +61,14 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
|||||||
if (pageStore.access !== formData.access) {
|
if (pageStore.access !== formData.access) {
|
||||||
formData.access === 1 ? await pageStore.makePrivate() : await pageStore.makePublic();
|
formData.access === 1 ? await pageStore.makePrivate() : await pageStore.makePublic();
|
||||||
}
|
}
|
||||||
|
capturePageEvent({
|
||||||
|
eventName: PAGE_UPDATED,
|
||||||
|
payload: {
|
||||||
|
...pageStore,
|
||||||
|
state: "SUCCESS",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log("Page updated successfully", pageStore);
|
||||||
} else {
|
} else {
|
||||||
await createProjectPage(formData);
|
await createProjectPage(formData);
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,14 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePage } from "hooks/store";
|
import { useEventTracker, usePage } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { useProjectPages } from "hooks/store/use-project-page";
|
import { useProjectPages } from "hooks/store/use-project-page";
|
||||||
|
// constants
|
||||||
|
import { PAGE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type TConfirmPageDeletionProps = {
|
type TConfirmPageDeletionProps = {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -27,6 +29,7 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
|
|||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { deletePage } = useProjectPages();
|
const { deletePage } = useProjectPages();
|
||||||
|
const { capturePageEvent } = useEventTracker();
|
||||||
const pageStore = usePage(pageId);
|
const pageStore = usePage(pageId);
|
||||||
|
|
||||||
// toast alert
|
// toast alert
|
||||||
@ -49,6 +52,13 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
|
|||||||
// Delete Page will only delete the page from the archive page map, at this point only archived pages can be deleted
|
// Delete Page will only delete the page from the archive page map, at this point only archived pages can be deleted
|
||||||
await deletePage(workspaceSlug.toString(), projectId as string, pageId)
|
await deletePage(workspaceSlug.toString(), projectId as string, pageId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
capturePageEvent({
|
||||||
|
eventName: PAGE_DELETED,
|
||||||
|
payload: {
|
||||||
|
...pageStore,
|
||||||
|
state: "SUCCESS",
|
||||||
|
},
|
||||||
|
});
|
||||||
handleClose();
|
handleClose();
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -57,6 +67,13 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
capturePageEvent({
|
||||||
|
eventName: PAGE_DELETED,
|
||||||
|
payload: {
|
||||||
|
...pageStore,
|
||||||
|
state: "FAILED",
|
||||||
|
},
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
|
@ -18,6 +18,7 @@ import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
|
|||||||
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
|
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { PROJECT_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -134,13 +135,8 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
|||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
};
|
};
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project created",
|
eventName: PROJECT_CREATED,
|
||||||
payload: newPayload,
|
payload: newPayload,
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: res.workspace,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -160,16 +156,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
|||||||
message: err.data[key],
|
message: err.data[key],
|
||||||
});
|
});
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project created",
|
eventName: PROJECT_CREATED,
|
||||||
payload: {
|
payload: {
|
||||||
...payload,
|
...payload,
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
},
|
}
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,8 @@ import useToast from "hooks/use-toast";
|
|||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IProject } from "@plane/types";
|
import type { IProject } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { PROJECT_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type DeleteProjectModal = {
|
type DeleteProjectModal = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -62,13 +64,8 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
|
|||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project deleted",
|
eventName: PROJECT_DELETED,
|
||||||
payload: { ...project, state: "SUCCESS", element: "Project general settings" },
|
payload: { ...project, state: "SUCCESS", element: "Project general settings" },
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -78,13 +75,8 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project deleted",
|
eventName: PROJECT_DELETED,
|
||||||
payload: { ...project, state: "FAILED", element: "Project general settings" },
|
payload: { ...project, state: "FAILED", element: "Project general settings" },
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
|
@ -18,6 +18,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
import { NETWORK_CHOICES } from "constants/project";
|
import { NETWORK_CHOICES } from "constants/project";
|
||||||
// services
|
// services
|
||||||
import { ProjectService } from "services/project";
|
import { ProjectService } from "services/project";
|
||||||
|
import { PROJECT_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IProjectDetailsForm {
|
export interface IProjectDetailsForm {
|
||||||
project: IProject;
|
project: IProject;
|
||||||
@ -45,7 +46,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
setValue,
|
setValue,
|
||||||
setError,
|
setError,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors },
|
formState: { errors, dirtyFields },
|
||||||
} = useForm<IProject>({
|
} = useForm<IProject>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...project,
|
...project,
|
||||||
@ -77,13 +78,15 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
|
|
||||||
return updateProject(workspaceSlug.toString(), project.id, payload)
|
return updateProject(workspaceSlug.toString(), project.id, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
const changed_properties = Object.keys(dirtyFields);
|
||||||
|
console.log(dirtyFields);
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project updated",
|
eventName: PROJECT_UPDATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "Project general settings" },
|
payload: {
|
||||||
group: {
|
...res,
|
||||||
isGrouping: true,
|
changed_properties: changed_properties,
|
||||||
groupType: "Workspace_metrics",
|
state: "SUCCESS",
|
||||||
groupId: res.workspace,
|
element: "Project general settings",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -94,13 +97,8 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project updated",
|
eventName: PROJECT_UPDATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "Project general settings" },
|
payload: { ...payload, state: "FAILED", element: "Project general settings" },
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -153,7 +151,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
||||||
|
|
||||||
<img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" />
|
<img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" />
|
||||||
<div className="absolute bottom-4 z-5 flex w-full items-end justify-between gap-3 px-4">
|
<div className="z-5 absolute bottom-4 flex w-full items-end justify-between gap-3 px-4">
|
||||||
<div className="flex flex-grow gap-3 truncate">
|
<div className="flex flex-grow gap-3 truncate">
|
||||||
<div className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-neutral-component-surface-medium">
|
<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">
|
<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";
|
import { Button, Input } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "@plane/types";
|
import { IProject } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker";
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
@ -63,8 +65,9 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
router.push(`/${workspaceSlug}/projects`);
|
router.push(`/${workspaceSlug}/projects`);
|
||||||
captureEvent("Project member leave", {
|
captureEvent(PROJECT_MEMBER_LEAVE, {
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
|
element: "Project settings members page",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -73,8 +76,9 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Something went wrong please try again later.",
|
message: "Something went wrong please try again later.",
|
||||||
});
|
});
|
||||||
captureEvent("Project member leave", {
|
captureEvent(PROJECT_MEMBER_LEAVE, {
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
|
element: "Project settings members page",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember, useProject, useUser } from "hooks/store";
|
import { useEventTracker, useMember, useProject, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { ConfirmProjectMemberRemove } from "components/project";
|
import { ConfirmProjectMemberRemove } from "components/project";
|
||||||
@ -14,6 +14,7 @@ import { ChevronDown, Dot, XCircle } from "lucide-react";
|
|||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -35,6 +36,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
project: { removeMemberFromProject, getProjectMemberDetails, updateMember },
|
project: { removeMemberFromProject, getProjectMemberDetails, updateMember },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -48,8 +50,11 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
if (userDetails.member.id === currentUser?.id) {
|
if (userDetails.member.id === currentUser?.id) {
|
||||||
await leaveProject(workspaceSlug.toString(), projectId.toString())
|
await leaveProject(workspaceSlug.toString(), projectId.toString())
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
captureEvent(PROJECT_MEMBER_LEAVE, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Project settings members page",
|
||||||
|
});
|
||||||
await fetchProjects(workspaceSlug.toString());
|
await fetchProjects(workspaceSlug.toString());
|
||||||
|
|
||||||
router.push(`/${workspaceSlug}/projects`);
|
router.push(`/${workspaceSlug}/projects`);
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
|
@ -9,9 +9,12 @@ import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui";
|
import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { getUserRole } from "helpers/user.helper";
|
||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { PROJECT_MEMBER_ADDED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -49,7 +52,6 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const {
|
const {
|
||||||
project: { projectMemberIds, bulkAddMembersToProject },
|
project: { projectMemberIds, bulkAddMembersToProject },
|
||||||
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
|
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
|
||||||
@ -79,7 +81,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
const payload = { ...formData };
|
const payload = { ...formData };
|
||||||
|
|
||||||
await bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload)
|
await bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload)
|
||||||
.then((res) => {
|
.then(() => {
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
onClose();
|
onClose();
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -87,32 +89,23 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
message: "Members added successfully.",
|
message: "Members added successfully.",
|
||||||
});
|
});
|
||||||
captureEvent(
|
captureEvent(PROJECT_MEMBER_ADDED, {
|
||||||
"Member added",
|
members: [
|
||||||
{
|
...payload.members.map((member) => ({
|
||||||
...res,
|
member_id: member.member_id,
|
||||||
state: "SUCCESS",
|
role: ROLE[member.role],
|
||||||
},
|
})),
|
||||||
{
|
],
|
||||||
isGrouping: true,
|
state: "SUCCESS",
|
||||||
groupType: "Workspace_metrics",
|
element: "Project settings members page",
|
||||||
groupId: currentWorkspace?.id!,
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
captureEvent(
|
captureEvent(PROJECT_MEMBER_ADDED, {
|
||||||
"Member added",
|
state: "FAILED",
|
||||||
{
|
element: "Project settings members page",
|
||||||
state: "FAILED",
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
|
@ -51,12 +51,11 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { setTrackElement, captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
currentUser,
|
currentUser,
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { currentProjectDetails, updateProject } = useProject();
|
const { currentProjectDetails, updateProject } = useProject();
|
||||||
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||||
// toast alert
|
// toast alert
|
||||||
@ -93,14 +92,9 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
|
|||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={Boolean(currentProjectDetails?.[feature.property as keyof IProject])}
|
value={Boolean(currentProjectDetails?.[feature.property as keyof IProject])}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setTrackElement("PROJECT_SETTINGS_FEATURES_PAGE");
|
|
||||||
captureEvent(`Toggle ${feature.title.toLowerCase()}`, {
|
captureEvent(`Toggle ${feature.title.toLowerCase()}`, {
|
||||||
workspace_id: currentWorkspace?.id,
|
|
||||||
workspace_slug: currentWorkspace?.slug,
|
|
||||||
project_id: currentProjectDetails?.id,
|
|
||||||
project_name: currentProjectDetails?.name,
|
|
||||||
project_identifier: currentProjectDetails?.identifier,
|
|
||||||
enabled: !currentProjectDetails?.[feature.property as keyof IProject],
|
enabled: !currentProjectDetails?.[feature.property as keyof IProject],
|
||||||
|
element: "Project settings feature page",
|
||||||
});
|
});
|
||||||
handleSubmit({
|
handleSubmit({
|
||||||
[feature.property]: !currentProjectDetails?.[feature.property as keyof IProject],
|
[feature.property]: !currentProjectDetails?.[feature.property as keyof IProject],
|
||||||
|
@ -13,6 +13,7 @@ import { Button, CustomSelect, Input, Tooltip } from "@plane/ui";
|
|||||||
import type { IState } from "@plane/types";
|
import type { IState } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_CHOICES } from "constants/project";
|
import { GROUP_CHOICES } from "constants/project";
|
||||||
|
import { STATE_CREATED, STATE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: IState | null;
|
data: IState | null;
|
||||||
@ -36,7 +37,7 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent, setTrackElement } = useEventTracker();
|
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
|
||||||
const { createState, updateState } = useProjectState();
|
const { createState, updateState } = useProjectState();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -86,9 +87,13 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
|
|||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "State created successfully.",
|
message: "State created successfully.",
|
||||||
});
|
});
|
||||||
captureEvent("State created", {
|
captureProjectStateEvent({
|
||||||
...res,
|
eventName: STATE_CREATED,
|
||||||
state: "SUCCESS",
|
payload: {
|
||||||
|
...res,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Project settings states page",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -104,8 +109,14 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "State could not be created. Please try again.",
|
message: "State could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
captureEvent("State created", {
|
|
||||||
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)
|
await updateState(workspaceSlug.toString(), projectId.toString(), data.id, formData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
handleClose();
|
handleClose();
|
||||||
captureEvent("State updated", {
|
captureProjectStateEvent({
|
||||||
...res,
|
eventName: STATE_UPDATED,
|
||||||
state: "SUCCESS",
|
payload: {
|
||||||
|
...res,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Project settings states page",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -139,8 +154,13 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "State could not be updated. Please try again.",
|
message: "State could not be updated. Please try again.",
|
||||||
});
|
});
|
||||||
captureEvent("State updated", {
|
captureProjectStateEvent({
|
||||||
state: "FAILED",
|
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";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IState } from "@plane/types";
|
import type { IState } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { STATE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -25,7 +27,7 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureProjectStateEvent } = useEventTracker();
|
||||||
const { deleteState } = useProjectState();
|
const { deleteState } = useProjectState();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -42,8 +44,12 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
await deleteState(workspaceSlug.toString(), data.project_id, data.id)
|
await deleteState(workspaceSlug.toString(), data.project_id, data.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
captureEvent("State deleted", {
|
captureProjectStateEvent({
|
||||||
state: "SUCCESS",
|
eventName: STATE_DELETED,
|
||||||
|
payload: {
|
||||||
|
...data,
|
||||||
|
state: "SUCCESS",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
handleClose();
|
handleClose();
|
||||||
})
|
})
|
||||||
@ -61,8 +67,12 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "State could not be deleted. Please try again.",
|
message: "State could not be deleted. Please try again.",
|
||||||
});
|
});
|
||||||
captureEvent("State deleted", {
|
captureProjectStateEvent({
|
||||||
state: "FAILED",
|
eventName: STATE_DELETED,
|
||||||
|
payload: {
|
||||||
|
...data,
|
||||||
|
state: "FAILED",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
@ -13,6 +13,7 @@ import { Button, CustomSelect, Input } from "@plane/ui";
|
|||||||
import { IWorkspace } from "@plane/types";
|
import { IWorkspace } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace";
|
import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace";
|
||||||
|
import { WORKSPACE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSubmit?: (res: IWorkspace) => Promise<void>;
|
onSubmit?: (res: IWorkspace) => Promise<void>;
|
||||||
@ -48,7 +49,7 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureWorkspaceEvent } = useEventTracker();
|
||||||
const { createWorkspace } = useWorkspace();
|
const { createWorkspace } = useWorkspace();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -70,9 +71,13 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
await createWorkspace(formData)
|
await createWorkspace(formData)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
captureEvent("Workspace created", {
|
captureWorkspaceEvent({
|
||||||
...res,
|
eventName: WORKSPACE_CREATED,
|
||||||
state: "SUCCESS",
|
payload: {
|
||||||
|
...res,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Create workspace page",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -83,14 +88,18 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||||||
if (onSubmit) await onSubmit(res);
|
if (onSubmit) await onSubmit(res);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
captureWorkspaceEvent({
|
||||||
|
eventName: WORKSPACE_CREATED,
|
||||||
|
payload: {
|
||||||
|
state: "FAILED",
|
||||||
|
element: "Create workspace page",
|
||||||
|
},
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Workspace could not be created. Please try again.",
|
message: "Workspace could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
captureEvent("Workspace created", {
|
|
||||||
state: "FAILED",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else setSlugError(true);
|
} else setSlugError(true);
|
||||||
})
|
})
|
||||||
@ -100,9 +109,6 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Some error occurred while creating workspace. Please try again.",
|
message: "Some error occurred while creating workspace. Please try again.",
|
||||||
});
|
});
|
||||||
captureEvent("Workspace created", {
|
|
||||||
state: "FAILED",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ import useToast from "hooks/use-toast";
|
|||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IWorkspace } from "@plane/types";
|
import type { IWorkspace } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { WORKSPACE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -28,7 +30,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureWorkspaceEvent } = useEventTracker();
|
||||||
const { deleteWorkspace } = useWorkspace();
|
const { deleteWorkspace } = useWorkspace();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -59,9 +61,13 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
handleClose();
|
handleClose();
|
||||||
router.push("/");
|
router.push("/");
|
||||||
captureEvent("Workspace deleted", {
|
captureWorkspaceEvent({
|
||||||
res,
|
eventName: WORKSPACE_DELETED,
|
||||||
state: "SUCCESS",
|
payload: {
|
||||||
|
...data,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Workspace general settings page",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -75,8 +81,13 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Something went wrong. Please try again later.",
|
message: "Something went wrong. Please try again later.",
|
||||||
});
|
});
|
||||||
captureEvent("Workspace deleted", {
|
captureWorkspaceEvent({
|
||||||
state: "FAILED",
|
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 { observer } from "mobx-react-lite";
|
||||||
import { ChevronDown, Dot, XCircle } from "lucide-react";
|
import { ChevronDown, Dot, XCircle } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember, useUser } from "hooks/store";
|
import { useEventTracker, useMember, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { ConfirmWorkspaceMemberRemove } from "components/workspace";
|
import { ConfirmWorkspaceMemberRemove } from "components/workspace";
|
||||||
@ -12,6 +12,7 @@ import { ConfirmWorkspaceMemberRemove } from "components/workspace";
|
|||||||
import { CustomSelect, Tooltip } from "@plane/ui";
|
import { CustomSelect, Tooltip } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
|
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
|
||||||
|
import { WORKSPACE_MEMBER_lEAVE } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
memberId: string;
|
memberId: string;
|
||||||
@ -33,6 +34,7 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
workspace: { updateMember, removeMemberFromWorkspace, getWorkspaceMemberDetails },
|
workspace: { updateMember, removeMemberFromWorkspace, getWorkspaceMemberDetails },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// derived values
|
// derived values
|
||||||
@ -42,7 +44,13 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
|||||||
if (!workspaceSlug || !currentUserSettings) return;
|
if (!workspaceSlug || !currentUserSettings) return;
|
||||||
|
|
||||||
await leaveWorkspace(workspaceSlug.toString())
|
await leaveWorkspace(workspaceSlug.toString())
|
||||||
.then(() => router.push("/profile"))
|
.then(() => {
|
||||||
|
captureEvent(WORKSPACE_MEMBER_lEAVE, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Workspace settings members page",
|
||||||
|
});
|
||||||
|
router.push("/profile");
|
||||||
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
|
@ -19,6 +19,7 @@ import { copyUrlToClipboard } from "helpers/string.helper";
|
|||||||
import { IWorkspace } from "@plane/types";
|
import { IWorkspace } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace";
|
import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace";
|
||||||
|
import { WORKSPACE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
const defaultValues: Partial<IWorkspace> = {
|
const defaultValues: Partial<IWorkspace> = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -37,7 +38,7 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
const [isImageRemoving, setIsImageRemoving] = useState(false);
|
const [isImageRemoving, setIsImageRemoving] = useState(false);
|
||||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureWorkspaceEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -68,9 +69,13 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
|
|
||||||
await updateWorkspace(currentWorkspace.slug, payload)
|
await updateWorkspace(currentWorkspace.slug, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
captureEvent("Workspace updated", {
|
captureWorkspaceEvent({
|
||||||
...res,
|
eventName: WORKSPACE_UPDATED,
|
||||||
state: "SUCCESS",
|
payload: {
|
||||||
|
...res,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Workspace general settings page",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@ -79,8 +84,12 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
captureEvent("Workspace updated", {
|
captureWorkspaceEvent({
|
||||||
state: "FAILED",
|
eventName: WORKSPACE_UPDATED,
|
||||||
|
payload: {
|
||||||
|
state: "FAILED",
|
||||||
|
element: "Workspace general settings page",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
console.error(err);
|
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">
|
<div className="flex w-full flex-col items-start justify-start gap-2 px-4 py-2 text-sm">
|
||||||
<Link
|
<Link
|
||||||
href="/create-workspace"
|
href="/create-workspace"
|
||||||
onClick={() => setTrackElement("APP_SIDEBAR_WORKSPACE_DROPDOWN")}
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
|
@ -3,7 +3,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useUser } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { NotificationPopover } from "components/notifications";
|
import { NotificationPopover } from "components/notifications";
|
||||||
// ui
|
// ui
|
||||||
@ -12,12 +12,14 @@ import { Crown } from "lucide-react";
|
|||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
import { SIDEBAR_MENU_ITEMS } from "constants/dashboard";
|
import { SIDEBAR_MENU_ITEMS } from "constants/dashboard";
|
||||||
|
import { SIDEBAR_CLICKED } from "constants/event-tracker";
|
||||||
// helper
|
// helper
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
|
|
||||||
export const WorkspaceSidebarMenu = observer(() => {
|
export const WorkspaceSidebarMenu = observer(() => {
|
||||||
// store hooks
|
// store hooks
|
||||||
const { theme: themeStore } = useApplication();
|
const { theme: themeStore } = useApplication();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -27,10 +29,13 @@ export const WorkspaceSidebarMenu = observer(() => {
|
|||||||
// computed
|
// computed
|
||||||
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
|
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
|
||||||
|
|
||||||
const handleLinkClick = () => {
|
const handleLinkClick = (itemKey: string) => {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
themeStore.toggleSidebar();
|
themeStore.toggleSidebar();
|
||||||
}
|
}
|
||||||
|
captureEvent(SIDEBAR_CLICKED, {
|
||||||
|
destination: itemKey,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -38,8 +43,8 @@ export const WorkspaceSidebarMenu = observer(() => {
|
|||||||
{SIDEBAR_MENU_ITEMS.map(
|
{SIDEBAR_MENU_ITEMS.map(
|
||||||
(link) =>
|
(link) =>
|
||||||
workspaceMemberInfo >= link.access && (
|
workspaceMemberInfo >= link.access && (
|
||||||
<Link key={link.key} href={`/${workspaceSlug}${link.href}`} onClick={handleLinkClick}>
|
<Link key={link.key} href={`/${workspaceSlug}${link.href}`} onClick={() => handleLinkClick(link.key)}>
|
||||||
<span className="block w-full my-1">
|
<span className="my-1 block w-full">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={link.label}
|
tooltipContent={link.label}
|
||||||
position="right"
|
position="right"
|
||||||
|
@ -4,12 +4,14 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
// store hooks
|
// store hooks
|
||||||
import { useGlobalView } from "hooks/store";
|
import { useGlobalView, useEventTracker } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IWorkspaceView } from "@plane/types";
|
import { IWorkspaceView } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { GLOBAL_VIEW_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: IWorkspaceView;
|
data: IWorkspaceView;
|
||||||
@ -26,6 +28,7 @@ export const DeleteGlobalViewModal: React.FC<Props> = observer((props) => {
|
|||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { deleteGlobalView } = useGlobalView();
|
const { deleteGlobalView } = useGlobalView();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -39,13 +42,23 @@ export const DeleteGlobalViewModal: React.FC<Props> = observer((props) => {
|
|||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await deleteGlobalView(workspaceSlug.toString(), data.id)
|
await deleteGlobalView(workspaceSlug.toString(), data.id)
|
||||||
.catch(() =>
|
.then(() => {
|
||||||
|
captureEvent(GLOBAL_VIEW_DELETED, {
|
||||||
|
view_id: data.id,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
captureEvent(GLOBAL_VIEW_DELETED, {
|
||||||
|
view_id: data.id,
|
||||||
|
state: "FAILED",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Something went wrong while deleting the view. Please try again.",
|
message: "Something went wrong while deleting the view. Please try again.",
|
||||||
})
|
});
|
||||||
)
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
handleClose();
|
handleClose();
|
||||||
|
@ -4,11 +4,12 @@ import Link from "next/link";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
// store hooks
|
// store hooks
|
||||||
import { useGlobalView, useUser } from "hooks/store";
|
import { useEventTracker, useGlobalView, useUser } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateWorkspaceViewModal } from "components/workspace";
|
import { CreateUpdateWorkspaceViewModal } from "components/workspace";
|
||||||
// constants
|
// constants
|
||||||
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
|
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { GLOBAL_VIEW_OPENED } from "constants/event-tracker";
|
||||||
|
|
||||||
const ViewTab = observer((props: { viewId: string }) => {
|
const ViewTab = observer((props: { viewId: string }) => {
|
||||||
const { viewId } = props;
|
const { viewId } = props;
|
||||||
@ -49,11 +50,19 @@ export const GlobalViewsHeader: React.FC = observer(() => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
||||||
// bring the active view to the centre of the header
|
// bring the active view to the centre of the header
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!globalViewId) return;
|
if (!globalViewId) return;
|
||||||
|
|
||||||
|
captureEvent(GLOBAL_VIEW_OPENED, {
|
||||||
|
view_id: globalViewId,
|
||||||
|
view_type: ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString())
|
||||||
|
? "Default"
|
||||||
|
: "Custom",
|
||||||
|
});
|
||||||
|
|
||||||
const activeTabElement = document.querySelector(`#global-view-${globalViewId.toString()}`);
|
const activeTabElement = document.querySelector(`#global-view-${globalViewId.toString()}`);
|
||||||
|
|
||||||
if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" });
|
if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" });
|
||||||
|
@ -3,12 +3,14 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// store hooks
|
// store hooks
|
||||||
import { useGlobalView } from "hooks/store";
|
import { useEventTracker, useGlobalView } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { WorkspaceViewForm } from "components/workspace";
|
import { WorkspaceViewForm } from "components/workspace";
|
||||||
// types
|
// types
|
||||||
import { IWorkspaceView } from "@plane/types";
|
import { IWorkspaceView } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { GLOBAL_VIEW_CREATED, GLOBAL_VIEW_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data?: IWorkspaceView;
|
data?: IWorkspaceView;
|
||||||
@ -24,6 +26,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
|
|||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { createGlobalView, updateGlobalView } = useGlobalView();
|
const { createGlobalView, updateGlobalView } = useGlobalView();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -43,6 +46,11 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
|
|||||||
|
|
||||||
await createGlobalView(workspaceSlug.toString(), payloadData)
|
await createGlobalView(workspaceSlug.toString(), payloadData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
captureEvent(GLOBAL_VIEW_CREATED, {
|
||||||
|
view_id: res.id,
|
||||||
|
applied_filters: res.filters,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -52,13 +60,17 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
|
|||||||
router.push(`/${workspaceSlug}/workspace-views/${res.id}`);
|
router.push(`/${workspaceSlug}/workspace-views/${res.id}`);
|
||||||
handleClose();
|
handleClose();
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() => {
|
||||||
|
captureEvent(GLOBAL_VIEW_CREATED, {
|
||||||
|
applied_filters: payload?.filters,
|
||||||
|
state: "FAILED",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "View could not be created. Please try again.",
|
message: "View could not be created. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateView = async (payload: Partial<IWorkspaceView>) => {
|
const handleUpdateView = async (payload: Partial<IWorkspaceView>) => {
|
||||||
@ -72,7 +84,12 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
|
|||||||
};
|
};
|
||||||
|
|
||||||
await updateGlobalView(workspaceSlug.toString(), data.id, payloadData)
|
await updateGlobalView(workspaceSlug.toString(), data.id, payloadData)
|
||||||
.then(() => {
|
.then((res) => {
|
||||||
|
captureEvent(GLOBAL_VIEW_UPDATED, {
|
||||||
|
view_id: res.id,
|
||||||
|
applied_filters: res.filters,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -80,13 +97,18 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
|
|||||||
});
|
});
|
||||||
handleClose();
|
handleClose();
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() => {
|
||||||
|
captureEvent(GLOBAL_VIEW_UPDATED, {
|
||||||
|
view_id: data.id,
|
||||||
|
applied_filters: data.filters,
|
||||||
|
state: "FAILED",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "View could not be updated. Please try again.",
|
message: "View could not be updated. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: Partial<IWorkspaceView>) => {
|
const handleFormSubmit = async (formData: Partial<IWorkspaceView>) => {
|
||||||
|
@ -4,7 +4,7 @@ import Link from "next/link";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Pencil, Trash2 } from "lucide-react";
|
import { Pencil, Trash2 } from "lucide-react";
|
||||||
// store hooks
|
// store hooks
|
||||||
import { useGlobalView } from "hooks/store";
|
import { useEventTracker, useGlobalView } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace";
|
import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace";
|
||||||
// ui
|
// ui
|
||||||
@ -25,6 +25,7 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
|
|||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getViewDetailsById } = useGlobalView();
|
const { getViewDetailsById } = useGlobalView();
|
||||||
|
const {setTrackElement} = useEventTracker();
|
||||||
// derived data
|
// derived data
|
||||||
const view = getViewDetailsById(viewId);
|
const view = getViewDetailsById(viewId);
|
||||||
|
|
||||||
@ -59,6 +60,7 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setTrackElement("List view");
|
||||||
setUpdateViewModal(true);
|
setUpdateViewModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -2,26 +2,31 @@ export type IssueEventProps = {
|
|||||||
eventName: string;
|
eventName: string;
|
||||||
payload: any;
|
payload: any;
|
||||||
updates?: any;
|
updates?: any;
|
||||||
group?: EventGroupProps;
|
|
||||||
path?: string;
|
path?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EventProps = {
|
export type EventProps = {
|
||||||
eventName: string;
|
eventName: string;
|
||||||
payload: any;
|
payload: any;
|
||||||
group?: EventGroupProps;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EventGroupProps = {
|
export const getWorkspaceEventPayload = (payload: any) => ({
|
||||||
isGrouping?: boolean;
|
workspace_id: payload.id,
|
||||||
groupType?: string;
|
created_at: payload.created_at,
|
||||||
groupId?: string;
|
updated_at: payload.updated_at,
|
||||||
};
|
organization_size: payload.organization_size,
|
||||||
|
first_time: payload.first_time,
|
||||||
|
state: payload.state,
|
||||||
|
element: payload.element,
|
||||||
|
});
|
||||||
|
|
||||||
export const getProjectEventPayload = (payload: any) => ({
|
export const getProjectEventPayload = (payload: any) => ({
|
||||||
workspace_id: payload.workspace_id,
|
workspace_id: payload.workspace_id,
|
||||||
project_id: payload.id,
|
project_id: payload.id,
|
||||||
identifier: payload.identifier,
|
identifier: payload.identifier,
|
||||||
|
project_visibility: payload.network == 2 ? "Public" : "Private",
|
||||||
|
changed_properties: payload.changed_properties,
|
||||||
|
lead_id: payload.project_lead,
|
||||||
created_at: payload.created_at,
|
created_at: payload.created_at,
|
||||||
updated_at: payload.updated_at,
|
updated_at: payload.updated_at,
|
||||||
state: payload.state,
|
state: payload.state,
|
||||||
@ -30,26 +35,43 @@ export const getProjectEventPayload = (payload: any) => ({
|
|||||||
|
|
||||||
export const getCycleEventPayload = (payload: any) => ({
|
export const getCycleEventPayload = (payload: any) => ({
|
||||||
workspace_id: payload.workspace_id,
|
workspace_id: payload.workspace_id,
|
||||||
project_id: payload.id,
|
project_id: payload.project,
|
||||||
cycle_id: payload.id,
|
cycle_id: payload.id,
|
||||||
created_at: payload.created_at,
|
created_at: payload.created_at,
|
||||||
updated_at: payload.updated_at,
|
updated_at: payload.updated_at,
|
||||||
start_date: payload.start_date,
|
start_date: payload.start_date,
|
||||||
target_date: payload.target_date,
|
target_date: payload.target_date,
|
||||||
cycle_status: payload.status,
|
cycle_status: payload.status,
|
||||||
|
changed_properties: payload.changed_properties,
|
||||||
state: payload.state,
|
state: payload.state,
|
||||||
element: payload.element,
|
element: payload.element,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getModuleEventPayload = (payload: any) => ({
|
export const getModuleEventPayload = (payload: any) => ({
|
||||||
workspace_id: payload.workspace_id,
|
workspace_id: payload.workspace_id,
|
||||||
project_id: payload.id,
|
project_id: payload.project,
|
||||||
module_id: payload.id,
|
module_id: payload.id,
|
||||||
created_at: payload.created_at,
|
created_at: payload.created_at,
|
||||||
updated_at: payload.updated_at,
|
updated_at: payload.updated_at,
|
||||||
start_date: payload.start_date,
|
start_date: payload.start_date,
|
||||||
target_date: payload.target_date,
|
target_date: payload.target_date,
|
||||||
module_status: payload.status,
|
module_status: payload.status,
|
||||||
|
lead_id: payload.lead,
|
||||||
|
changed_properties: payload.changed_properties,
|
||||||
|
member_ids: payload.members,
|
||||||
|
state: payload.state,
|
||||||
|
element: payload.element,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getPageEventPayload = (payload: any) => ({
|
||||||
|
workspace_id: payload.workspace_id,
|
||||||
|
project_id: payload.project,
|
||||||
|
created_at: payload.created_at,
|
||||||
|
updated_at: payload.updated_at,
|
||||||
|
access: payload.access === 0 ? "Public" : "Private",
|
||||||
|
is_locked: payload.is_locked,
|
||||||
|
archived_at: payload.archived_at,
|
||||||
|
created_by: payload.created_by,
|
||||||
state: payload.state,
|
state: payload.state,
|
||||||
element: payload.element,
|
element: payload.element,
|
||||||
});
|
});
|
||||||
@ -71,6 +93,7 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
|
|||||||
sub_issues_count: payload.sub_issues_count,
|
sub_issues_count: payload.sub_issues_count,
|
||||||
parent_id: payload.parent_id,
|
parent_id: payload.parent_id,
|
||||||
project_id: payload.project_id,
|
project_id: payload.project_id,
|
||||||
|
workspace_id: payload.workspace_id,
|
||||||
priority: payload.priority,
|
priority: payload.priority,
|
||||||
state_id: payload.state_id,
|
state_id: payload.state_id,
|
||||||
start_date: payload.start_date,
|
start_date: payload.start_date,
|
||||||
@ -82,7 +105,7 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
|
|||||||
view_id: path?.includes("workspace-views") || path?.includes("views") ? path.split("/").pop() : "",
|
view_id: path?.includes("workspace-views") || path?.includes("views") ? path.split("/").pop() : "",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (eventName === "Issue updated") {
|
if (eventName === ISSUE_UPDATED) {
|
||||||
eventPayload = {
|
eventPayload = {
|
||||||
...eventPayload,
|
...eventPayload,
|
||||||
...updates,
|
...updates,
|
||||||
@ -103,3 +126,99 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
|
|||||||
}
|
}
|
||||||
return eventPayload;
|
return eventPayload;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProjectStateEventPayload = (payload: any) => {
|
||||||
|
return {
|
||||||
|
workspace_id: payload.workspace_id,
|
||||||
|
project_id: payload.id,
|
||||||
|
state_id: payload.id,
|
||||||
|
created_at: payload.created_at,
|
||||||
|
updated_at: payload.updated_at,
|
||||||
|
group: payload.group,
|
||||||
|
color: payload.color,
|
||||||
|
default: payload.default,
|
||||||
|
state: payload.state,
|
||||||
|
element: payload.element,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Workspace crud Events
|
||||||
|
export const WORKSPACE_CREATED = "Workspace created";
|
||||||
|
export const WORKSPACE_UPDATED = "Workspace updated";
|
||||||
|
export const WORKSPACE_DELETED = "Workspace deleted";
|
||||||
|
// Project Events
|
||||||
|
export const PROJECT_CREATED = "Project created";
|
||||||
|
export const PROJECT_UPDATED = "Project updated";
|
||||||
|
export const PROJECT_DELETED = "Project deleted";
|
||||||
|
// Cycle Events
|
||||||
|
export const CYCLE_CREATED = "Cycle created";
|
||||||
|
export const CYCLE_UPDATED = "Cycle updated";
|
||||||
|
export const CYCLE_DELETED = "Cycle deleted";
|
||||||
|
export const CYCLE_FAVORITED = "Cycle favorited";
|
||||||
|
export const CYCLE_UNFAVORITED = "Cycle unfavorited";
|
||||||
|
// Module Events
|
||||||
|
export const MODULE_CREATED = "Module created";
|
||||||
|
export const MODULE_UPDATED = "Module updated";
|
||||||
|
export const MODULE_DELETED = "Module deleted";
|
||||||
|
export const MODULE_FAVORITED = "Module favorited";
|
||||||
|
export const MODULE_UNFAVORITED = "Module unfavorited";
|
||||||
|
export const MODULE_LINK_CREATED = "Module link created";
|
||||||
|
export const MODULE_LINK_UPDATED = "Module link updated";
|
||||||
|
export const MODULE_LINK_DELETED = "Module link deleted";
|
||||||
|
// Issue Events
|
||||||
|
export const ISSUE_CREATED = "Issue created";
|
||||||
|
export const ISSUE_UPDATED = "Issue updated";
|
||||||
|
export const ISSUE_DELETED = "Issue deleted";
|
||||||
|
export const ISSUE_OPENED = "Issue opened";
|
||||||
|
// Project State Events
|
||||||
|
export const STATE_CREATED = "State created";
|
||||||
|
export const STATE_UPDATED = "State updated";
|
||||||
|
export const STATE_DELETED = "State deleted";
|
||||||
|
// Project Page Events
|
||||||
|
export const PAGE_CREATED = "Page created";
|
||||||
|
export const PAGE_UPDATED = "Page updated";
|
||||||
|
export const PAGE_DELETED = "Page deleted";
|
||||||
|
// Member Events
|
||||||
|
export const MEMBER_INVITED = "Member invited";
|
||||||
|
export const MEMBER_ACCEPTED = "Member accepted";
|
||||||
|
export const PROJECT_MEMBER_ADDED = "Project member added";
|
||||||
|
export const PROJECT_MEMBER_LEAVE = "Project member leave";
|
||||||
|
export const WORKSPACE_MEMBER_lEAVE = "Workspace member leave";
|
||||||
|
// Sign-in & Sign-up Events
|
||||||
|
export const NAVIGATE_TO_SIGNUP = "Navigate to sign-up page";
|
||||||
|
export const NAVIGATE_TO_SIGNIN = "Navigate to sign-in page";
|
||||||
|
export const CODE_VERIFIED = "Code verified";
|
||||||
|
export const SETUP_PASSWORD = "Password setup";
|
||||||
|
export const PASSWORD_CREATE_SELECTED = "Password created";
|
||||||
|
export const PASSWORD_CREATE_SKIPPED = "Skipped to setup";
|
||||||
|
export const SIGN_IN_WITH_PASSWORD = "Sign in with password";
|
||||||
|
export const FORGOT_PASSWORD = "Forgot password clicked";
|
||||||
|
export const FORGOT_PASS_LINK = "Forgot password link generated";
|
||||||
|
export const NEW_PASS_CREATED = "New password created";
|
||||||
|
// Onboarding Events
|
||||||
|
export const USER_DETAILS = "User details added";
|
||||||
|
export const USER_ONBOARDING_COMPLETED = "User onboarding completed";
|
||||||
|
// Product Tour Events
|
||||||
|
export const PRODUCT_TOUR_STARTED = "Product tour started";
|
||||||
|
export const PRODUCT_TOUR_COMPLETED = "Product tour completed";
|
||||||
|
export const PRODUCT_TOUR_SKIPPED = "Product tour skipped";
|
||||||
|
// Dashboard Events
|
||||||
|
export const CHANGELOG_REDIRECTED = "Changelog redirected";
|
||||||
|
export const GITHUB_REDIRECTED = "Github redirected";
|
||||||
|
// Sidebar Events
|
||||||
|
export const SIDEBAR_CLICKED = "Sidenav clicked";
|
||||||
|
// Global View Events
|
||||||
|
export const GLOBAL_VIEW_CREATED = "Global view created";
|
||||||
|
export const GLOBAL_VIEW_UPDATED = "Global view updated";
|
||||||
|
export const GLOBAL_VIEW_DELETED = "Global view deleted";
|
||||||
|
export const GLOBAL_VIEW_OPENED = "Global view opened";
|
||||||
|
// Notification Events
|
||||||
|
export const NOTIFICATION_ARCHIVED = "Notification archived";
|
||||||
|
export const NOTIFICATION_SNOOZED = "Notification snoozed";
|
||||||
|
export const NOTIFICATION_READ = "Notification marked read";
|
||||||
|
export const UNREAD_NOTIFICATIONS = "Unread notifications viewed";
|
||||||
|
export const NOTIFICATIONS_READ = "All notifications marked read";
|
||||||
|
export const SNOOZED_NOTIFICATIONS= "Snoozed notifications viewed";
|
||||||
|
export const ARCHIVED_NOTIFICATIONS = "Archived notifications viewed";
|
||||||
|
// Groups
|
||||||
|
export const GROUP_WORKSPACE = "Workspace_metrics";
|
||||||
|
@ -5,7 +5,7 @@ import NProgress from "nprogress";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useUser } from "hooks/store";
|
import { useApplication, useUser, useWorkspace } from "hooks/store";
|
||||||
// constants
|
// constants
|
||||||
import { THEMES } from "constants/themes";
|
import { THEMES } from "constants/themes";
|
||||||
// layouts
|
// layouts
|
||||||
@ -37,6 +37,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
|||||||
currentUser,
|
currentUser,
|
||||||
membership: { currentProjectRole, currentWorkspaceRole },
|
membership: { currentProjectRole, currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
const {
|
const {
|
||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
@ -49,6 +50,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
|||||||
<CrispWrapper user={currentUser}>
|
<CrispWrapper user={currentUser}>
|
||||||
<PostHogProvider
|
<PostHogProvider
|
||||||
user={currentUser}
|
user={currentUser}
|
||||||
|
currentWorkspaceId= {currentWorkspace?.id}
|
||||||
workspaceRole={currentWorkspaceRole}
|
workspaceRole={currentWorkspaceRole}
|
||||||
projectRole={currentProjectRole}
|
projectRole={currentProjectRole}
|
||||||
posthogAPIKey={envConfig?.posthog_api_key || null}
|
posthogAPIKey={envConfig?.posthog_api_key || null}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, ReactNode, useEffect } from "react";
|
import { FC, ReactNode, useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import posthog from "posthog-js";
|
import posthog from "posthog-js";
|
||||||
import { PostHogProvider as PHProvider } from "posthog-js/react";
|
import { PostHogProvider as PHProvider } from "posthog-js/react";
|
||||||
@ -6,10 +6,13 @@ import { PostHogProvider as PHProvider } from "posthog-js/react";
|
|||||||
import { IUser } from "@plane/types";
|
import { IUser } from "@plane/types";
|
||||||
// helpers
|
// helpers
|
||||||
import { getUserRole } from "helpers/user.helper";
|
import { getUserRole } from "helpers/user.helper";
|
||||||
|
// constants
|
||||||
|
import { GROUP_WORKSPACE } from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IPosthogWrapper {
|
export interface IPosthogWrapper {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
user: IUser | null;
|
user: IUser | null;
|
||||||
|
currentWorkspaceId: string | undefined;
|
||||||
workspaceRole: number | undefined;
|
workspaceRole: number | undefined;
|
||||||
projectRole: number | undefined;
|
projectRole: number | undefined;
|
||||||
posthogAPIKey: string | null;
|
posthogAPIKey: string | null;
|
||||||
@ -17,7 +20,9 @@ export interface IPosthogWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PostHogProvider: FC<IPosthogWrapper> = (props) => {
|
const PostHogProvider: FC<IPosthogWrapper> = (props) => {
|
||||||
const { children, user, workspaceRole, projectRole, posthogAPIKey, posthogHost } = props;
|
const { children, user, workspaceRole, currentWorkspaceId, projectRole, posthogAPIKey, posthogHost } = props;
|
||||||
|
// states
|
||||||
|
const [lastWorkspaceId, setLastWorkspaceId] = useState(currentWorkspaceId);
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -25,10 +30,11 @@ const PostHogProvider: FC<IPosthogWrapper> = (props) => {
|
|||||||
if (user) {
|
if (user) {
|
||||||
// Identify sends an event, so you want may want to limit how often you call it
|
// Identify sends an event, so you want may want to limit how often you call it
|
||||||
posthog?.identify(user.email, {
|
posthog?.identify(user.email, {
|
||||||
email: user.email,
|
id: user.id,
|
||||||
first_name: user.first_name,
|
first_name: user.first_name,
|
||||||
last_name: user.last_name,
|
last_name: user.last_name,
|
||||||
id: user.id,
|
email: user.email,
|
||||||
|
use_case: user.use_case,
|
||||||
workspace_role: workspaceRole ? getUserRole(workspaceRole) : undefined,
|
workspace_role: workspaceRole ? getUserRole(workspaceRole) : undefined,
|
||||||
project_role: projectRole ? getUserRole(projectRole) : undefined,
|
project_role: projectRole ? getUserRole(projectRole) : undefined,
|
||||||
});
|
});
|
||||||
@ -45,6 +51,15 @@ const PostHogProvider: FC<IPosthogWrapper> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [posthogAPIKey, posthogHost]);
|
}, [posthogAPIKey, posthogHost]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Join workspace group on workspace change
|
||||||
|
if (lastWorkspaceId !== currentWorkspaceId && currentWorkspaceId && user) {
|
||||||
|
setLastWorkspaceId(currentWorkspaceId);
|
||||||
|
posthog?.identify(user.email);
|
||||||
|
posthog?.group(GROUP_WORKSPACE, currentWorkspaceId);
|
||||||
|
}
|
||||||
|
}, [currentWorkspaceId, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Track page views
|
// Track page views
|
||||||
const handleRouteChange = () => {
|
const handleRouteChange = () => {
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"posthog-js": "^1.88.4",
|
"posthog-js": "^1.105.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-datepicker": "^4.8.0",
|
"react-datepicker": "^4.8.0",
|
||||||
|
@ -6,7 +6,7 @@ import useSWR from "swr";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useUser } from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// layouts
|
// layouts
|
||||||
@ -60,6 +60,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
|||||||
const {
|
const {
|
||||||
commandPalette: { toggleCreatePageModal },
|
commandPalette: { toggleCreatePageModal },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
|
const { setTrackElement } = useEventTracker();
|
||||||
|
|
||||||
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
|
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
|
||||||
useProjectPages();
|
useProjectPages();
|
||||||
@ -194,7 +195,10 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
|||||||
description="Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button."
|
description="Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your 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={{
|
primaryButton={{
|
||||||
text: "Create your first page",
|
text: "Create your first page",
|
||||||
onClick: () => toggleCreatePageModal(true),
|
onClick: () => {
|
||||||
|
setTrackElement("Pages empty state");
|
||||||
|
toggleCreatePageModal(true);
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
comicBox={{
|
comicBox={{
|
||||||
title: "A page can be a doc or a doc of docs.",
|
title: "A page can be a doc or a doc of docs.",
|
||||||
|
@ -16,8 +16,11 @@ import { Button } from "@plane/ui";
|
|||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import { IWorkspaceBulkInviteFormData } from "@plane/types";
|
import { IWorkspaceBulkInviteFormData } from "@plane/types";
|
||||||
|
// helpers
|
||||||
|
import { getUserRole } from "helpers/user.helper";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { MEMBER_INVITED } from "constants/event-tracker";
|
||||||
|
|
||||||
const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
||||||
// states
|
// states
|
||||||
@ -43,7 +46,17 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
return inviteMembersToWorkspace(workspaceSlug.toString(), data)
|
return inviteMembersToWorkspace(workspaceSlug.toString(), data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setInviteModal(false);
|
setInviteModal(false);
|
||||||
captureEvent("Member invited", { state: "SUCCESS" });
|
captureEvent(MEMBER_INVITED, {
|
||||||
|
emails: [
|
||||||
|
...data.emails.map((email) => ({
|
||||||
|
email: email.email,
|
||||||
|
role: getUserRole(email.role),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
project_id: undefined,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Workspace settings member page",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -51,7 +64,17 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
captureEvent("Member invited", { state: "FAILED" });
|
captureEvent(MEMBER_INVITED, {
|
||||||
|
emails: [
|
||||||
|
...data.emails.map((email) => ({
|
||||||
|
email: email.email,
|
||||||
|
role: getUserRole(email.role),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
project_id: undefined,
|
||||||
|
state: "FAILED",
|
||||||
|
element: "Workspace settings member page",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -84,14 +107,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{hasAddMemberPermission && (
|
{hasAddMemberPermission && (
|
||||||
<Button
|
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setTrackElement("WORKSPACE_SETTINGS_MEMBERS_PAGE_HEADER");
|
|
||||||
setInviteModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add member
|
Add member
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
@ -7,6 +7,7 @@ import { AuthService } from "services/auth.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useTimer from "hooks/use-timer";
|
import useTimer from "hooks/use-timer";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import DefaultLayout from "layouts/default-layout";
|
||||||
// components
|
// components
|
||||||
@ -19,6 +20,7 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
|||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// type
|
// type
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
import { FORGOT_PASS_LINK } from "constants/event-tracker";
|
||||||
|
|
||||||
type TForgotPasswordFormValues = {
|
type TForgotPasswordFormValues = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -35,6 +37,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { email } = router.query;
|
const { email } = router.query;
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast
|
// toast
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// timer
|
// timer
|
||||||
@ -57,6 +61,9 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
|
|||||||
email: formData.email,
|
email: formData.email,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
captureEvent(FORGOT_PASS_LINK, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Email sent",
|
title: "Email sent",
|
||||||
@ -65,13 +72,16 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
|
|||||||
});
|
});
|
||||||
setResendCodeTimer(30);
|
setResendCodeTimer(30);
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(FORGOT_PASS_LINK, {
|
||||||
|
state: "FAILED",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,6 +7,7 @@ import { AuthService } from "services/auth.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import DefaultLayout from "layouts/default-layout";
|
||||||
// components
|
// components
|
||||||
@ -21,6 +22,8 @@ import { checkEmailValidity } from "helpers/string.helper";
|
|||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// icons
|
// icons
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
// constants
|
||||||
|
import { NEW_PASS_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type TResetPasswordFormValues = {
|
type TResetPasswordFormValues = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -41,6 +44,8 @@ const ResetPasswordPage: NextPageWithLayout = () => {
|
|||||||
const { uidb64, token, email } = router.query;
|
const { uidb64, token, email } = router.query;
|
||||||
// states
|
// states
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast
|
// toast
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// sign in redirection hook
|
// sign in redirection hook
|
||||||
@ -66,14 +71,22 @@ const ResetPasswordPage: NextPageWithLayout = () => {
|
|||||||
|
|
||||||
await authService
|
await authService
|
||||||
.resetPassword(uidb64.toString(), token.toString(), payload)
|
.resetPassword(uidb64.toString(), token.toString(), payload)
|
||||||
.then(() => handleRedirection())
|
.then(() => {
|
||||||
.catch((err) =>
|
captureEvent(NEW_PASS_CREATED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
|
handleRedirection();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
captureEvent(NEW_PASS_CREATED, {
|
||||||
|
state: "FAILED",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -23,11 +23,13 @@ import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-l
|
|||||||
import emptyInvitation from "public/empty-state/invitation.svg";
|
import emptyInvitation from "public/empty-state/invitation.svg";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
|
import { getUserRole } from "helpers/user.helper";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import type { IWorkspaceMemberInvitation } from "@plane/types";
|
import type { IWorkspaceMemberInvitation } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
|
import { MEMBER_ACCEPTED } from "constants/event-tracker";
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
|
|
||||||
@ -40,7 +42,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
|
|||||||
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
||||||
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
|
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent, joinWorkspaceMetricGroup } = useEventTracker();
|
||||||
const { currentUser, currentUserSettings } = useUser();
|
const { currentUser, currentUserSettings } = useUser();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -81,11 +83,16 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate("USER_WORKSPACES");
|
mutate("USER_WORKSPACES");
|
||||||
const firstInviteId = invitationsRespond[0];
|
const firstInviteId = invitationsRespond[0];
|
||||||
|
const invitation = invitations?.find((i) => i.id === firstInviteId);
|
||||||
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
|
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
|
||||||
captureEvent("Member accepted", {
|
joinWorkspaceMetricGroup(redirectWorkspace?.id);
|
||||||
...res,
|
captureEvent(MEMBER_ACCEPTED, {
|
||||||
state: "SUCCESS",
|
member_id: invitation?.id,
|
||||||
|
role: getUserRole(invitation?.role!),
|
||||||
|
project_id: undefined,
|
||||||
accepted_from: "App",
|
accepted_from: "App",
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Workspace invitations page",
|
||||||
});
|
});
|
||||||
userService
|
userService
|
||||||
.updateUser({ last_workspace_id: redirectWorkspace?.id })
|
.updateUser({ last_workspace_id: redirectWorkspace?.id })
|
||||||
@ -103,6 +110,12 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
captureEvent(MEMBER_ACCEPTED, {
|
||||||
|
project_id: undefined,
|
||||||
|
accepted_from: "App",
|
||||||
|
state: "FAILED",
|
||||||
|
element: "Workspace invitations page",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
|
@ -24,6 +24,8 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
|||||||
// types
|
// types
|
||||||
import { IUser, TOnboardingSteps } from "@plane/types";
|
import { IUser, TOnboardingSteps } from "@plane/types";
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// constants
|
||||||
|
import { USER_ONBOARDING_COMPLETED } from "constants/event-tracker";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const workspaceService = new WorkspaceService();
|
const workspaceService = new WorkspaceService();
|
||||||
@ -79,7 +81,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
await updateUserOnBoard()
|
await updateUserOnBoard()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
captureEvent("User onboarding completed", {
|
captureEvent(USER_ONBOARDING_COMPLETED, {
|
||||||
user_role: user.role,
|
user_role: user.role,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
|
@ -3,39 +3,49 @@ import posthog from "posthog-js";
|
|||||||
// stores
|
// stores
|
||||||
import { RootStore } from "./root.store";
|
import { RootStore } from "./root.store";
|
||||||
import {
|
import {
|
||||||
EventGroupProps,
|
GROUP_WORKSPACE,
|
||||||
|
WORKSPACE_CREATED,
|
||||||
EventProps,
|
EventProps,
|
||||||
IssueEventProps,
|
IssueEventProps,
|
||||||
getCycleEventPayload,
|
getCycleEventPayload,
|
||||||
getIssueEventPayload,
|
getIssueEventPayload,
|
||||||
getModuleEventPayload,
|
getModuleEventPayload,
|
||||||
getProjectEventPayload,
|
getProjectEventPayload,
|
||||||
|
getProjectStateEventPayload,
|
||||||
|
getWorkspaceEventPayload,
|
||||||
|
getPageEventPayload,
|
||||||
} from "constants/event-tracker";
|
} from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IEventTrackerStore {
|
export interface IEventTrackerStore {
|
||||||
// properties
|
// properties
|
||||||
trackElement: string;
|
trackElement: string | undefined;
|
||||||
// computed
|
// computed
|
||||||
getRequiredPayload: any;
|
getRequiredProperties: any;
|
||||||
// actions
|
// actions
|
||||||
|
resetSession: () => void;
|
||||||
setTrackElement: (element: string) => void;
|
setTrackElement: (element: string) => void;
|
||||||
captureEvent: (eventName: string, payload: object | [] | null, group?: EventGroupProps) => void;
|
captureEvent: (eventName: string, payload?: any) => void;
|
||||||
|
joinWorkspaceMetricGroup: (workspaceId?: string) => void;
|
||||||
|
captureWorkspaceEvent: (props: EventProps) => void;
|
||||||
captureProjectEvent: (props: EventProps) => void;
|
captureProjectEvent: (props: EventProps) => void;
|
||||||
captureCycleEvent: (props: EventProps) => void;
|
captureCycleEvent: (props: EventProps) => void;
|
||||||
captureModuleEvent: (props: EventProps) => void;
|
captureModuleEvent: (props: EventProps) => void;
|
||||||
|
capturePageEvent: (props: EventProps) => void;
|
||||||
captureIssueEvent: (props: IssueEventProps) => void;
|
captureIssueEvent: (props: IssueEventProps) => void;
|
||||||
|
captureProjectStateEvent: (props: EventProps) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventTrackerStore implements IEventTrackerStore {
|
export class EventTrackerStore implements IEventTrackerStore {
|
||||||
trackElement: string = "";
|
trackElement: string | undefined = undefined;
|
||||||
rootStore;
|
rootStore;
|
||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// properties
|
// properties
|
||||||
trackElement: observable,
|
trackElement: observable,
|
||||||
// computed
|
// computed
|
||||||
getRequiredPayload: computed,
|
getRequiredProperties: computed,
|
||||||
// actions
|
// actions
|
||||||
|
resetSession: action,
|
||||||
setTrackElement: action,
|
setTrackElement: action,
|
||||||
captureEvent: action,
|
captureEvent: action,
|
||||||
captureProjectEvent: action,
|
captureProjectEvent: action,
|
||||||
@ -48,12 +58,12 @@ export class EventTrackerStore implements IEventTrackerStore {
|
|||||||
/**
|
/**
|
||||||
* @description: Returns the necessary property for the event tracking
|
* @description: Returns the necessary property for the event tracking
|
||||||
*/
|
*/
|
||||||
get getRequiredPayload() {
|
get getRequiredProperties() {
|
||||||
const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
|
const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
|
||||||
const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails;
|
const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails;
|
||||||
return {
|
return {
|
||||||
workspace_id: currentWorkspaceDetails?.id ?? "",
|
workspace_id: currentWorkspaceDetails?.id,
|
||||||
project_id: currentProjectDetails?.id ?? "",
|
project_id: currentProjectDetails?.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,42 +71,74 @@ export class EventTrackerStore implements IEventTrackerStore {
|
|||||||
* @description: Set the trigger point of event.
|
* @description: Set the trigger point of event.
|
||||||
* @param {string} element
|
* @param {string} element
|
||||||
*/
|
*/
|
||||||
setTrackElement = (element: string) => {
|
setTrackElement = (element?: string) => {
|
||||||
this.trackElement = element;
|
this.trackElement = element;
|
||||||
};
|
};
|
||||||
|
|
||||||
postHogGroup = (group: EventGroupProps) => {
|
/**
|
||||||
if (group && group!.isGrouping === true) {
|
* @description: Reset the session.
|
||||||
posthog?.group(group!.groupType!, group!.groupId!, {
|
*/
|
||||||
date: new Date(),
|
resetSession = () => {
|
||||||
workspace_id: group!.groupId,
|
posthog?.reset();
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
captureEvent = (eventName: string, payload: object | [] | null) => {
|
/**
|
||||||
posthog?.capture(eventName, {
|
* @description: Creates the workspace metric group.
|
||||||
...payload,
|
* @param {string} userEmail
|
||||||
element: this.trackElement ?? "",
|
* @param {string} workspaceId
|
||||||
|
*/
|
||||||
|
joinWorkspaceMetricGroup = (workspaceId?: string) => {
|
||||||
|
if (!workspaceId) return;
|
||||||
|
posthog?.group(GROUP_WORKSPACE, workspaceId, {
|
||||||
|
date: new Date().toDateString(),
|
||||||
|
workspace_id: workspaceId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Captures the event.
|
||||||
|
* @param {string} eventName
|
||||||
|
* @param {any} payload
|
||||||
|
*/
|
||||||
|
captureEvent = (eventName: string, payload?: any) => {
|
||||||
|
posthog?.capture(eventName, {
|
||||||
|
...this.getRequiredProperties,
|
||||||
|
...payload,
|
||||||
|
element: payload?.element ?? this.trackElement,
|
||||||
|
});
|
||||||
|
this.setTrackElement(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Captures the workspace crud related events.
|
||||||
|
* @param {EventProps} props
|
||||||
|
*/
|
||||||
|
captureWorkspaceEvent = (props: EventProps) => {
|
||||||
|
const { eventName, payload } = props;
|
||||||
|
if (eventName === WORKSPACE_CREATED && payload.state == "SUCCESS") {
|
||||||
|
this.joinWorkspaceMetricGroup(payload.id);
|
||||||
|
}
|
||||||
|
const eventPayload: any = getWorkspaceEventPayload({
|
||||||
|
...payload,
|
||||||
|
element: payload.element ?? this.trackElement,
|
||||||
|
});
|
||||||
|
posthog?.capture(eventName, eventPayload);
|
||||||
|
this.setTrackElement(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: Captures the project related events.
|
* @description: Captures the project related events.
|
||||||
* @param {EventProps} props
|
* @param {EventProps} props
|
||||||
*/
|
*/
|
||||||
captureProjectEvent = (props: EventProps) => {
|
captureProjectEvent = (props: EventProps) => {
|
||||||
const { eventName, payload, group } = props;
|
const { eventName, payload } = props;
|
||||||
if (group) {
|
|
||||||
this.postHogGroup(group);
|
|
||||||
}
|
|
||||||
const eventPayload: any = getProjectEventPayload({
|
const eventPayload: any = getProjectEventPayload({
|
||||||
...this.getRequiredPayload,
|
...this.getRequiredProperties,
|
||||||
...payload,
|
...payload,
|
||||||
element: payload.element ?? this.trackElement,
|
element: payload.element ?? this.trackElement,
|
||||||
});
|
});
|
||||||
posthog?.capture(eventName, eventPayload);
|
posthog?.capture(eventName, eventPayload);
|
||||||
this.setTrackElement("");
|
this.setTrackElement(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,17 +146,14 @@ export class EventTrackerStore implements IEventTrackerStore {
|
|||||||
* @param {EventProps} props
|
* @param {EventProps} props
|
||||||
*/
|
*/
|
||||||
captureCycleEvent = (props: EventProps) => {
|
captureCycleEvent = (props: EventProps) => {
|
||||||
const { eventName, payload, group } = props;
|
const { eventName, payload } = props;
|
||||||
if (group) {
|
|
||||||
this.postHogGroup(group);
|
|
||||||
}
|
|
||||||
const eventPayload: any = getCycleEventPayload({
|
const eventPayload: any = getCycleEventPayload({
|
||||||
...this.getRequiredPayload,
|
...this.getRequiredProperties,
|
||||||
...payload,
|
...payload,
|
||||||
element: payload.element ?? this.trackElement,
|
element: payload.element ?? this.trackElement,
|
||||||
});
|
});
|
||||||
posthog?.capture(eventName, eventPayload);
|
posthog?.capture(eventName, eventPayload);
|
||||||
this.setTrackElement("");
|
this.setTrackElement(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,17 +161,29 @@ export class EventTrackerStore implements IEventTrackerStore {
|
|||||||
* @param {EventProps} props
|
* @param {EventProps} props
|
||||||
*/
|
*/
|
||||||
captureModuleEvent = (props: EventProps) => {
|
captureModuleEvent = (props: EventProps) => {
|
||||||
const { eventName, payload, group } = props;
|
const { eventName, payload } = props;
|
||||||
if (group) {
|
|
||||||
this.postHogGroup(group);
|
|
||||||
}
|
|
||||||
const eventPayload: any = getModuleEventPayload({
|
const eventPayload: any = getModuleEventPayload({
|
||||||
...this.getRequiredPayload,
|
...this.getRequiredProperties,
|
||||||
...payload,
|
...payload,
|
||||||
element: payload.element ?? this.trackElement,
|
element: payload.element ?? this.trackElement,
|
||||||
});
|
});
|
||||||
posthog?.capture(eventName, eventPayload);
|
posthog?.capture(eventName, eventPayload);
|
||||||
this.setTrackElement("");
|
this.setTrackElement(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Captures the project pages related events.
|
||||||
|
* @param {EventProps} props
|
||||||
|
*/
|
||||||
|
capturePageEvent = (props: EventProps) => {
|
||||||
|
const { eventName, payload } = props;
|
||||||
|
const eventPayload: any = getPageEventPayload({
|
||||||
|
...this.getRequiredProperties,
|
||||||
|
...payload,
|
||||||
|
element: payload.element ?? this.trackElement,
|
||||||
|
});
|
||||||
|
posthog?.capture(eventName, eventPayload);
|
||||||
|
this.setTrackElement(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,16 +191,29 @@ export class EventTrackerStore implements IEventTrackerStore {
|
|||||||
* @param {IssueEventProps} props
|
* @param {IssueEventProps} props
|
||||||
*/
|
*/
|
||||||
captureIssueEvent = (props: IssueEventProps) => {
|
captureIssueEvent = (props: IssueEventProps) => {
|
||||||
const { eventName, payload, group } = props;
|
const { eventName, payload } = props;
|
||||||
if (group) {
|
|
||||||
this.postHogGroup(group);
|
|
||||||
}
|
|
||||||
const eventPayload: any = {
|
const eventPayload: any = {
|
||||||
...getIssueEventPayload(props),
|
...getIssueEventPayload(props),
|
||||||
...this.getRequiredPayload,
|
...this.getRequiredProperties,
|
||||||
state_group: this.rootStore.state.getStateById(payload.state_id)?.group ?? "",
|
state_group: this.rootStore.state.getStateById(payload.state_id)?.group ?? "",
|
||||||
element: payload.element ?? this.trackElement,
|
element: payload.element ?? this.trackElement,
|
||||||
};
|
};
|
||||||
posthog?.capture(eventName, eventPayload);
|
posthog?.capture(eventName, eventPayload);
|
||||||
|
this.setTrackElement(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Captures the issue related events.
|
||||||
|
* @param {IssueEventProps} props
|
||||||
|
*/
|
||||||
|
captureProjectStateEvent = (props: EventProps) => {
|
||||||
|
const { eventName, payload } = props;
|
||||||
|
const eventPayload: any = getProjectStateEventPayload({
|
||||||
|
...this.getRequiredProperties,
|
||||||
|
...payload,
|
||||||
|
element: payload.element ?? this.trackElement,
|
||||||
|
});
|
||||||
|
posthog?.capture(eventName, eventPayload);
|
||||||
|
this.setTrackElement(undefined);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import sortBy from "lodash/sortBy";
|
import orderBy from "lodash/orderBy";
|
||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import indexOf from "lodash/indexOf";
|
import indexOf from "lodash/indexOf";
|
||||||
import reverse from "lodash/reverse";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import values from "lodash/values";
|
import values from "lodash/values";
|
||||||
// types
|
// types
|
||||||
import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
|
import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
|
||||||
@ -144,98 +144,189 @@ export class IssueHelperStore implements TIssueHelperStore {
|
|||||||
issueDisplayFiltersDefaultData = (groupBy: string | null): string[] => {
|
issueDisplayFiltersDefaultData = (groupBy: string | null): string[] => {
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case "state":
|
case "state":
|
||||||
return this.rootStore?.states || [];
|
return Object.keys(this.rootStore?.stateMap || {});
|
||||||
case "state_detail.group":
|
case "state_detail.group":
|
||||||
return Object.keys(STATE_GROUPS);
|
return Object.keys(STATE_GROUPS);
|
||||||
case "priority":
|
case "priority":
|
||||||
return ISSUE_PRIORITIES.map((i) => i.key);
|
return ISSUE_PRIORITIES.map((i) => i.key);
|
||||||
case "labels":
|
case "labels":
|
||||||
return this.rootStore?.labels || [];
|
return Object.keys(this.rootStore?.labelMap || {});
|
||||||
case "created_by":
|
case "created_by":
|
||||||
return this.rootStore?.members || [];
|
return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {});
|
||||||
case "assignees":
|
case "assignees":
|
||||||
return this.rootStore?.members || [];
|
return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {});
|
||||||
case "project":
|
case "project":
|
||||||
return this.rootStore?.projects || [];
|
return Object.keys(this.rootStore?.projectMap || {});
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Method is used to get data of the issue based on the ids of the data for states, labels adn assignees
|
||||||
|
* @param dataType what type of data is being sent
|
||||||
|
* @param dataIds id/ids of the data that is to be populated
|
||||||
|
* @param order ascending or descending for arrays of data
|
||||||
|
* @returns string | string[] of sortable fields to be used for sorting
|
||||||
|
*/
|
||||||
|
populateIssueDataForSorting(
|
||||||
|
dataType: "state_id" | "label_ids" | "assignee_ids",
|
||||||
|
dataIds: string | string[] | null | undefined,
|
||||||
|
order?: "asc" | "desc"
|
||||||
|
) {
|
||||||
|
if (!dataIds) return;
|
||||||
|
|
||||||
|
const dataValues: string[] = [];
|
||||||
|
const isDataIdsArray = Array.isArray(dataIds);
|
||||||
|
const dataIdsArray = isDataIdsArray ? dataIds : [dataIds];
|
||||||
|
|
||||||
|
switch (dataType) {
|
||||||
|
case "state_id":
|
||||||
|
const stateMap = this.rootStore?.stateMap;
|
||||||
|
if (!stateMap) break;
|
||||||
|
for (const dataId of dataIdsArray) {
|
||||||
|
const state = stateMap[dataId];
|
||||||
|
if (state && state.name) dataValues.push(state.name.toLocaleLowerCase());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "label_ids":
|
||||||
|
const labelMap = this.rootStore?.labelMap;
|
||||||
|
if (!labelMap) break;
|
||||||
|
for (const dataId of dataIdsArray) {
|
||||||
|
const label = labelMap[dataId];
|
||||||
|
if (label && label.name) dataValues.push(label.name.toLocaleLowerCase());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "assignee_ids":
|
||||||
|
const memberMap = this.rootStore?.memberMap;
|
||||||
|
if (!memberMap) break;
|
||||||
|
for (const dataId of dataIdsArray) {
|
||||||
|
const member = memberMap[dataId];
|
||||||
|
if (memberMap && member.first_name) dataValues.push(member.first_name.toLocaleLowerCase());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDataIdsArray ? (order ? orderBy(dataValues, undefined, [order]) : dataValues) : dataValues[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Method is mainly used to filter out empty values in the begining
|
||||||
|
* @param key key of the value that is to be checked if empty
|
||||||
|
* @param object any object in which the key's value is to be checked
|
||||||
|
* @returns 1 if emoty, 0 if not empty
|
||||||
|
*/
|
||||||
|
getSortOrderToFilterEmptyValues(key: string, object: any) {
|
||||||
|
const value = object?.[key];
|
||||||
|
|
||||||
|
if (typeof value !== "number" && isEmpty(value)) return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
issuesSortWithOrderBy = (issueObject: TIssueMap, key: Partial<TIssueOrderByOptions>): TIssue[] => {
|
issuesSortWithOrderBy = (issueObject: TIssueMap, key: Partial<TIssueOrderByOptions>): TIssue[] => {
|
||||||
let array = values(issueObject);
|
let array = values(issueObject);
|
||||||
array = reverse(sortBy(array, "created_at"));
|
array = orderBy(array, "created_at");
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "sort_order":
|
case "sort_order":
|
||||||
return sortBy(array, "sort_order");
|
return orderBy(array, "sort_order");
|
||||||
|
|
||||||
case "state__name":
|
case "state__name":
|
||||||
return reverse(sortBy(array, "state"));
|
return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]));
|
||||||
case "-state__name":
|
case "-state__name":
|
||||||
return sortBy(array, "state");
|
return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]), ["desc"]);
|
||||||
|
|
||||||
// dates
|
// dates
|
||||||
case "created_at":
|
case "created_at":
|
||||||
return sortBy(array, "created_at");
|
return orderBy(array, "created_at");
|
||||||
case "-created_at":
|
case "-created_at":
|
||||||
return reverse(sortBy(array, "created_at"));
|
return orderBy(array, "created_at", ["desc"]);
|
||||||
|
|
||||||
case "updated_at":
|
case "updated_at":
|
||||||
return sortBy(array, "updated_at");
|
return orderBy(array, "updated_at");
|
||||||
case "-updated_at":
|
case "-updated_at":
|
||||||
return reverse(sortBy(array, "updated_at"));
|
return orderBy(array, "updated_at", ["desc"]);
|
||||||
|
|
||||||
case "start_date":
|
case "start_date":
|
||||||
return sortBy(array, "start_date");
|
return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"]); //preferring sorting based on empty values to always keep the empty values below
|
||||||
case "-start_date":
|
case "-start_date":
|
||||||
return reverse(sortBy(array, "start_date"));
|
return orderBy(
|
||||||
|
array,
|
||||||
|
[this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"], //preferring sorting based on empty values to always keep the empty values below
|
||||||
|
["asc", "desc"]
|
||||||
|
);
|
||||||
|
|
||||||
case "target_date":
|
case "target_date":
|
||||||
return sortBy(array, "target_date");
|
return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"]); //preferring sorting based on empty values to always keep the empty values below
|
||||||
case "-target_date":
|
case "-target_date":
|
||||||
return reverse(sortBy(array, "target_date"));
|
return orderBy(
|
||||||
|
array,
|
||||||
|
[this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"], //preferring sorting based on empty values to always keep the empty values below
|
||||||
|
["asc", "desc"]
|
||||||
|
);
|
||||||
|
|
||||||
// custom
|
// custom
|
||||||
case "priority": {
|
case "priority": {
|
||||||
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
|
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
|
||||||
return reverse(sortBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority)));
|
return orderBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority), ["desc"]);
|
||||||
}
|
}
|
||||||
case "-priority": {
|
case "-priority": {
|
||||||
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
|
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
|
||||||
return sortBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority));
|
return orderBy(array, (_issue: TIssue) => indexOf(sortArray, _issue.priority));
|
||||||
}
|
}
|
||||||
|
|
||||||
// number
|
// number
|
||||||
case "attachment_count":
|
case "attachment_count":
|
||||||
return sortBy(array, "attachment_count");
|
return orderBy(array, "attachment_count");
|
||||||
case "-attachment_count":
|
case "-attachment_count":
|
||||||
return reverse(sortBy(array, "attachment_count"));
|
return orderBy(array, "attachment_count", ["desc"]);
|
||||||
|
|
||||||
case "estimate_point":
|
case "estimate_point":
|
||||||
return sortBy(array, "estimate_point");
|
return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"]); //preferring sorting based on empty values to always keep the empty values below
|
||||||
case "-estimate_point":
|
case "-estimate_point":
|
||||||
return reverse(sortBy(array, "estimate_point"));
|
return orderBy(
|
||||||
|
array,
|
||||||
|
[this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"], //preferring sorting based on empty values to always keep the empty values below
|
||||||
|
["asc", "desc"]
|
||||||
|
);
|
||||||
|
|
||||||
case "link_count":
|
case "link_count":
|
||||||
return sortBy(array, "link_count");
|
return orderBy(array, "link_count");
|
||||||
case "-link_count":
|
case "-link_count":
|
||||||
return reverse(sortBy(array, "link_count"));
|
return orderBy(array, "link_count", ["desc"]);
|
||||||
|
|
||||||
case "sub_issues_count":
|
case "sub_issues_count":
|
||||||
return sortBy(array, "sub_issues_count");
|
return orderBy(array, "sub_issues_count");
|
||||||
case "-sub_issues_count":
|
case "-sub_issues_count":
|
||||||
return reverse(sortBy(array, "sub_issues_count"));
|
return orderBy(array, "sub_issues_count", ["desc"]);
|
||||||
|
|
||||||
// Array
|
// Array
|
||||||
case "labels__name":
|
case "labels__name":
|
||||||
return reverse(sortBy(array, "labels"));
|
return orderBy(array, [
|
||||||
|
this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||||
|
(issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "asc"),
|
||||||
|
]);
|
||||||
case "-labels__name":
|
case "-labels__name":
|
||||||
return sortBy(array, "labels");
|
return orderBy(
|
||||||
|
array,
|
||||||
|
[
|
||||||
|
this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||||
|
(issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "desc"),
|
||||||
|
],
|
||||||
|
["asc", "desc"]
|
||||||
|
);
|
||||||
|
|
||||||
case "assignees__first_name":
|
case "assignees__first_name":
|
||||||
return reverse(sortBy(array, "assignees"));
|
return orderBy(array, [
|
||||||
|
this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||||
|
(issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "asc"),
|
||||||
|
]);
|
||||||
case "-assignees__first_name":
|
case "-assignees__first_name":
|
||||||
return sortBy(array, "assignees");
|
return orderBy(
|
||||||
|
array,
|
||||||
|
[
|
||||||
|
this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||||
|
(issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "desc"),
|
||||||
|
],
|
||||||
|
["asc", "desc"]
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return array;
|
return array;
|
||||||
|
@ -4,7 +4,7 @@ import isEmpty from "lodash/isEmpty";
|
|||||||
import { RootStore } from "../root.store";
|
import { RootStore } from "../root.store";
|
||||||
import { IStateStore, StateStore } from "../state.store";
|
import { IStateStore, StateStore } from "../state.store";
|
||||||
// issues data store
|
// issues data store
|
||||||
import { IState } from "@plane/types";
|
import { IIssueLabel, IProject, IState, IUserLite } from "@plane/types";
|
||||||
import { IIssueStore, IssueStore } from "./issue.store";
|
import { IIssueStore, IssueStore } from "./issue.store";
|
||||||
import { IIssueDetail, IssueDetail } from "./issue-details/root.store";
|
import { IIssueDetail, IssueDetail } from "./issue-details/root.store";
|
||||||
import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace";
|
import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace";
|
||||||
@ -22,6 +22,7 @@ import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedI
|
|||||||
import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft";
|
import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft";
|
||||||
import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store";
|
import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store";
|
||||||
import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store";
|
import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store";
|
||||||
|
import { IWorkspaceMembership } from "store/member/workspace-member.store";
|
||||||
|
|
||||||
export interface IIssueRootStore {
|
export interface IIssueRootStore {
|
||||||
currentUserId: string | undefined;
|
currentUserId: string | undefined;
|
||||||
@ -32,11 +33,12 @@ export interface IIssueRootStore {
|
|||||||
viewId: string | undefined;
|
viewId: string | undefined;
|
||||||
globalViewId: string | undefined; // all issues view id
|
globalViewId: string | undefined; // all issues view id
|
||||||
userId: string | undefined; // user profile detail Id
|
userId: string | undefined; // user profile detail Id
|
||||||
states: string[] | undefined;
|
stateMap: Record<string, IState> | undefined;
|
||||||
stateDetails: IState[] | undefined;
|
stateDetails: IState[] | undefined;
|
||||||
labels: string[] | undefined;
|
labelMap: Record<string, IIssueLabel> | undefined;
|
||||||
members: string[] | undefined;
|
workSpaceMemberRolesMap: Record<string, IWorkspaceMembership> | undefined;
|
||||||
projects: string[] | undefined;
|
memberMap: Record<string, IUserLite> | undefined;
|
||||||
|
projectMap: Record<string, IProject> | undefined;
|
||||||
|
|
||||||
rootStore: RootStore;
|
rootStore: RootStore;
|
||||||
|
|
||||||
@ -83,11 +85,12 @@ export class IssueRootStore implements IIssueRootStore {
|
|||||||
viewId: string | undefined = undefined;
|
viewId: string | undefined = undefined;
|
||||||
globalViewId: string | undefined = undefined;
|
globalViewId: string | undefined = undefined;
|
||||||
userId: string | undefined = undefined;
|
userId: string | undefined = undefined;
|
||||||
states: string[] | undefined = undefined;
|
stateMap: Record<string, IState> | undefined = undefined;
|
||||||
stateDetails: IState[] | undefined = undefined;
|
stateDetails: IState[] | undefined = undefined;
|
||||||
labels: string[] | undefined = undefined;
|
labelMap: Record<string, IIssueLabel> | undefined = undefined;
|
||||||
members: string[] | undefined = undefined;
|
workSpaceMemberRolesMap: Record<string, IWorkspaceMembership> | undefined = undefined;
|
||||||
projects: string[] | undefined = undefined;
|
memberMap: Record<string, IUserLite> | undefined = undefined;
|
||||||
|
projectMap: Record<string, IProject> | undefined = undefined;
|
||||||
|
|
||||||
rootStore: RootStore;
|
rootStore: RootStore;
|
||||||
|
|
||||||
@ -133,11 +136,12 @@ export class IssueRootStore implements IIssueRootStore {
|
|||||||
viewId: observable.ref,
|
viewId: observable.ref,
|
||||||
userId: observable.ref,
|
userId: observable.ref,
|
||||||
globalViewId: observable.ref,
|
globalViewId: observable.ref,
|
||||||
states: observable,
|
stateMap: observable,
|
||||||
stateDetails: observable,
|
stateDetails: observable,
|
||||||
labels: observable,
|
labelMap: observable,
|
||||||
members: observable,
|
memberMap: observable,
|
||||||
projects: observable,
|
workSpaceMemberRolesMap: observable,
|
||||||
|
projectMap: observable,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = rootStore;
|
this.rootStore = rootStore;
|
||||||
@ -151,13 +155,14 @@ export class IssueRootStore implements IIssueRootStore {
|
|||||||
if (rootStore.app.router.viewId) this.viewId = rootStore.app.router.viewId;
|
if (rootStore.app.router.viewId) this.viewId = rootStore.app.router.viewId;
|
||||||
if (rootStore.app.router.globalViewId) this.globalViewId = rootStore.app.router.globalViewId;
|
if (rootStore.app.router.globalViewId) this.globalViewId = rootStore.app.router.globalViewId;
|
||||||
if (rootStore.app.router.userId) this.userId = rootStore.app.router.userId;
|
if (rootStore.app.router.userId) this.userId = rootStore.app.router.userId;
|
||||||
if (!isEmpty(rootStore?.state?.stateMap)) this.states = Object.keys(rootStore?.state?.stateMap);
|
if (!isEmpty(rootStore?.state?.stateMap)) this.stateMap = rootStore?.state?.stateMap;
|
||||||
if (!isEmpty(rootStore?.state?.projectStates)) this.stateDetails = rootStore?.state?.projectStates;
|
if (!isEmpty(rootStore?.state?.projectStates)) this.stateDetails = rootStore?.state?.projectStates;
|
||||||
if (!isEmpty(rootStore?.label?.labelMap)) this.labels = Object.keys(rootStore?.label?.labelMap);
|
if (!isEmpty(rootStore?.label?.labelMap)) this.labelMap = rootStore?.label?.labelMap;
|
||||||
if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap))
|
if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap))
|
||||||
this.members = Object.keys(rootStore?.memberRoot?.workspace?.workspaceMemberMap);
|
this.workSpaceMemberRolesMap = rootStore?.memberRoot?.workspace?.memberMap || undefined;
|
||||||
|
if (!isEmpty(rootStore?.memberRoot?.memberMap)) this.memberMap = rootStore?.memberRoot?.memberMap || undefined;
|
||||||
if (!isEmpty(rootStore?.projectRoot?.project?.projectMap))
|
if (!isEmpty(rootStore?.projectRoot?.project?.projectMap))
|
||||||
this.projects = Object.keys(rootStore?.projectRoot?.project?.projectMap);
|
this.projectMap = rootStore?.projectRoot?.project?.projectMap;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.issues = new IssueStore();
|
this.issues = new IssueStore();
|
||||||
|
@ -26,6 +26,7 @@ export interface IWorkspaceMemberStore {
|
|||||||
// computed
|
// computed
|
||||||
workspaceMemberIds: string[] | null;
|
workspaceMemberIds: string[] | null;
|
||||||
workspaceMemberInvitationIds: string[] | null;
|
workspaceMemberInvitationIds: string[] | null;
|
||||||
|
memberMap: Record<string, IWorkspaceMembership> | null;
|
||||||
// computed actions
|
// computed actions
|
||||||
getSearchedWorkspaceMemberIds: (searchQuery: string) => string[] | null;
|
getSearchedWorkspaceMemberIds: (searchQuery: string) => string[] | null;
|
||||||
getSearchedWorkspaceInvitationIds: (searchQuery: string) => string[] | null;
|
getSearchedWorkspaceInvitationIds: (searchQuery: string) => string[] | null;
|
||||||
@ -68,6 +69,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
|||||||
// computed
|
// computed
|
||||||
workspaceMemberIds: computed,
|
workspaceMemberIds: computed,
|
||||||
workspaceMemberInvitationIds: computed,
|
workspaceMemberInvitationIds: computed,
|
||||||
|
memberMap: computed,
|
||||||
// actions
|
// actions
|
||||||
fetchWorkspaceMembers: action,
|
fetchWorkspaceMembers: action,
|
||||||
updateMember: action,
|
updateMember: action,
|
||||||
@ -100,6 +102,12 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
|||||||
return memberIds;
|
return memberIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get memberMap() {
|
||||||
|
const workspaceSlug = this.routerStore.workspaceSlug;
|
||||||
|
if (!workspaceSlug) return null;
|
||||||
|
return this.workspaceMemberMap?.[workspaceSlug] ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
get workspaceMemberInvitationIds() {
|
get workspaceMemberInvitationIds() {
|
||||||
const workspaceSlug = this.routerStore.workspaceSlug;
|
const workspaceSlug = this.routerStore.workspaceSlug;
|
||||||
if (!workspaceSlug) return null;
|
if (!workspaceSlug) return null;
|
||||||
|
@ -250,6 +250,7 @@ export class UserRootStore implements IUserRootStore {
|
|||||||
this.isUserLoggedIn = false;
|
this.isUserLoggedIn = false;
|
||||||
});
|
});
|
||||||
this.membership = new UserMembershipStore(this.rootStore);
|
this.membership = new UserMembershipStore(this.rootStore);
|
||||||
|
this.rootStore.eventTracker.resetSession();
|
||||||
this.rootStore.resetOnSignout();
|
this.rootStore.resetOnSignout();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -264,6 +265,7 @@ export class UserRootStore implements IUserRootStore {
|
|||||||
this.isUserLoggedIn = false;
|
this.isUserLoggedIn = false;
|
||||||
});
|
});
|
||||||
this.membership = new UserMembershipStore(this.rootStore);
|
this.membership = new UserMembershipStore(this.rootStore);
|
||||||
|
this.rootStore.eventTracker.resetSession();
|
||||||
this.rootStore.resetOnSignout();
|
this.rootStore.resetOnSignout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user