feat: init
This commit is contained in:
429
apps/web/helpers/authentication.helper.tsx
Normal file
429
apps/web/helpers/authentication.helper.tsx
Normal file
@@ -0,0 +1,429 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
// plane imports
|
||||
import { SUPPORT_EMAIL } from "@plane/constants";
|
||||
|
||||
export enum EPageTypes {
|
||||
PUBLIC = "PUBLIC",
|
||||
NON_AUTHENTICATED = "NON_AUTHENTICATED",
|
||||
SET_PASSWORD = "SET_PASSWORD",
|
||||
ONBOARDING = "ONBOARDING",
|
||||
AUTHENTICATED = "AUTHENTICATED",
|
||||
}
|
||||
|
||||
export enum EAuthModes {
|
||||
SIGN_IN = "SIGN_IN",
|
||||
SIGN_UP = "SIGN_UP",
|
||||
}
|
||||
|
||||
export enum EAuthSteps {
|
||||
EMAIL = "EMAIL",
|
||||
PASSWORD = "PASSWORD",
|
||||
UNIQUE_CODE = "UNIQUE_CODE",
|
||||
}
|
||||
|
||||
export enum EErrorAlertType {
|
||||
BANNER_ALERT = "BANNER_ALERT",
|
||||
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL = "INLINE_EMAIL",
|
||||
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||
}
|
||||
|
||||
export enum EAuthenticationErrorCodes {
|
||||
// Global
|
||||
INSTANCE_NOT_CONFIGURED = "5000",
|
||||
INVALID_EMAIL = "5005",
|
||||
EMAIL_REQUIRED = "5010",
|
||||
SIGNUP_DISABLED = "5015",
|
||||
MAGIC_LINK_LOGIN_DISABLED = "5016",
|
||||
PASSWORD_LOGIN_DISABLED = "5018",
|
||||
USER_ACCOUNT_DEACTIVATED = "5019",
|
||||
// Password strength
|
||||
INVALID_PASSWORD = "5020",
|
||||
SMTP_NOT_CONFIGURED = "5025",
|
||||
// Sign Up
|
||||
USER_ALREADY_EXIST = "5030",
|
||||
AUTHENTICATION_FAILED_SIGN_UP = "5035",
|
||||
REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5040",
|
||||
INVALID_EMAIL_SIGN_UP = "5045",
|
||||
INVALID_EMAIL_MAGIC_SIGN_UP = "5050",
|
||||
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055",
|
||||
// Sign In
|
||||
USER_DOES_NOT_EXIST = "5060",
|
||||
AUTHENTICATION_FAILED_SIGN_IN = "5065",
|
||||
REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070",
|
||||
INVALID_EMAIL_SIGN_IN = "5075",
|
||||
INVALID_EMAIL_MAGIC_SIGN_IN = "5080",
|
||||
MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED = "5085",
|
||||
// Both Sign in and Sign up for magic
|
||||
INVALID_MAGIC_CODE_SIGN_IN = "5090",
|
||||
INVALID_MAGIC_CODE_SIGN_UP = "5092",
|
||||
EXPIRED_MAGIC_CODE_SIGN_IN = "5095",
|
||||
EXPIRED_MAGIC_CODE_SIGN_UP = "5097",
|
||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100",
|
||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102",
|
||||
// Oauth
|
||||
OAUTH_NOT_CONFIGURED = "5104",
|
||||
GOOGLE_NOT_CONFIGURED = "5105",
|
||||
GITHUB_NOT_CONFIGURED = "5110",
|
||||
GITLAB_NOT_CONFIGURED = "5111",
|
||||
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
|
||||
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
|
||||
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
|
||||
// Reset Password
|
||||
INVALID_PASSWORD_TOKEN = "5125",
|
||||
EXPIRED_PASSWORD_TOKEN = "5130",
|
||||
// Change password
|
||||
INCORRECT_OLD_PASSWORD = "5135",
|
||||
MISSING_PASSWORD = "5138",
|
||||
INVALID_NEW_PASSWORD = "5140",
|
||||
// set password
|
||||
PASSWORD_ALREADY_SET = "5145",
|
||||
// Admin
|
||||
ADMIN_ALREADY_EXIST = "5150",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155",
|
||||
INVALID_ADMIN_EMAIL = "5160",
|
||||
INVALID_ADMIN_PASSWORD = "5165",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD = "5170",
|
||||
ADMIN_AUTHENTICATION_FAILED = "5175",
|
||||
ADMIN_USER_ALREADY_EXIST = "5180",
|
||||
ADMIN_USER_DOES_NOT_EXIST = "5185",
|
||||
ADMIN_USER_DEACTIVATED = "5190",
|
||||
// Rate limit
|
||||
RATE_LIMIT_EXCEEDED = "5900",
|
||||
}
|
||||
|
||||
export type TAuthErrorInfo = {
|
||||
type: EErrorAlertType;
|
||||
code: EAuthenticationErrorCodes;
|
||||
title: string;
|
||||
message: ReactNode;
|
||||
};
|
||||
|
||||
const errorCodeMessages: {
|
||||
[key in EAuthenticationErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
|
||||
} = {
|
||||
// global
|
||||
[EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED]: {
|
||||
title: `Instance not configured`,
|
||||
message: () => `Instance not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EMAIL_REQUIRED]: {
|
||||
title: `Email required`,
|
||||
message: () => `Email required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.SIGNUP_DISABLED]: {
|
||||
title: `Sign up disabled`,
|
||||
message: () => `Sign up disabled. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.MAGIC_LINK_LOGIN_DISABLED]: {
|
||||
title: `Magic link login disabled`,
|
||||
message: () => `Magic link login disabled. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.PASSWORD_LOGIN_DISABLED]: {
|
||||
title: `Password login disabled`,
|
||||
message: () => `Password login disabled. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
|
||||
title: `User account deactivated`,
|
||||
message: () => `User account deactivated. Please contact ${!!SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_PASSWORD]: {
|
||||
title: `Invalid password`,
|
||||
message: () => `Invalid password. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED]: {
|
||||
title: `SMTP not configured`,
|
||||
message: () => `SMTP not configured. Please contact your administrator.`,
|
||||
},
|
||||
|
||||
// sign up
|
||||
[EAuthenticationErrorCodes.USER_ALREADY_EXIST]: {
|
||||
title: `User already exists`,
|
||||
message: (email = undefined) => (
|
||||
<div>
|
||||
Your account is already registered.
|
||||
<Link
|
||||
className="underline underline-offset-4 font-medium hover:font-bold transition-all"
|
||||
href={`/sign-in${email ? `?email=${encodeURIComponent(email)}` : ``}`}
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
now.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP]: {
|
||||
title: `Email and password required`,
|
||||
message: () => `Email and password required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Authentication failed. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED]: {
|
||||
title: `Email and code required`,
|
||||
message: () => `Email and code required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
|
||||
[EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: {
|
||||
title: `User does not exist`,
|
||||
message: (email = undefined) => (
|
||||
<div>
|
||||
No account found.
|
||||
<Link
|
||||
className="underline underline-offset-4 font-medium hover:font-bold transition-all"
|
||||
href={`/${email ? `?email=${encodeURIComponent(email)}` : ``}`}
|
||||
>
|
||||
Create one
|
||||
</Link>
|
||||
to get started.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN]: {
|
||||
title: `Email and password required`,
|
||||
message: () => `Email and password required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Authentication failed. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED]: {
|
||||
title: `Email and code required`,
|
||||
message: () => `Email and code required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
|
||||
// Both Sign in and Sign up
|
||||
[EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Invalid magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Invalid magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN]: {
|
||||
title: `Expired magic code`,
|
||||
message: () => `Expired magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP]: {
|
||||
title: `Expired magic code`,
|
||||
message: () => `Expired magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN]: {
|
||||
title: `Expired magic code`,
|
||||
message: () => `Expired magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP]: {
|
||||
title: `Expired magic code`,
|
||||
message: () => `Expired magic code. Please try again.`,
|
||||
},
|
||||
|
||||
// Oauth
|
||||
[EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: {
|
||||
title: `OAuth not configured`,
|
||||
message: () => `OAuth not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
|
||||
title: `Google not configured`,
|
||||
message: () => `Google not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED]: {
|
||||
title: `GitHub not configured`,
|
||||
message: () => `GitHub not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED]: {
|
||||
title: `GitLab not configured`,
|
||||
message: () => `GitLab not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
|
||||
title: `Google OAuth provider error`,
|
||||
message: () => `Google OAuth provider error. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR]: {
|
||||
title: `GitHub OAuth provider error`,
|
||||
message: () => `GitHub OAuth provider error. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR]: {
|
||||
title: `GitLab OAuth provider error`,
|
||||
message: () => `GitLab OAuth provider error. Please try again.`,
|
||||
},
|
||||
|
||||
// Reset Password
|
||||
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
|
||||
title: `Invalid password token`,
|
||||
message: () => `Invalid password token.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN]: {
|
||||
title: `Expired password token`,
|
||||
message: () => `Expired password token. Please try again.`,
|
||||
},
|
||||
|
||||
// Change password
|
||||
[EAuthenticationErrorCodes.MISSING_PASSWORD]: {
|
||||
title: `Password required`,
|
||||
message: () => `Password required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD]: {
|
||||
title: `Incorrect old password`,
|
||||
message: () => `Incorrect old password. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_NEW_PASSWORD]: {
|
||||
title: `Invalid new password`,
|
||||
message: () => `Invalid new password. Please try again.`,
|
||||
},
|
||||
|
||||
// set password
|
||||
[EAuthenticationErrorCodes.PASSWORD_ALREADY_SET]: {
|
||||
title: `Password already set`,
|
||||
message: () => `Password already set. Please try again.`,
|
||||
},
|
||||
|
||||
// admin
|
||||
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
|
||||
title: `Admin already exists`,
|
||||
message: () => `Admin already exists. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
|
||||
title: `Email, password and first name required`,
|
||||
message: () => `Email, password and first name required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
|
||||
title: `Invalid admin email`,
|
||||
message: () => `Invalid admin email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
|
||||
title: `Invalid admin password`,
|
||||
message: () => `Invalid admin password. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
|
||||
title: `Email and password required`,
|
||||
message: () => `Email and password required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Authentication failed. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
|
||||
title: `Admin user already exists`,
|
||||
message: () => (
|
||||
<div>
|
||||
Admin user already exists.
|
||||
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
|
||||
Sign In
|
||||
</Link>
|
||||
now.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
|
||||
title: `Admin user does not exist`,
|
||||
message: () => (
|
||||
<div>
|
||||
Admin user does not exist.
|
||||
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
|
||||
Sign In
|
||||
</Link>
|
||||
now.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED]: {
|
||||
title: `Admin user deactivated`,
|
||||
message: () => <div>Your account is deactivated</div>,
|
||||
},
|
||||
[EAuthenticationErrorCodes.RATE_LIMIT_EXCEEDED]: {
|
||||
title: "",
|
||||
message: () => `Rate limit exceeded. Please try again later.`,
|
||||
},
|
||||
};
|
||||
|
||||
export const authErrorHandler = (
|
||||
errorCode: EAuthenticationErrorCodes,
|
||||
email?: string | undefined
|
||||
): TAuthErrorInfo | undefined => {
|
||||
const bannerAlertErrorCodes = [
|
||||
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL,
|
||||
EAuthenticationErrorCodes.EMAIL_REQUIRED,
|
||||
EAuthenticationErrorCodes.SIGNUP_DISABLED,
|
||||
EAuthenticationErrorCodes.MAGIC_LINK_LOGIN_DISABLED,
|
||||
EAuthenticationErrorCodes.PASSWORD_LOGIN_DISABLED,
|
||||
EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED,
|
||||
EAuthenticationErrorCodes.INVALID_PASSWORD,
|
||||
EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.USER_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP,
|
||||
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP,
|
||||
EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED,
|
||||
EAuthenticationErrorCodes.USER_DOES_NOT_EXIST,
|
||||
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN,
|
||||
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN,
|
||||
EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED,
|
||||
EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN,
|
||||
EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP,
|
||||
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN,
|
||||
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
|
||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
|
||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
|
||||
EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
|
||||
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
|
||||
EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR,
|
||||
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
|
||||
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
|
||||
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
|
||||
EAuthenticationErrorCodes.MISSING_PASSWORD,
|
||||
EAuthenticationErrorCodes.INVALID_NEW_PASSWORD,
|
||||
EAuthenticationErrorCodes.PASSWORD_ALREADY_SET,
|
||||
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
|
||||
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
|
||||
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
|
||||
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
|
||||
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
|
||||
EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED,
|
||||
EAuthenticationErrorCodes.RATE_LIMIT_EXCEEDED,
|
||||
];
|
||||
|
||||
if (bannerAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.BANNER_ALERT,
|
||||
code: errorCode,
|
||||
title: errorCodeMessages[errorCode]?.title || "Error",
|
||||
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
return undefined;
|
||||
};
|
||||
95
apps/web/helpers/dashboard.helper.ts
Normal file
95
apps/web/helpers/dashboard.helper.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { endOfMonth, endOfWeek, endOfYear, startOfMonth, startOfWeek, startOfYear } from "date-fns";
|
||||
// helpers
|
||||
// types
|
||||
import { DURATION_FILTER_OPTIONS, EDurationFilters } from "@plane/constants";
|
||||
import type { TIssuesListTypes } from "@plane/types";
|
||||
// constants
|
||||
import { renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
|
||||
|
||||
// -------------------- DEPRECATED --------------------
|
||||
|
||||
/**
|
||||
* @description returns date range based on the duration filter
|
||||
* @param duration
|
||||
*/
|
||||
export const getCustomDates = (duration: EDurationFilters, customDates: string[]): string => {
|
||||
const today = new Date();
|
||||
let firstDay, lastDay;
|
||||
|
||||
switch (duration) {
|
||||
case EDurationFilters.NONE:
|
||||
return "";
|
||||
case EDurationFilters.TODAY:
|
||||
firstDay = renderFormattedPayloadDate(today);
|
||||
lastDay = renderFormattedPayloadDate(today);
|
||||
return `${firstDay};after,${lastDay};before`;
|
||||
case EDurationFilters.THIS_WEEK:
|
||||
firstDay = renderFormattedPayloadDate(startOfWeek(today));
|
||||
lastDay = renderFormattedPayloadDate(endOfWeek(today));
|
||||
return `${firstDay};after,${lastDay};before`;
|
||||
case EDurationFilters.THIS_MONTH:
|
||||
firstDay = renderFormattedPayloadDate(startOfMonth(today));
|
||||
lastDay = renderFormattedPayloadDate(endOfMonth(today));
|
||||
return `${firstDay};after,${lastDay};before`;
|
||||
case EDurationFilters.THIS_YEAR:
|
||||
firstDay = renderFormattedPayloadDate(startOfYear(today));
|
||||
lastDay = renderFormattedPayloadDate(endOfYear(today));
|
||||
return `${firstDay};after,${lastDay};before`;
|
||||
case EDurationFilters.CUSTOM:
|
||||
return customDates.join(",");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns redirection filters for the issues list
|
||||
* @param type
|
||||
*/
|
||||
export const getRedirectionFilters = (type: TIssuesListTypes): string => {
|
||||
const today = renderFormattedPayloadDate(new Date());
|
||||
|
||||
const filterParams =
|
||||
type === "pending"
|
||||
? "?state_group=backlog,unstarted,started"
|
||||
: type === "upcoming"
|
||||
? `?target_date=${today};after`
|
||||
: type === "overdue"
|
||||
? `?target_date=${today};before`
|
||||
: "?state_group=completed";
|
||||
|
||||
return filterParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns the tab key based on the duration filter
|
||||
* @param duration
|
||||
* @param tab
|
||||
*/
|
||||
export const getTabKey = (duration: EDurationFilters, tab: TIssuesListTypes | undefined): TIssuesListTypes => {
|
||||
if (!tab) return "completed";
|
||||
|
||||
if (tab === "completed") return tab;
|
||||
|
||||
if (duration === "none") return "pending";
|
||||
else {
|
||||
if (["upcoming", "overdue"].includes(tab)) return tab;
|
||||
else return "upcoming";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns the label for the duration filter dropdown
|
||||
* @param duration
|
||||
* @param customDates
|
||||
*/
|
||||
export const getDurationFilterDropdownLabel = (duration: EDurationFilters, customDates: string[]): string => {
|
||||
if (duration !== "custom") return DURATION_FILTER_OPTIONS.find((option) => option.key === duration)?.label ?? "";
|
||||
else {
|
||||
const afterDate = customDates.find((date) => date.includes("after"))?.split(";")[0];
|
||||
const beforeDate = customDates.find((date) => date.includes("before"))?.split(";")[0];
|
||||
|
||||
if (afterDate && beforeDate) return `${renderFormattedDate(afterDate)} - ${renderFormattedDate(beforeDate)}`;
|
||||
else if (afterDate) return `After ${renderFormattedDate(afterDate)}`;
|
||||
else if (beforeDate) return `Before ${renderFormattedDate(beforeDate)}`;
|
||||
else return "";
|
||||
}
|
||||
};
|
||||
23
apps/web/helpers/emoji.helper.tsx
Normal file
23
apps/web/helpers/emoji.helper.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Renders an emoji or icon
|
||||
* @param {string | { name: string; color: string }} emoji - The emoji or icon to render
|
||||
* @returns {React.ReactNode} The rendered emoji or icon
|
||||
*/
|
||||
export const renderEmoji = (
|
||||
emoji:
|
||||
| string
|
||||
| {
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
): React.ReactNode => {
|
||||
if (!emoji) return;
|
||||
|
||||
if (typeof emoji === "object")
|
||||
return (
|
||||
<span style={{ fontSize: "16px", color: emoji.color }} className="material-symbols-rounded">
|
||||
{emoji.name}
|
||||
</span>
|
||||
);
|
||||
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
|
||||
};
|
||||
146
apps/web/helpers/event-tracker.helper.ts
Normal file
146
apps/web/helpers/event-tracker.helper.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import posthog from "posthog-js";
|
||||
|
||||
export type TEventState = "SUCCESS" | "ERROR";
|
||||
export type TElementContext = Record<string, any>;
|
||||
export type TEventContext = Record<string, any>;
|
||||
export type TInteractionType = "clicked" | "viewed" | "hovered";
|
||||
|
||||
/**
|
||||
* Join a event group in PostHog
|
||||
* @param groupName - The name of the group
|
||||
* @param groupId - The ID of the group
|
||||
* @param properties - The properties of the group
|
||||
*/
|
||||
export const joinEventGroup = (groupName: string, groupId: string, properties: Record<string, any>) => {
|
||||
posthog?.group(groupName, groupId, properties);
|
||||
};
|
||||
|
||||
type TCaptureElementParams = {
|
||||
elementName: string;
|
||||
interaction_type: TInteractionType;
|
||||
context?: TElementContext;
|
||||
};
|
||||
|
||||
/**
|
||||
* Capture UI element interactions (clicks, hovers, views, etc.)
|
||||
* This helps understand user behavior and interaction patterns
|
||||
*
|
||||
* @param element - Generic UI element type
|
||||
* @param context - Context about where and why the interaction happened
|
||||
*/
|
||||
const captureElement = (params: TCaptureElementParams) => {
|
||||
const { elementName, interaction_type, context } = params;
|
||||
if (!posthog) return;
|
||||
|
||||
const elementEvent = `${elementName}_${interaction_type}`;
|
||||
|
||||
const payload = {
|
||||
element_type: elementName,
|
||||
timestamp: new Date().toISOString(),
|
||||
...context,
|
||||
};
|
||||
|
||||
posthog.capture(elementEvent, payload);
|
||||
};
|
||||
|
||||
type TCaptureClickParams = Omit<TCaptureElementParams, "interaction_type">;
|
||||
/**
|
||||
* Capture click events
|
||||
* @param element - The element that was clicked
|
||||
* @param context - Additional context
|
||||
*/
|
||||
export const captureClick = (params: TCaptureClickParams) => {
|
||||
captureElement({ ...params, interaction_type: "clicked" });
|
||||
};
|
||||
|
||||
type TCaptureViewParams = Omit<TCaptureElementParams, "interaction_type">;
|
||||
/**
|
||||
* Capture view events
|
||||
* @param element - The element that was viewed
|
||||
* @param context - Additional context
|
||||
*/
|
||||
export const captureView = (params: TCaptureViewParams) => {
|
||||
captureElement({ ...params, interaction_type: "viewed" });
|
||||
};
|
||||
|
||||
type TCaptureHoverParams = Omit<TCaptureElementParams, "interaction_type">;
|
||||
/**
|
||||
* Capture hover events
|
||||
* @param element - The element that was hovered
|
||||
* @param context - Additional context
|
||||
*/
|
||||
export const captureHover = (params: TCaptureHoverParams) => {
|
||||
captureElement({ ...params, interaction_type: "hovered" });
|
||||
};
|
||||
|
||||
type TCaptureEventParams = {
|
||||
eventName: string;
|
||||
payload?: Record<string, any>;
|
||||
context?: TEventContext;
|
||||
state: TEventState;
|
||||
};
|
||||
/**
|
||||
* Capture business events (outcomes, state changes, etc.)
|
||||
* This helps understand business metrics and conversion rates
|
||||
*
|
||||
* @param eventName - Business event name (e.g., "cycle_created", "project_updated")
|
||||
* @param state - Success or error state
|
||||
* @param payload - Event-specific data
|
||||
* @param context - Additional context
|
||||
*/
|
||||
const captureEvent = (params: TCaptureEventParams) => {
|
||||
const { eventName, payload, context, state } = params;
|
||||
if (!posthog) return;
|
||||
|
||||
const finalPayload = {
|
||||
...context,
|
||||
...payload,
|
||||
state,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
posthog.capture(eventName, finalPayload);
|
||||
};
|
||||
|
||||
type TCaptureSuccessParams = Omit<TCaptureEventParams, "state">;
|
||||
/**
|
||||
* Capture success events
|
||||
* @param eventName - The name of the event
|
||||
* @param payload - Additional payload
|
||||
* @param context - Additional context
|
||||
*/
|
||||
export const captureSuccess = (params: TCaptureSuccessParams) => {
|
||||
captureEvent({ ...params, state: "SUCCESS" });
|
||||
};
|
||||
|
||||
type TCaptureErrorParams = Omit<TCaptureEventParams, "state"> & {
|
||||
error?: Error | string;
|
||||
};
|
||||
/**
|
||||
* Capture error events
|
||||
* @param eventName - The name of the event
|
||||
* @param error - The error object
|
||||
* @param payload - Additional payload
|
||||
* @param context - Additional context
|
||||
*/
|
||||
export const captureError = (params: TCaptureErrorParams) => {
|
||||
captureEvent({ ...params, state: "ERROR", payload: { ...params.payload, error: params.error } });
|
||||
};
|
||||
|
||||
type TCaptureElementAndEventParams = {
|
||||
element: Omit<TCaptureElementParams, "interaction_type">;
|
||||
event: TCaptureEventParams;
|
||||
};
|
||||
/**
|
||||
* Capture both element interaction and business event together
|
||||
* @param element - The element that was interacted with
|
||||
* @param event - The business event that was triggered
|
||||
*/
|
||||
export const captureElementAndEvent = (params: TCaptureElementAndEventParams) => {
|
||||
const { element, event } = params;
|
||||
// Capture the element interaction first
|
||||
captureElement({ ...element, interaction_type: "clicked" });
|
||||
|
||||
// Then capture the business event
|
||||
captureEvent(event);
|
||||
};
|
||||
26
apps/web/helpers/graph.helper.ts
Normal file
26
apps/web/helpers/graph.helper.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// ------------ DEPRECATED (Use re-charts and its helpers instead) ------------
|
||||
|
||||
export const generateYAxisTickValues = (data: number[]) => {
|
||||
if (!data || !Array.isArray(data) || data.length === 0) return [];
|
||||
|
||||
const minValue = 0;
|
||||
const maxValue = Math.max(...data);
|
||||
|
||||
const valueRange = maxValue - minValue;
|
||||
|
||||
let tickInterval = 1;
|
||||
|
||||
if (valueRange < 10) tickInterval = 1;
|
||||
else if (valueRange < 20) tickInterval = 2;
|
||||
else if (valueRange < 50) tickInterval = 5;
|
||||
else tickInterval = (Math.ceil(valueRange / 100) * 100) / 10;
|
||||
|
||||
const tickValues: number[] = [];
|
||||
let tickValue = minValue;
|
||||
while (tickValue <= maxValue) {
|
||||
tickValues.push(tickValue);
|
||||
tickValue += tickInterval;
|
||||
}
|
||||
|
||||
return tickValues;
|
||||
};
|
||||
27
apps/web/helpers/react-hook-form.helper.ts
Normal file
27
apps/web/helpers/react-hook-form.helper.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { FieldError, FieldValues } from "react-hook-form";
|
||||
|
||||
/**
|
||||
* Get a nested error from a form's errors object
|
||||
* @param errors - The form's errors object
|
||||
* @param path - The path to the error
|
||||
* @returns The error or undefined if not found
|
||||
*/
|
||||
export const getNestedError = <T extends FieldValues>(errors: T, path: string): FieldError | undefined => {
|
||||
const keys = path.split(".");
|
||||
let current: unknown = errors;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === "object" && key in current) {
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the final value is a FieldError
|
||||
if (current && typeof current === "object" && "message" in current) {
|
||||
return current as FieldError;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
17
apps/web/helpers/views.helper.ts
Normal file
17
apps/web/helpers/views.helper.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { Globe2, Lock } from "lucide-react";
|
||||
import { VIEW_ACCESS_SPECIFIERS as VIEW_ACCESS_SPECIFIERS_CONSTANTS } from "@plane/constants";
|
||||
import { EViewAccess } from "@plane/types";
|
||||
|
||||
const VIEW_ACCESS_ICONS = {
|
||||
[EViewAccess.PUBLIC]: Globe2,
|
||||
[EViewAccess.PRIVATE]: Lock,
|
||||
};
|
||||
export const VIEW_ACCESS_SPECIFIERS: {
|
||||
key: EViewAccess;
|
||||
i18n_label: string;
|
||||
icon: LucideIcon;
|
||||
}[] = VIEW_ACCESS_SPECIFIERS_CONSTANTS.map((option) => ({
|
||||
...option,
|
||||
icon: VIEW_ACCESS_ICONS[option.key as keyof typeof VIEW_ACCESS_ICONS],
|
||||
}));
|
||||
Reference in New Issue
Block a user