Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5e1619c98 |
@@ -14,10 +14,10 @@ jobs:
|
||||
space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
|
||||
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files_yaml: |
|
||||
apiserver:
|
||||
@@ -49,9 +49,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.get-changed-files.outputs.apiserver_changed == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.x" # Specify the Python version you need
|
||||
- name: Install Pylint
|
||||
@@ -66,9 +66,9 @@ jobs:
|
||||
if: needs.get-changed-files.outputs.admin_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
@@ -79,9 +79,9 @@ jobs:
|
||||
if: needs.get-changed-files.outputs.space_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
@@ -92,9 +92,9 @@ jobs:
|
||||
if: needs.get-changed-files.outputs.web_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
@@ -104,9 +104,9 @@ jobs:
|
||||
needs: lint-admin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
@@ -116,9 +116,9 @@ jobs:
|
||||
needs: lint-space
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
@@ -128,9 +128,9 @@ jobs:
|
||||
needs: lint-web
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
|
||||
@@ -7,10 +7,9 @@ import { Transition } from "@headlessui/react";
|
||||
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
import { useInstance, useTheme } from "@/hooks/store";
|
||||
// assets
|
||||
import packageJson from "package.json";
|
||||
import { WEB_BASE_URL } from "@/helpers/common.helper";
|
||||
|
||||
const helpOptions = [
|
||||
{
|
||||
@@ -31,6 +30,8 @@ const helpOptions = [
|
||||
];
|
||||
|
||||
export const HelpSection: FC = observer(() => {
|
||||
// hooks
|
||||
const { instance } = useInstance();
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
// store
|
||||
@@ -38,7 +39,7 @@ export const HelpSection: FC = observer(() => {
|
||||
// refs
|
||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
|
||||
const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export enum EPageTypes {
|
||||
PUBLIC = "PUBLIC",
|
||||
NON_AUTHENTICATED = "NON_AUTHENTICATED",
|
||||
SET_PASSWORD = "SET_PASSWORD",
|
||||
ONBOARDING = "ONBOARDING",
|
||||
AUTHENTICATED = "AUTHENTICATED",
|
||||
"PUBLIC" = "PUBLIC",
|
||||
"NON_AUTHENTICATED" = "NON_AUTHENTICATED",
|
||||
"ONBOARDING" = "ONBOARDING",
|
||||
"AUTHENTICATED" = "AUTHENTICATED",
|
||||
}
|
||||
|
||||
export enum EAuthModes {
|
||||
@@ -20,26 +18,28 @@ export enum EAuthSteps {
|
||||
UNIQUE_CODE = "UNIQUE_CODE",
|
||||
}
|
||||
|
||||
export enum EAuthenticationErrorCodes {
|
||||
INSTANCE_NOT_CONFIGURED = "5000",
|
||||
// Admin
|
||||
ADMIN_ALREADY_EXIST = "5029",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5030",
|
||||
INVALID_ADMIN_EMAIL = "5031",
|
||||
INVALID_ADMIN_PASSWORD = "5032",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD = "5033",
|
||||
ADMIN_AUTHENTICATION_FAILED = "5034",
|
||||
ADMIN_USER_ALREADY_EXIST = "5035",
|
||||
ADMIN_USER_DOES_NOT_EXIST = "5036",
|
||||
}
|
||||
|
||||
export enum EErrorAlertType {
|
||||
BANNER_ALERT = "BANNER_ALERT",
|
||||
TOAST_ALERT = "TOAST_ALERT",
|
||||
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL = "INLINE_EMAIL",
|
||||
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||
}
|
||||
|
||||
export enum EAuthenticationErrorCodes {
|
||||
// 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",
|
||||
}
|
||||
|
||||
export type TAuthErrorInfo = {
|
||||
type: EErrorAlertType;
|
||||
code: EAuthenticationErrorCodes;
|
||||
@@ -50,54 +50,41 @@ export type TAuthErrorInfo = {
|
||||
const errorCodeMessages: {
|
||||
[key in EAuthenticationErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
|
||||
} = {
|
||||
// admin
|
||||
[EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED]: {
|
||||
title: "Instance not configured",
|
||||
message: () => "Please contact your administrator to configure the instance.",
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
|
||||
title: `Admin already exists`,
|
||||
message: () => `Admin already exists. Please try again.`,
|
||||
title: "Admin already exists",
|
||||
message: () => "Admin already exists. Please sign in.",
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
|
||||
title: `Email, password and first name required`,
|
||||
message: () => `Email, password and first name required. Please try again.`,
|
||||
title: "Required",
|
||||
message: () => "Please enter email, password and first name.",
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
|
||||
title: `Invalid admin email`,
|
||||
message: () => `Invalid admin email. Please try again.`,
|
||||
title: "Invalid email",
|
||||
message: () => "Please enter a valid email.",
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
|
||||
title: `Invalid admin password`,
|
||||
message: () => `Invalid admin password. Please try again.`,
|
||||
title: "Invalid password",
|
||||
message: () => "Password must be at least 8 characters long.",
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
|
||||
title: `Email and password required`,
|
||||
message: () => `Email and password required. Please try again.`,
|
||||
title: "Required",
|
||||
message: () => "Please enter email and password.",
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Authentication failed. Please try again.`,
|
||||
title: "Authentication failed",
|
||||
message: () => "Please check your email and password and 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>
|
||||
),
|
||||
title: "User already exists",
|
||||
message: () => "User already exists. Please sign in.",
|
||||
},
|
||||
[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>
|
||||
),
|
||||
title: "User does not exist",
|
||||
message: () => "User does not exist. Please sign up.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -105,16 +92,28 @@ export const authErrorHandler = (
|
||||
errorCode: EAuthenticationErrorCodes,
|
||||
email?: string | undefined
|
||||
): TAuthErrorInfo | undefined => {
|
||||
const bannerAlertErrorCodes = [
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
|
||||
const toastAlertErrorCodes = [
|
||||
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
|
||||
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
|
||||
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
|
||||
];
|
||||
const bannerAlertErrorCodes = [
|
||||
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
|
||||
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
|
||||
];
|
||||
|
||||
if (toastAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.TOAST_ALERT,
|
||||
code: errorCode,
|
||||
title: errorCodeMessages[errorCode]?.title || "Error",
|
||||
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (bannerAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.BANNER_ALERT,
|
||||
|
||||
+1
-2
@@ -14,7 +14,6 @@
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/constants": "*",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
@@ -26,7 +25,7 @@
|
||||
"mobx-react-lite": "^4.0.5",
|
||||
"next": "^14.2.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "8.4.23",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.51.0",
|
||||
|
||||
@@ -315,7 +315,7 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
).exclude(pk=instance.id).exists():
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
|
||||
@@ -462,7 +462,7 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
).exclude(pk=instance.id).exists():
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
|
||||
@@ -96,7 +96,6 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||
@invalidate_cache(path="/api/instances/", user=False)
|
||||
def create(self, request):
|
||||
try:
|
||||
serializer = WorkSpaceSerializer(data=request.data)
|
||||
@@ -152,12 +151,8 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/workspaces/", multiple=True, user=False
|
||||
)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/settings/", multiple=True, user=False
|
||||
)
|
||||
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True, user=False)
|
||||
@invalidate_cache(path="/api/users/me/settings/", multiple=True, user=False)
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -100,12 +100,6 @@ class Adapter:
|
||||
user.save()
|
||||
Profile.objects.create(user=user)
|
||||
|
||||
if not user.is_active:
|
||||
raise AuthenticationException(
|
||||
AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
|
||||
# Update user details
|
||||
user.last_login_medium = self.provider
|
||||
user.last_active = timezone.now()
|
||||
|
||||
@@ -4,9 +4,6 @@ AUTHENTICATION_ERROR_CODES = {
|
||||
"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,
|
||||
@@ -38,7 +35,6 @@ AUTHENTICATION_ERROR_CODES = {
|
||||
"EXPIRED_PASSWORD_TOKEN": 5130,
|
||||
# Change password
|
||||
"INCORRECT_OLD_PASSWORD": 5135,
|
||||
"MISSING_PASSWORD": 5138,
|
||||
"INVALID_NEW_PASSWORD": 5140,
|
||||
# set passowrd
|
||||
"PASSWORD_ALREADY_SET": 5145,
|
||||
@@ -51,7 +47,6 @@ AUTHENTICATION_ERROR_CODES = {
|
||||
"ADMIN_AUTHENTICATION_FAILED": 5175,
|
||||
"ADMIN_USER_ALREADY_EXIST": 5180,
|
||||
"ADMIN_USER_DOES_NOT_EXIST": 5185,
|
||||
"ADMIN_USER_DEACTIVATED": 5190,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.adapter.credential import CredentialAdapter
|
||||
from plane.db.models import User
|
||||
@@ -8,7 +5,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
class EmailProvider(CredentialAdapter):
|
||||
@@ -27,21 +23,6 @@ class EmailProvider(CredentialAdapter):
|
||||
self.code = code
|
||||
self.is_signup = is_signup
|
||||
|
||||
(ENABLE_EMAIL_PASSWORD,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_EMAIL_PASSWORD",
|
||||
"default": os.environ.get("ENABLE_EMAIL_PASSWORD"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if ENABLE_EMAIL_PASSWORD == "0":
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["ENABLE_EMAIL_PASSWORD"],
|
||||
error_message="ENABLE_EMAIL_PASSWORD",
|
||||
)
|
||||
|
||||
def set_user_data(self):
|
||||
if self.is_signup:
|
||||
# Check if the user already exists
|
||||
|
||||
@@ -26,20 +26,23 @@ class MagicCodeProvider(CredentialAdapter):
|
||||
code=None,
|
||||
):
|
||||
|
||||
(
|
||||
EMAIL_HOST,
|
||||
ENABLE_MAGIC_LINK_LOGIN,
|
||||
) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
|
||||
},
|
||||
]
|
||||
(EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD) = (
|
||||
get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_USER",
|
||||
"default": os.environ.get("EMAIL_HOST_USER"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_PASSWORD",
|
||||
"default": os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if not (EMAIL_HOST):
|
||||
@@ -49,15 +52,6 @@ class MagicCodeProvider(CredentialAdapter):
|
||||
payload={"email": str(self.key)},
|
||||
)
|
||||
|
||||
if ENABLE_MAGIC_LINK_LOGIN == "0":
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"MAGIC_LINK_LOGIN_DISABLED"
|
||||
],
|
||||
error_message="MAGIC_LINK_LOGIN_DISABLED",
|
||||
payload={"email": str(self.key)},
|
||||
)
|
||||
|
||||
super().__init__(request, self.provider)
|
||||
self.key = key
|
||||
self.code = code
|
||||
|
||||
@@ -24,59 +24,62 @@ class EmailCheckSignUpEndpoint(APIView):
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INSTANCE_NOT_CONFIGURED"
|
||||
],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
|
||||
# Validate email
|
||||
validate_email(email)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
# check if the account is the deactivated
|
||||
if not existing_user.is_active:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ALREADY_EXIST"
|
||||
],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
)
|
||||
return Response(
|
||||
{"status": True},
|
||||
status=status.HTTP_200_OK,
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INSTANCE_NOT_CONFIGURED"
|
||||
],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate email
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
raise AuthenticationException(
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
|
||||
error_message="INVALID_EMAIL",
|
||||
)
|
||||
except AuthenticationException as e:
|
||||
return Response(
|
||||
e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response(
|
||||
{"status": True},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class EmailCheckSignInEndpoint(APIView):
|
||||
|
||||
@@ -85,59 +88,61 @@ class EmailCheckSignInEndpoint(APIView):
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INSTANCE_NOT_CONFIGURED"
|
||||
],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
|
||||
# Validate email
|
||||
validate_email(email)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
# If existing user
|
||||
if existing_user:
|
||||
# Raise different exception when user is not active
|
||||
if not existing_user.is_active:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"status": True,
|
||||
"is_password_autoset": existing_user.is_password_autoset,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INSTANCE_NOT_CONFIGURED"
|
||||
],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate email
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
raise AuthenticationException(
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
|
||||
error_message="INVALID_EMAIL",
|
||||
)
|
||||
except AuthenticationException as e:
|
||||
return Response(
|
||||
e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
return Response(
|
||||
{
|
||||
"status": True,
|
||||
"is_password_autoset": existing_user.is_password_autoset,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -90,9 +90,7 @@ class SignInAuthEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
if not User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
@@ -107,22 +105,6 @@ class SignInAuthEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = EmailProvider(
|
||||
request=request, key=email, code=password, is_signup=False
|
||||
@@ -215,26 +197,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
# Existing User
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
|
||||
@@ -95,26 +95,7 @@ class MagicSignInEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
@@ -186,25 +167,8 @@ class MagicSignUpEndpoint(View):
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Existing user
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
if not existing_user:
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
|
||||
@@ -63,13 +63,23 @@ class ForgotPasswordEndpoint(APIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
(EMAIL_HOST,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
]
|
||||
(EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD) = (
|
||||
get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_USER",
|
||||
"default": os.environ.get("EMAIL_HOST_USER"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_PASSWORD",
|
||||
"default": os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if not (EMAIL_HOST):
|
||||
|
||||
@@ -36,59 +36,54 @@ class CSRFTokenEndpoint(APIView):
|
||||
|
||||
class ChangePasswordEndpoint(APIView):
|
||||
def post(self, request):
|
||||
serializer = ChangePasswordSerializer(data=request.data)
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
if serializer.is_valid():
|
||||
if not user.check_password(serializer.data.get("old_password")):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INCORRECT_OLD_PASSWORD"
|
||||
],
|
||||
error_message="INCORRECT_OLD_PASSWORD",
|
||||
payload={"error": "Old password is not correct"},
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
old_password = request.data.get("old_password", False)
|
||||
new_password = request.data.get("new_password", False)
|
||||
# check the password score
|
||||
results = zxcvbn(serializer.data.get("new_password"))
|
||||
if results["score"] < 3:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INVALID_NEW_PASSWORD"
|
||||
],
|
||||
error_message="INVALID_NEW_PASSWORD",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not old_password or not new_password:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MISSING_PASSWORD"],
|
||||
error_message="MISSING_PASSWORD",
|
||||
payload={"error": "Old or new password is missing"},
|
||||
)
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(serializer.data.get("new_password"))
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
user_login(user=user, request=request, is_app=True)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{"message": "Password updated successfully"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
if not user.check_password(old_password):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INCORRECT_OLD_PASSWORD"
|
||||
],
|
||||
error_message="INCORRECT_OLD_PASSWORD",
|
||||
payload={"error": "Old password is not correct"},
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# check the password score
|
||||
results = zxcvbn(new_password)
|
||||
if results["score"] < 3:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INVALID_NEW_PASSWORD"
|
||||
],
|
||||
error_message="INVALID_NEW_PASSWORD",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(new_password)
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
user_login(user=user, request=request, is_app=True)
|
||||
return Response(
|
||||
{"message": "Password updated successfully"},
|
||||
status=status.HTTP_200_OK,
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
|
||||
error_message="INVALID_PASSWORD",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class SetUserPasswordEndpoint(APIView):
|
||||
def post(self, request):
|
||||
|
||||
@@ -68,17 +68,6 @@ class EmailCheckEndpoint(APIView):
|
||||
|
||||
# If existing user
|
||||
if existing_user:
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"existing": True,
|
||||
|
||||
@@ -83,10 +83,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
if not User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
@@ -101,22 +98,6 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = EmailProvider(
|
||||
request=request, key=email, code=password, is_signup=False
|
||||
@@ -204,26 +185,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
|
||||
@@ -90,14 +90,11 @@ class MagicSignInSpaceEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if not User.objects.filter(email=email).exists():
|
||||
params = {
|
||||
"error_code": "USER_DOES_NOT_EXIST",
|
||||
"error_message": "User could not be found with the given email.",
|
||||
}
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
@@ -106,22 +103,6 @@ class MagicSignInSpaceEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Active User
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
provider = MagicCodeProvider(
|
||||
request=request, key=f"magic_{email}", code=code
|
||||
@@ -174,25 +155,8 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
if existing_user:
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django imports
|
||||
from django.views import View
|
||||
from django.contrib.auth import logout
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Module imports
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
|
||||
@@ -40,10 +37,10 @@ class Command(BaseCommand):
|
||||
timeout=30,
|
||||
)
|
||||
# Prepare email details
|
||||
subject = "Test email from Plane"
|
||||
|
||||
html_content = render_to_string("emails/test_email.html")
|
||||
text_content = strip_tags(html_content)
|
||||
subject = "Email Notification from Plane"
|
||||
message = (
|
||||
"This is a sample email notification sent from Plane application."
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Trying to send test email..."))
|
||||
|
||||
@@ -51,14 +48,11 @@ class Command(BaseCommand):
|
||||
try:
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
body=message,
|
||||
from_email=EMAIL_FROM,
|
||||
to=[
|
||||
receiver_email,
|
||||
],
|
||||
to=[receiver_email],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
self.stdout.write(self.style.SUCCESS("Email successfully sent"))
|
||||
except Exception as e:
|
||||
|
||||
@@ -316,20 +316,6 @@ class InstanceAdminSignInEndpoint(View):
|
||||
# Fetch the user
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# is_active
|
||||
if not user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"ADMIN_USER_DEACTIVATED"
|
||||
],
|
||||
error_message="ADMIN_USER_DEACTIVATED",
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Error out if the user is not present
|
||||
if not user:
|
||||
exc = AuthenticationException(
|
||||
|
||||
@@ -38,8 +38,6 @@ class InstanceEndpoint(BaseAPIView):
|
||||
@cache_response(60 * 60 * 2, user=False)
|
||||
def get(self, request):
|
||||
instance = Instance.objects.first()
|
||||
|
||||
print("Instance: ", instance)
|
||||
# get the instance
|
||||
if instance is None:
|
||||
return Response(
|
||||
|
||||
@@ -30,6 +30,19 @@ INTERNAL_IPS = ("127.0.0.1",)
|
||||
MEDIA_URL = "/uploads/"
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") # noqa
|
||||
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost",
|
||||
"http://127.0.0.1",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:3001",
|
||||
"http://127.0.0.1:3001",
|
||||
"http://localhost:3002",
|
||||
"http://127.0.0.1:3002",
|
||||
]
|
||||
CSRF_TRUSTED_ORIGINS = CORS_ALLOWED_ORIGINS
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
LOG_DIR = os.path.join(BASE_DIR, "logs") # noqa
|
||||
|
||||
if not os.path.exists(LOG_DIR):
|
||||
|
||||
@@ -151,10 +151,6 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
|
||||
search_fields = []
|
||||
|
||||
authentication_classes = [
|
||||
BaseSessionAuthentication,
|
||||
]
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
for backend in list(self.filter_backends):
|
||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html>
|
||||
<p>This is a test email sent to verify if email configuration is working as expected in your Plane instance.</p>
|
||||
|
||||
<p>Regards,</br> Team Plane </p>
|
||||
</html>
|
||||
@@ -96,7 +96,7 @@ This will prompt you with the below options.
|
||||
|
||||
```bash
|
||||
Select a Action you want to perform:
|
||||
1) Install (x86_64)
|
||||
1) Install (arm64)
|
||||
2) Start
|
||||
3) Stop
|
||||
4) Restart
|
||||
@@ -344,7 +344,7 @@ Similarly, you can view the logs of other services.
|
||||
|
||||
There would a time when you might want to backup your data from docker volumes to external storage like S3 or drives.
|
||||
|
||||
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `7` to Backup the data.
|
||||
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `8` to view logs.
|
||||
|
||||
```bash
|
||||
Select a Action you want to perform:
|
||||
|
||||
@@ -390,7 +390,7 @@ fi
|
||||
|
||||
# CPU ARCHITECHTURE BASED SETTINGS
|
||||
CPU_ARCH=$(uname -m)
|
||||
if [[ $FORCE_CPU == "amd64" || $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]];
|
||||
if [[ $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]];
|
||||
then
|
||||
USE_GLOBAL_IMAGES=1
|
||||
DOCKERHUB_USER=makeplane
|
||||
|
||||
@@ -16,9 +16,7 @@ http {
|
||||
add_header Permissions-Policy "interest-cohort=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Forwarded-Proto "${dollar}scheme";
|
||||
add_header X-Forwarded-Host "${dollar}host";
|
||||
add_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
|
||||
add_header Host "${dollar}http_host";
|
||||
add_header Host "${dollar}host";
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:3000/;
|
||||
@@ -28,24 +26,20 @@ http {
|
||||
}
|
||||
|
||||
location /god-mode/ {
|
||||
proxy_pass http://admin:3001/god-mode/;
|
||||
proxy_pass http://admin:3000/god-mode/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_set_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
|
||||
proxy_set_header Host "${dollar}http_host";
|
||||
proxy_pass http://api:8000/api/;
|
||||
}
|
||||
|
||||
location /auth/ {
|
||||
proxy_set_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
|
||||
proxy_set_header Host "${dollar}http_host";
|
||||
proxy_pass http://api:8000/auth/;
|
||||
}
|
||||
|
||||
location /spaces/ {
|
||||
rewrite ^/spaces/?$ /spaces/login break;
|
||||
proxy_pass http://space:3002/spaces/;
|
||||
proxy_pass http://space:4000/spaces/;
|
||||
}
|
||||
|
||||
location /${BUCKET_NAME}/ {
|
||||
|
||||
@@ -16,17 +16,13 @@ http {
|
||||
add_header Permissions-Policy "interest-cohort=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Forwarded-Proto "${dollar}scheme";
|
||||
add_header X-Forwarded-Host "${dollar}host";
|
||||
add_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
|
||||
add_header Host "${dollar}http_host";
|
||||
add_header Host "${dollar}host";
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:3000/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_set_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
|
||||
proxy_set_header Host "${dollar}http_host";
|
||||
proxy_pass http://api:8000/api/;
|
||||
}
|
||||
|
||||
@@ -39,8 +35,6 @@ http {
|
||||
}
|
||||
|
||||
location /auth/ {
|
||||
proxy_set_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
|
||||
proxy_set_header Host "${dollar}http_host";
|
||||
proxy_pass http://api:8000/auth/;
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -12,8 +12,7 @@
|
||||
"packages/tailwind-config-custom",
|
||||
"packages/tsconfig",
|
||||
"packages/ui",
|
||||
"packages/types",
|
||||
"packages/constants"
|
||||
"packages/types"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*"
|
||||
}
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
// TODO: remove this
|
||||
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 EAuthErrorCodes {
|
||||
// Global
|
||||
INSTANCE_NOT_CONFIGURED = "5000",
|
||||
INVALID_EMAIL = "5005",
|
||||
EMAIL_REQUIRED = "5010",
|
||||
SIGNUP_DISABLED = "5015",
|
||||
MAGIC_LINK_LOGIN_DISABLED = "5017",
|
||||
PASSWORD_LOGIN_DISABLED = "5019",
|
||||
SMTP_NOT_CONFIGURED = "5025",
|
||||
// Password strength
|
||||
INVALID_PASSWORD = "5020",
|
||||
// Sign Up
|
||||
USER_ACCOUNT_DEACTIVATED = "5019",
|
||||
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 = "5090",
|
||||
EXPIRED_MAGIC_CODE = "5095",
|
||||
EMAIL_CODE_ATTEMPT_EXHAUSTED = "5100",
|
||||
// Oauth
|
||||
GOOGLE_NOT_CONFIGURED = "5105",
|
||||
GITHUB_NOT_CONFIGURED = "5110",
|
||||
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
|
||||
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
|
||||
// Reset Password
|
||||
INVALID_PASSWORD_TOKEN = "5125",
|
||||
EXPIRED_PASSWORD_TOKEN = "5130",
|
||||
// Change password
|
||||
INCORRECT_OLD_PASSWORD = "5135",
|
||||
MISSING_PASSWORD= "5138",
|
||||
INVALID_NEW_PASSWORD = "5140",
|
||||
// set passowrd
|
||||
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",
|
||||
}
|
||||
|
||||
export type TAuthErrorInfo = {
|
||||
type: EErrorAlertType;
|
||||
code: EAuthErrorCodes;
|
||||
title: string;
|
||||
message: ReactNode;
|
||||
};
|
||||
|
||||
const errorCodeMessages: {
|
||||
[key in EAuthErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
|
||||
} = {
|
||||
// global
|
||||
[EAuthErrorCodes.INSTANCE_NOT_CONFIGURED]: {
|
||||
title: `Instance not configured`,
|
||||
message: () => `Instance not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthErrorCodes.SIGNUP_DISABLED]: {
|
||||
title: `Sign up disabled`,
|
||||
message: () => `Sign up disabled. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthErrorCodes.INVALID_PASSWORD]: {
|
||||
title: `Invalid password`,
|
||||
message: () => `Invalid password. Please try again.`,
|
||||
},
|
||||
[EAuthErrorCodes.SMTP_NOT_CONFIGURED]: {
|
||||
title: `SMTP not configured`,
|
||||
message: () => `SMTP not configured. Please contact your administrator.`,
|
||||
},
|
||||
|
||||
// email check in both sign up and sign in
|
||||
[EAuthErrorCodes.INVALID_EMAIL]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EMAIL_REQUIRED]: {
|
||||
title: `Email required`,
|
||||
message: () => `Email required. Please try again.`,
|
||||
},
|
||||
|
||||
// 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=${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.`,
|
||||
},
|
||||
|
||||
// sign in
|
||||
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
|
||||
title: `User account deactivated`,
|
||||
message: () => <div>Your account is deactivated. Contact support@plane.so.</div>,
|
||||
},
|
||||
[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=${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]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Invalid magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE]: {
|
||||
title: `Expired magic code`,
|
||||
message: () => `Expired magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED]: {
|
||||
title: `Expired magic code`,
|
||||
message: () => `Expired magic code. Please try again.`,
|
||||
},
|
||||
|
||||
// Oauth
|
||||
[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.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.`,
|
||||
},
|
||||
|
||||
// Reset Password
|
||||
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
|
||||
title: `Invalid password token`,
|
||||
message: () => `Invalid password token. Please try again.`,
|
||||
},
|
||||
[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>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
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.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,
|
||||
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE,
|
||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED,
|
||||
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
|
||||
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
|
||||
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
|
||||
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
|
||||
EAuthenticationErrorCodes.INCORRECT_OLD_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,
|
||||
];
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./auth";
|
||||
@@ -61,7 +61,7 @@
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"eslint-config-custom": "*",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.29",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsconfig": "*",
|
||||
"tsup": "^7.2.0",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"eslint-config-custom": "*",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.29",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsconfig": "*",
|
||||
"tsup": "^7.2.0",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"eslint-config-custom": "*",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.29",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsconfig": "*",
|
||||
"tsup": "^7.2.0",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"eslint-config-custom": "*",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.29",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsconfig": "*",
|
||||
"tsup": "^7.2.0",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"eslint-config-custom": "*",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.29",
|
||||
"react": "^18.2.0",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsconfig": "*",
|
||||
|
||||
@@ -13,7 +13,7 @@ module.exports = {
|
||||
plugins: ["react", "@typescript-eslint", "import"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/", "admin/", "packages/*/"],
|
||||
rootDir: ["web/", "space/", "packages/*/"],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"tailwindcss": "^3.2.7",
|
||||
|
||||
@@ -68,13 +68,6 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
if (closeOnSelect) closeDropdown();
|
||||
};
|
||||
|
||||
const handleMenuButtonClick = (e:React.MouseEvent<HTMLButtonElement, MouseEvent>)=>{
|
||||
e.stopPropagation();
|
||||
e.preventDefault()
|
||||
isOpen ? closeDropdown() : openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}
|
||||
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
let menuItems = (
|
||||
@@ -119,7 +112,11 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
onClick={handleMenuButtonClick}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
isOpen ? closeDropdown() : openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
className={customButtonClassName}
|
||||
tabIndex={customButtonTabIndex}
|
||||
>
|
||||
@@ -133,7 +130,12 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
onClick={handleMenuButtonClick}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isOpen ? closeDropdown() : openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
disabled={disabled}
|
||||
className={`relative grid place-items-center rounded p-1 text-custom-text-200 outline-none hover:text-custom-text-100 ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
||||
@@ -155,7 +157,12 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={handleMenuButtonClick}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isOpen ? closeDropdown() : openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
tabIndex={customButtonTabIndex}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export default function ProjectError() {
|
||||
return <>Project Error</>;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import Image from "next/image";
|
||||
// components
|
||||
import IssueNavbar from "@/components/issues/navbar";
|
||||
// services
|
||||
import ProjectService from "@/services/project.service";
|
||||
// assets
|
||||
import planeLogo from "public/plane-logo.svg";
|
||||
|
||||
const projectService = new ProjectService();
|
||||
|
||||
export default async function ProjectLayout({ children, params }: { children: React.ReactNode; params: any }) {
|
||||
const { workspace_slug, project_id } = params;
|
||||
const projectSettings = await projectService.getProjectSettings(workspace_slug, project_id);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
|
||||
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
|
||||
<IssueNavbar projectSettings={projectSettings} workspaceSlug={workspace_slug} projectId={project_id} />
|
||||
</div>
|
||||
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div>
|
||||
<a
|
||||
href="https://plane.so"
|
||||
className="fixed bottom-2.5 right-5 !z-[999999] flex items-center gap-1 rounded border border-custom-border-200 bg-custom-background-100 px-2 py-1 shadow-custom-shadow-2xs"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<div className="relative grid h-6 w-6 place-items-center">
|
||||
<Image src={planeLogo} alt="Plane logo" className="h-6 w-6" height="24" width="24" />
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
Powered by <span className="font-semibold">Plane Deploy</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function ProjectSettingsNotFound() {
|
||||
return <>Project Settings not found</>;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// components
|
||||
import { ProjectDetailsView } from "@/components/views";
|
||||
|
||||
export default async function WorkspaceProjectPage({ params }: { params: any }) {
|
||||
const { workspace_slug, project_id, peekId } = params;
|
||||
|
||||
return <ProjectDetailsView workspaceSlug={workspace_slug} projectId={project_id} peekId={peekId} />;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
|
||||
import InstanceFailureImage from "@/public/instance/instance-failure.svg";
|
||||
|
||||
export default function InstanceError() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
|
||||
|
||||
const handleRetry = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center mt-10">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<Image src={instanceImage} alt="Plane Logo" />
|
||||
<h3 className="font-medium text-2xl text-white ">Unable to fetch instance details.</h3>
|
||||
<p className="font-medium text-base text-center">
|
||||
We were unable to fetch the details of the instance. <br />
|
||||
Fret not, it might just be a connectivity issue.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button size="md" onClick={handleRetry}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Metadata } from "next";
|
||||
// styles
|
||||
import "@/styles/globals.css";
|
||||
// helpers
|
||||
import { ASSET_PREFIX } from "@/helpers/common.helper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Plane Deploy | Make your Plane boards public with one-click",
|
||||
description: "Plane Deploy is a customer feedback management tool built on top of plane.so",
|
||||
openGraph: {
|
||||
title: "Plane Deploy | Make your Plane boards public with one-click",
|
||||
description: "Plane Deploy is a customer feedback management tool built on top of plane.so",
|
||||
url: "https://sites.plane.so/",
|
||||
},
|
||||
keywords:
|
||||
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration",
|
||||
twitter: {
|
||||
site: "@planepowers",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${ASSET_PREFIX}favicon/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${ASSET_PREFIX}favicon/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${ASSET_PREFIX}favicon/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${ASSET_PREFIX}site.webmanifest.json`} />
|
||||
<link rel="shortcut icon" href={`${ASSET_PREFIX}favicon/favicon.ico`} />
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
|
||||
import InstanceFailureImage from "@/public/instance/instance-failure.svg";
|
||||
|
||||
export default function InstanceNotFound() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
|
||||
|
||||
const handleRetry = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center mt-10">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<Image src={instanceImage} alt="Plane Logo" />
|
||||
<h3 className="font-medium text-2xl text-white ">Unable to fetch instance details.</h3>
|
||||
<p className="font-medium text-base text-center">
|
||||
We were unable to fetch the details of the instance. <br />
|
||||
Fret not, it might just be a connectivity issue.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button size="md" onClick={handleRetry}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// components
|
||||
import { UserLoggedIn } from "@/components/accounts";
|
||||
import { InstanceNotReady, InstanceFailureView } from "@/components/instance";
|
||||
import { AuthView } from "@/components/views";
|
||||
// helpers
|
||||
// import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
// import { useInstance, useUser } from "@/hooks/store";
|
||||
// wrapper
|
||||
// import { AuthWrapper } from "@/lib/wrappers";
|
||||
// lib
|
||||
import { AppProvider } from "@/lib/app-providers";
|
||||
// services
|
||||
import { InstanceService } from "@/services/instance.service";
|
||||
import { UserService } from "@/services/user.service";
|
||||
|
||||
const userServices = new UserService();
|
||||
const instanceService = new InstanceService();
|
||||
|
||||
export default async function HomePage() {
|
||||
const instanceDetails = await instanceService.getInstanceInfo().catch(() => undefined);
|
||||
const user = await userServices
|
||||
.currentUser()
|
||||
.then((user) => ({ ...user, isAuthenticated: true }))
|
||||
.catch(() => ({ isAuthenticated: false }));
|
||||
|
||||
if (!instanceDetails) {
|
||||
return <InstanceFailureView />;
|
||||
}
|
||||
|
||||
if (!instanceDetails?.instance?.is_setup_done) {
|
||||
<InstanceNotReady />;
|
||||
}
|
||||
|
||||
if (user.isAuthenticated) {
|
||||
return <UserLoggedIn />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppProvider initialState={{ instance: instanceDetails.instance }}>
|
||||
<AuthView />
|
||||
</AppProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// icons
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
"use client";
|
||||
import { Fragment, useState } from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
// icons
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
import { Eye, EyeOff, XCircle } from "lucide-react";
|
||||
// ui
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
@@ -14,7 +12,7 @@ import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { getPasswordStrength } from "@/helpers/password.helper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
import { AuthService } from "@/services/authentication.service";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
@@ -45,11 +43,12 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
||||
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// hooks
|
||||
const { data: instance, config: instanceConfig } = useInstance();
|
||||
const { instance } = useInstance();
|
||||
// router
|
||||
const { next_path } = useParams<any>();
|
||||
const router = useRouter();
|
||||
const { next_path } = router.query;
|
||||
// derived values
|
||||
const isSmtpConfigured = instanceConfig?.is_smtp_configured;
|
||||
const isSmtpConfigured = instance?.config?.is_smtp_configured;
|
||||
|
||||
const handleFormChange = (key: keyof TPasswordFormValues, value: string) =>
|
||||
setPasswordFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
@@ -9,7 +7,7 @@ import { EmailForm, UniqueCodeForm, PasswordForm, OAuthOptions, TermsAndConditio
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import useToast from "@/hooks/use-toast";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
import { AuthService } from "@/services/authentication.service";
|
||||
|
||||
export enum EAuthSteps {
|
||||
EMAIL = "EMAIL",
|
||||
@@ -62,9 +60,9 @@ export const AuthRoot = observer(() => {
|
||||
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||
const [email, setEmail] = useState("");
|
||||
// hooks
|
||||
const { config: instanceConfig } = useInstance();
|
||||
const { instance } = useInstance();
|
||||
// derived values
|
||||
const isSmtpConfigured = instanceConfig?.is_smtp_configured;
|
||||
const isSmtpConfigured = instance?.config?.is_smtp_configured;
|
||||
|
||||
const { header, subHeader } = getHeaderSubHeader(authMode);
|
||||
|
||||
@@ -114,8 +112,8 @@ export const AuthRoot = observer(() => {
|
||||
);
|
||||
};
|
||||
|
||||
const isOAuthEnabled = instanceConfig && (instanceConfig?.is_google_enabled || instanceConfig?.is_github_enabled);
|
||||
|
||||
const isOAuthEnabled =
|
||||
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
|
||||
return (
|
||||
<div className="relative flex flex-col space-y-6">
|
||||
<div className="space-y-1 text-center">
|
||||
@@ -151,7 +149,7 @@ export const AuthRoot = observer(() => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isOAuthEnabled !== undefined && <OAuthOptions />}
|
||||
{isOAuthEnabled && <OAuthOptions />}
|
||||
<TermsAndConditions mode={authMode} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { CircleCheck, XCircle } from "lucide-react";
|
||||
// ui
|
||||
@@ -12,7 +10,7 @@ import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import useTimer from "@/hooks/use-timer";
|
||||
import useToast from "@/hooks/use-toast";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
import { AuthService } from "@/services/authentication.service";
|
||||
// types
|
||||
import { IEmailCheckData } from "@/types/auth";
|
||||
import { EAuthModes } from "./root";
|
||||
@@ -45,7 +43,8 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// router
|
||||
const { next_path } = useParams<any>();
|
||||
const router = useRouter();
|
||||
const { next_path } = router.query;
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// timer
|
||||
@@ -151,7 +150,6 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
placeholder="gets-sets-flys"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
/>
|
||||
<div className="flex w-full items-center justify-between px-1 text-xs">
|
||||
<p className="flex items-center gap-1 font-medium text-green-700">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { GithubOAuthButton, GoogleOAuthButton } from "@/components/accounts";
|
||||
@@ -8,7 +6,7 @@ import { useInstance } from "@/hooks/store";
|
||||
|
||||
export const OAuthOptions: React.FC = observer(() => {
|
||||
// hooks
|
||||
const { config: instanceConfig } = useInstance();
|
||||
const { instance } = useInstance();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -18,12 +16,12 @@ export const OAuthOptions: React.FC = observer(() => {
|
||||
<hr className="w-full border-onboarding-border-100" />
|
||||
</div>
|
||||
<div className={`mx-auto mt-7 grid gap-4 overflow-hidden sm:w-96`}>
|
||||
{instanceConfig?.is_google_enabled && (
|
||||
{instance?.config?.is_google_enabled && (
|
||||
<div className="flex h-[42px] items-center !overflow-hidden">
|
||||
<GoogleOAuthButton text="SignIn with Google" />
|
||||
</div>
|
||||
)}
|
||||
{instanceConfig?.is_github_enabled && <GithubOAuthButton text="SignIn with Github" />}
|
||||
{instance?.config?.is_github_enabled && <GithubOAuthButton text="SignIn with Github" />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@@ -10,7 +8,7 @@ import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { UserImageUploadModal } from "@/components/accounts";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
// services
|
||||
import fileService from "@/services/file.service";
|
||||
|
||||
@@ -37,7 +35,9 @@ export const OnBoardingForm: React.FC<Props> = observer((props) => {
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||
// store hooks
|
||||
const { updateCurrentUser } = useUser();
|
||||
const {
|
||||
user: { updateCurrentUser },
|
||||
} = useMobxStore();
|
||||
// form info
|
||||
const {
|
||||
getValues,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
// icons
|
||||
import { CircleCheck } from "lucide-react";
|
||||
// helpers
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { EAuthModes } from "./auth-forms";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
@@ -28,7 +27,7 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||
// store hooks
|
||||
const { config: instanceConfig } = useInstance();
|
||||
const { instance } = useInstance();
|
||||
|
||||
const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]);
|
||||
|
||||
@@ -37,7 +36,7 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
|
||||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"],
|
||||
},
|
||||
maxSize: (instanceConfig?.file_size_limit as number) ?? MAX_FILE_SIZE,
|
||||
maxSize: instance?.config?.file_size_limit ?? MAX_FILE_SIZE,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./not-ready-view";
|
||||
export * from "./instance-failure-view";
|
||||
export * from "./not-ready-view";
|
||||
@@ -1,42 +0,0 @@
|
||||
"use client";
|
||||
import { FC } from "react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import InstanceFailureDarkImage from "public/instance/instance-failure-dark.svg";
|
||||
import InstanceFailureImage from "public/instance/instance-failure.svg";
|
||||
|
||||
type InstanceFailureViewProps = {
|
||||
// mutate: () => void;
|
||||
};
|
||||
|
||||
export const InstanceFailureView: FC<InstanceFailureViewProps> = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
|
||||
|
||||
const handleRetry = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center mt-10">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<Image src={instanceImage} alt="Plane Logo" />
|
||||
<h3 className="font-medium text-2xl text-white ">Unable to fetch instance details.</h3>
|
||||
<p className="font-medium text-base text-center">
|
||||
We were unable to fetch the details of the instance. <br />
|
||||
Fret not, it might just be a connectivity issue.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button size="md" onClick={handleRetry}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
// icons
|
||||
import { UserCog2 } from "lucide-react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// helper
|
||||
import { ADMIN_BASE_URL, ADMIN_BASE_PATH } from "@/helpers/common.helper";
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
// images
|
||||
import PlaneTakeOffImage from "@/public/instance/plane-takeoff.png";
|
||||
import instanceNotReady from "public/instance/plane-instance-not-ready.webp";
|
||||
import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
|
||||
import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
|
||||
|
||||
export const InstanceNotReady: FC = () => {
|
||||
const GOD_MODE_URL = encodeURI(ADMIN_BASE_URL + ADMIN_BASE_PATH);
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center pt-12">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||
<p className="font-medium text-base text-onboarding-text-400">
|
||||
Get started by setting up your instance and workspace
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href={GOD_MODE_URL}>
|
||||
<Button size="lg" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</a>
|
||||
<div className="h-screen w-full overflow-y-auto bg-onboarding-gradient-100">
|
||||
<div className="h-full w-full pt-24">
|
||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-100 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
|
||||
<div className="relative h-full rounded-t-md bg-onboarding-gradient-200 px-7 sm:px-0">
|
||||
<div className="flex items-center justify-center py-10">
|
||||
<Image src={planeLogo} className="h-[44px] w-full" alt="Plane logo" />
|
||||
</div>
|
||||
<div className="mt-20">
|
||||
<Image src={instanceNotReady} className="w-full" alt="Instance not ready" />
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center gap-5 py-12 pb-20">
|
||||
<h3 className="text-2xl font-medium">Your Plane instance isn{"'"}t ready yet</h3>
|
||||
<p className="text-sm">Ask your Instance Admin to complete set-up first.</p>
|
||||
<a href="/god-mode" className={`${getButtonStyling("primary", "md")} mt-4`}>
|
||||
<UserCog2 className="h-3.5 w-3.5" />
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,48 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
// components
|
||||
import { useRouter } from "next/router";
|
||||
import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date";
|
||||
import { IssueBlockPriority } from "@/components/issues/board-views/block-priority";
|
||||
import { IssueBlockState } from "@/components/issues/board-views/block-state";
|
||||
// hooks
|
||||
import { useIssueDetails, useProject } from "@/hooks/store";
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
|
||||
// components
|
||||
// interfaces
|
||||
import { IIssue } from "@/types/issue";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { IIssue } from "types/issue";
|
||||
|
||||
type IssueKanBanBlockProps = {
|
||||
issue: IIssue;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
params: any;
|
||||
};
|
||||
export const IssueKanBanBlock = observer(({ issue }: { issue: IIssue }) => {
|
||||
const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||
|
||||
export const IssueKanBanBlock: FC<IssueKanBanBlockProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, params, issue } = props;
|
||||
const { board, priorities, states, labels } = params;
|
||||
// store
|
||||
const { project } = useProject();
|
||||
const { setPeekId } = useIssueDetails();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { workspace_slug, project_slug, board, priorities, states, labels } = router.query as {
|
||||
workspace_slug: string;
|
||||
project_slug: string;
|
||||
board: string;
|
||||
priorities: string;
|
||||
states: string;
|
||||
labels: string;
|
||||
};
|
||||
|
||||
const handleBlockClick = () => {
|
||||
setPeekId(issue.id);
|
||||
issueDetailStore.setPeekId(issue.id);
|
||||
const params: any = { board: board, peekId: issue.id };
|
||||
if (states && states.length > 0) params.states = states;
|
||||
if (priorities && priorities.length > 0) params.priorities = priorities;
|
||||
if (labels && labels.length > 0) params.labels = labels;
|
||||
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
|
||||
router.push(
|
||||
{
|
||||
pathname: `/${workspace_slug}/${project_slug}`,
|
||||
query: { ...params },
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs">
|
||||
{/* id */}
|
||||
<div className="break-words text-xs text-custom-text-300">
|
||||
{project?.identifier}-{issue?.sequence_id}
|
||||
{projectStore?.project?.identifier}-{issue?.sequence_id}
|
||||
</div>
|
||||
|
||||
{/* name */}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// ui
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// constants
|
||||
import { issueGroupFilter } from "@/constants/data";
|
||||
// mobx hook
|
||||
import { useIssue } from "@/hooks/store";
|
||||
// interfaces
|
||||
import { IIssueState } from "@/types/issue";
|
||||
// constants
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
import { issueGroupFilter } from "@/constants/data";
|
||||
// ui
|
||||
// mobx hook
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { IIssueState } from "types/issue";
|
||||
|
||||
export const IssueKanBanHeader = observer(({ state }: { state: IIssueState }) => {
|
||||
const { getCountOfIssuesByState } = useIssue();
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
const stateGroup = issueGroupFilter(state.group);
|
||||
|
||||
if (stateGroup === null) return <></>;
|
||||
@@ -21,7 +23,9 @@ export const IssueKanBanHeader = observer(({ state }: { state: IIssueState }) =>
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="14" width="14" />
|
||||
</div>
|
||||
<div className="mr-1 truncate font-semibold capitalize text-custom-text-200">{state?.name}</div>
|
||||
<span className="flex-shrink-0 rounded-full text-custom-text-300">{getCountOfIssuesByState(state.id)}</span>
|
||||
<span className="flex-shrink-0 rounded-full text-custom-text-300">
|
||||
{store.issue.getCountOfIssuesByState(state.id)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,47 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { IssueKanBanBlock } from "@/components/issues/board-views/kanban/block";
|
||||
import { IssueKanBanHeader } from "@/components/issues/board-views/kanban/header";
|
||||
// ui
|
||||
import { Icon } from "@/components/ui";
|
||||
// mobx hook
|
||||
import { useIssue } from "@/hooks/store";
|
||||
// interfaces
|
||||
import { IIssueState, IIssue } from "@/types/issue";
|
||||
// mobx hook
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { IIssueState, IIssue } from "types/issue";
|
||||
|
||||
type IssueKanbanViewProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const IssueKanbanView: FC<IssueKanbanViewProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
// store hooks
|
||||
const { states, getFilteredIssuesByState } = useIssue();
|
||||
export const IssueKanbanView = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full gap-3 overflow-hidden overflow-x-auto">
|
||||
{states &&
|
||||
states.length > 0 &&
|
||||
states.map((_state: IIssueState) => (
|
||||
{store?.issue?.states &&
|
||||
store?.issue?.states.length > 0 &&
|
||||
store?.issue?.states.map((_state: IIssueState) => (
|
||||
<div key={_state.id} className="relative flex h-full w-[340px] flex-shrink-0 flex-col">
|
||||
<div className="flex-shrink-0">
|
||||
<IssueKanBanHeader state={_state} />
|
||||
</div>
|
||||
<div className="hide-vertical-scrollbar h-full w-full overflow-hidden overflow-y-auto">
|
||||
{getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? (
|
||||
{store.issue.getFilteredIssuesByState(_state.id) &&
|
||||
store.issue.getFilteredIssuesByState(_state.id).length > 0 ? (
|
||||
<div className="space-y-3 px-2 pb-2">
|
||||
{getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
|
||||
<IssueKanBanBlock
|
||||
key={_issue.id}
|
||||
issue={_issue}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
params={{}}
|
||||
/>
|
||||
{store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
|
||||
<IssueKanBanBlock key={_issue.id} issue={_issue} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,40 +1,47 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
// components
|
||||
import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date";
|
||||
import { IssueBlockLabels } from "@/components/issues/board-views/block-labels";
|
||||
import { IssueBlockPriority } from "@/components/issues/board-views/block-priority";
|
||||
import { IssueBlockState } from "@/components/issues/board-views/block-state";
|
||||
// mobx hook
|
||||
import { useIssueDetails, useProject } from "@/hooks/store";
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
// interfaces
|
||||
import { IIssue } from "@/types/issue";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { IIssue } from "types/issue";
|
||||
// store
|
||||
|
||||
type IssueListBlockProps = {
|
||||
issue: IIssue;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const IssueListBlock: FC<IssueListBlockProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issue } = props;
|
||||
const { board, states, priorities, labels } = useParams<any>();
|
||||
const searchParams = useSearchParams();
|
||||
export const IssueListBlock: FC<{ issue: IIssue }> = observer((props) => {
|
||||
const { issue } = props;
|
||||
// store
|
||||
const { project } = useProject();
|
||||
const { setPeekId } = useIssueDetails();
|
||||
const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug, board, priorities, states, labels } = router.query as {
|
||||
workspace_slug: string;
|
||||
project_slug: string;
|
||||
board: string;
|
||||
priorities: string;
|
||||
states: string;
|
||||
labels: string;
|
||||
};
|
||||
|
||||
const handleBlockClick = () => {
|
||||
setPeekId(issue.id);
|
||||
issueDetailStore.setPeekId(issue.id);
|
||||
const params: any = { board: board, peekId: issue.id };
|
||||
if (states && states.length > 0) params.states = states;
|
||||
if (priorities && priorities.length > 0) params.priorities = priorities;
|
||||
if (labels && labels.length > 0) params.labels = labels;
|
||||
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
|
||||
router.push(
|
||||
{
|
||||
pathname: `/${workspace_slug}/${project_slug}`,
|
||||
query: { ...params },
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
);
|
||||
// router.push(`/${workspace_slug?.toString()}/${project_slug}?board=${board?.toString()}&peekId=${issue.id}`);
|
||||
};
|
||||
|
||||
@@ -43,7 +50,7 @@ export const IssueListBlock: FC<IssueListBlockProps> = observer((props) => {
|
||||
<div className="relative flex w-full flex-grow items-center gap-3 overflow-hidden">
|
||||
{/* id */}
|
||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||
{project?.identifier}-{issue?.sequence_id}
|
||||
{projectStore?.project?.identifier}-{issue?.sequence_id}
|
||||
</div>
|
||||
{/* name */}
|
||||
<div onClick={handleBlockClick} className="flex-grow cursor-pointer truncate text-sm font-medium">
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// interfaces
|
||||
// ui
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// constants
|
||||
import { issueGroupFilter } from "@/constants/data";
|
||||
// mobx hook
|
||||
import { useIssue } from "@/hooks/store";
|
||||
// types
|
||||
import { IIssueState } from "@/types/issue";
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { IIssueState } from "types/issue";
|
||||
|
||||
export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
|
||||
const { getCountOfIssuesByState } = useIssue();
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
const stateGroup = issueGroupFilter(state.group);
|
||||
|
||||
if (stateGroup === null) return <></>;
|
||||
@@ -20,7 +23,7 @@ export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="14" width="14" />
|
||||
</div>
|
||||
<div className="mr-1 font-medium capitalize">{state?.name}</div>
|
||||
<div className="text-sm font-medium text-custom-text-200">{getCountOfIssuesByState(state.id)}</div>
|
||||
<div className="text-sm font-medium text-custom-text-200">{store.issue.getCountOfIssuesByState(state.id)}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { IssueListBlock } from "@/components/issues/board-views/list/block";
|
||||
import { IssueListHeader } from "@/components/issues/board-views/list/header";
|
||||
// interfaces
|
||||
// mobx hook
|
||||
import { useIssue } from "@/hooks/store";
|
||||
// types
|
||||
import { IIssueState, IIssue } from "@/types/issue";
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
// store
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { IIssueState, IIssue } from "types/issue";
|
||||
|
||||
type IssueListViewProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const IssueListView: FC<IssueListViewProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
// store hooks
|
||||
const { states, getFilteredIssuesByState } = useIssue();
|
||||
export const IssueListView = observer(() => {
|
||||
const { issue: issueStore }: RootStore = useMobxStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
{states &&
|
||||
states.length > 0 &&
|
||||
states.map((_state: IIssueState) => (
|
||||
{issueStore?.states &&
|
||||
issueStore?.states.length > 0 &&
|
||||
issueStore?.states.map((_state: IIssueState) => (
|
||||
<div key={_state.id} className="relative w-full">
|
||||
<IssueListHeader state={_state} />
|
||||
{getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? (
|
||||
{issueStore.getFilteredIssuesByState(_state.id) &&
|
||||
issueStore.getFilteredIssuesByState(_state.id).length > 0 ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
|
||||
<IssueListBlock key={_issue.id} issue={_issue} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
{issueStore.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
|
||||
<IssueListBlock key={_issue.id} issue={_issue} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// components
|
||||
// icons
|
||||
import { X } from "lucide-react";
|
||||
// types
|
||||
import { IIssueLabel, IIssueState, IIssueFilterOptions } from "@/types/issue";
|
||||
// components
|
||||
// helpers
|
||||
import { IIssueFilterOptions } from "@/store/issues/types";
|
||||
import { IIssueLabel, IIssueState } from "types/issue";
|
||||
import { AppliedPriorityFilters } from "./priority";
|
||||
import { AppliedStateFilters } from "./state";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
appliedFilters: IIssueFilterOptions;
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/navigation";
|
||||
// hooks
|
||||
import { useIssue, useProject, useIssueFilter } from "@/hooks/store";
|
||||
// store
|
||||
import { IIssueFilterOptions } from "@/types/issue";
|
||||
import { useRouter } from "next/router";
|
||||
// components
|
||||
// store
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
import { IIssueFilterOptions } from "@/store/issues/types";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { AppliedFiltersList } from "./filters-list";
|
||||
|
||||
// TODO: fix component types
|
||||
export const IssueAppliedFilters: FC = observer((props: any) => {
|
||||
export const IssueAppliedFilters: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = props;
|
||||
const { states, labels } = useIssue();
|
||||
const { activeLayout } = useProject();
|
||||
const { issueFilters, updateFilters } = useIssueFilter();
|
||||
const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as {
|
||||
workspace_slug: string;
|
||||
project_slug: string;
|
||||
};
|
||||
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
issue: { states, labels },
|
||||
project: { activeBoard },
|
||||
}: RootStore = useMobxStore();
|
||||
|
||||
const userFilters = issueFilters?.filters || {};
|
||||
|
||||
const appliedFilters: any = {};
|
||||
|
||||
const appliedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(userFilters).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
appliedFilters[key] = value;
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const updateRouteParams = useCallback(
|
||||
@@ -34,17 +36,16 @@ export const IssueAppliedFilters: FC = observer((props: any) => {
|
||||
const priority = key === "priority" ? value || [] : issueFilters?.filters?.priority ?? [];
|
||||
const labels = key === "labels" ? value || [] : issueFilters?.filters?.labels ?? [];
|
||||
|
||||
let params: any = { board: activeLayout || "list" };
|
||||
let params: any = { board: activeBoard || "list" };
|
||||
if (!clearFields) {
|
||||
if (priority.length > 0) params = { ...params, priorities: priority.join(",") };
|
||||
if (state.length > 0) params = { ...params, states: state.join(",") };
|
||||
if (labels.length > 0) params = { ...params, labels: labels.join(",") };
|
||||
}
|
||||
console.log("params", params);
|
||||
// TODO: fix this redirection
|
||||
// router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true });
|
||||
|
||||
router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true });
|
||||
},
|
||||
[workspaceSlug, projectId, activeLayout, issueFilters, router]
|
||||
[workspaceSlug, projectId, activeBoard, issueFilters, router]
|
||||
);
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
@@ -79,7 +80,7 @@ export const IssueAppliedFilters: FC = observer((props: any) => {
|
||||
<div className="border-b border-custom-border-200 p-5 py-3">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters || {}}
|
||||
handleRemoveFilter={handleRemoveFilter as any}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
handleRemoveAllFilters={handleRemoveAllFilters}
|
||||
labels={labels ?? []}
|
||||
states={states ?? []}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// hooks
|
||||
import { useIssue, useIssueFilter, useProject } from "@/hooks/store";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@/types/issue";
|
||||
import { useRouter } from "next/router";
|
||||
// components
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/store/issues/helpers";
|
||||
import { IIssueFilterOptions } from "@/store/issues/types";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { FiltersDropdown } from "./helpers/dropdown";
|
||||
import { FilterSelection } from "./selection";
|
||||
// types
|
||||
// helpers
|
||||
// store
|
||||
|
||||
type IssueFiltersDropdownProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
const searchParams = useSearchParams();
|
||||
export const IssueFiltersDropdown: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
// store hooks
|
||||
const { activeLayout } = useProject();
|
||||
const { states, labels } = useIssue();
|
||||
const { issueFilters, updateFilters } = useIssueFilter();
|
||||
const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as {
|
||||
workspace_slug: string;
|
||||
project_slug: string;
|
||||
};
|
||||
|
||||
const {
|
||||
project: { activeBoard },
|
||||
issue: { states, labels },
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
}: RootStore = useMobxStore();
|
||||
|
||||
const updateRouteParams = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string[]) => {
|
||||
@@ -31,14 +31,14 @@ export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((pro
|
||||
const priority = key === "priority" ? value : issueFilters?.filters?.priority ?? [];
|
||||
const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? [];
|
||||
|
||||
let params: any = { board: activeLayout || "list" };
|
||||
let params: any = { board: activeBoard || "list" };
|
||||
if (priority.length > 0) params = { ...params, priorities: priority.join(",") };
|
||||
if (state.length > 0) params = { ...params, states: state.join(",") };
|
||||
if (labels.length > 0) params = { ...params, labels: labels.join(",") };
|
||||
console.log("params", params);
|
||||
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
|
||||
|
||||
router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true });
|
||||
},
|
||||
[workspaceSlug, projectId, activeLayout, issueFilters, router]
|
||||
[workspaceSlug, projectId, activeBoard, issueFilters, router]
|
||||
);
|
||||
|
||||
const handleFilters = useCallback(
|
||||
@@ -66,8 +66,8 @@ export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((pro
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFilters={handleFilters as any}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
handleFilters={handleFilters}
|
||||
layoutDisplayFiltersOptions={activeBoard ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeBoard] : undefined}
|
||||
states={states ?? undefined}
|
||||
labels={labels ?? undefined}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Search, X } from "lucide-react";
|
||||
// types
|
||||
import { IIssueState, IIssueLabel, IIssueFilterOptions } from "@/types/issue";
|
||||
import { ILayoutDisplayFiltersOptions } from "@/types/issue-filters";
|
||||
// components
|
||||
// types
|
||||
|
||||
// filter helpers
|
||||
import { ILayoutDisplayFiltersOptions } from "@/store/issues/helpers";
|
||||
import { IIssueFilterOptions } from "@/store/issues/types";
|
||||
import { IIssueState, IIssueLabel } from "types/issue";
|
||||
import { FilterPriority, FilterState } from "./";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,109 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, FC } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useParams, useSearchParams, usePathname } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
// components
|
||||
import { Briefcase } from "lucide-react";
|
||||
import { Avatar, Button } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectLogo } from "@/components/common";
|
||||
import { IssueFiltersDropdown } from "@/components/issues/filters";
|
||||
// hooks
|
||||
import { useProject, useUser, useIssueFilter } from "@/hooks/store";
|
||||
// types
|
||||
import { useMobxStore, useUser } from "@/hooks/store";
|
||||
// store
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { TIssueBoardKeys } from "@/types/issue";
|
||||
// components
|
||||
import { NavbarIssueBoardView } from "./issue-board-view";
|
||||
import { NavbarTheme } from "./theme";
|
||||
|
||||
type IssueNavbarProps = {
|
||||
projectSettings: any;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
const IssueNavbar: FC<IssueNavbarProps> = observer((props) => {
|
||||
const { projectSettings, workspaceSlug, projectId } = props;
|
||||
const { project_details, views } = projectSettings;
|
||||
const { board, labels, states, priorities, peekId } = useParams<any>();
|
||||
const searchParams = useSearchParams();
|
||||
const pathName = usePathname();
|
||||
// hooks
|
||||
const router = useRouter();
|
||||
// store
|
||||
const { settings, activeLayout, hydrate, setActiveLayout } = useProject();
|
||||
const IssueNavbar = observer(() => {
|
||||
const {
|
||||
project: projectStore,
|
||||
issuesFilter: { updateFilters },
|
||||
}: RootStore = useMobxStore();
|
||||
const { data: user } = useUser();
|
||||
const { updateFilters } = useIssueFilter();
|
||||
hydrate(projectSettings);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug, board, peekId, states, priorities, labels } = router.query as {
|
||||
workspace_slug: string;
|
||||
project_slug: string;
|
||||
peekId: string;
|
||||
board: string;
|
||||
states: string;
|
||||
priorities: string;
|
||||
labels: string;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId && settings) {
|
||||
if (workspace_slug && project_slug) {
|
||||
projectStore.fetchProjectSettings(workspace_slug.toString(), project_slug.toString());
|
||||
}
|
||||
}, [projectStore, workspace_slug, project_slug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace_slug && project_slug && projectStore?.deploySettings) {
|
||||
const viewsAcceptable: string[] = [];
|
||||
const currentBoard: TIssueBoardKeys | null = null;
|
||||
let currentBoard: TIssueBoardKeys | null = null;
|
||||
|
||||
if (settings?.views?.list) viewsAcceptable.push("list");
|
||||
if (settings?.views?.kanban) viewsAcceptable.push("kanban");
|
||||
if (settings?.views?.calendar) viewsAcceptable.push("calendar");
|
||||
if (settings?.views?.gantt) viewsAcceptable.push("gantt");
|
||||
if (settings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet");
|
||||
if (projectStore?.deploySettings?.views?.list) viewsAcceptable.push("list");
|
||||
if (projectStore?.deploySettings?.views?.kanban) viewsAcceptable.push("kanban");
|
||||
if (projectStore?.deploySettings?.views?.calendar) viewsAcceptable.push("calendar");
|
||||
if (projectStore?.deploySettings?.views?.gantt) viewsAcceptable.push("gantt");
|
||||
if (projectStore?.deploySettings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet");
|
||||
|
||||
// if (board) {
|
||||
// if (viewsAcceptable.includes(board.toString())) {
|
||||
// currentBoard = board.toString() as TIssueBoardKeys;
|
||||
// } else {
|
||||
// if (viewsAcceptable && viewsAcceptable.length > 0) {
|
||||
// currentBoard = viewsAcceptable[0] as TIssueBoardKeys;
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// if (viewsAcceptable && viewsAcceptable.length > 0) {
|
||||
// currentBoard = viewsAcceptable[0] as TIssueBoardKeys;
|
||||
// }
|
||||
// }
|
||||
if (board) {
|
||||
if (viewsAcceptable.includes(board.toString())) {
|
||||
currentBoard = board.toString() as TIssueBoardKeys;
|
||||
} else {
|
||||
if (viewsAcceptable && viewsAcceptable.length > 0) {
|
||||
currentBoard = viewsAcceptable[0] as TIssueBoardKeys;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (viewsAcceptable && viewsAcceptable.length > 0) {
|
||||
currentBoard = viewsAcceptable[0] as TIssueBoardKeys;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBoard) {
|
||||
if (activeLayout === null || activeLayout !== currentBoard) {
|
||||
if (projectStore?.activeBoard === null || projectStore?.activeBoard !== currentBoard) {
|
||||
let params: any = { board: currentBoard };
|
||||
if (peekId && peekId.length > 0) params = { ...params, peekId: peekId };
|
||||
if (priorities && priorities.length > 0) params = { ...params, priorities: priorities };
|
||||
if (states && states.length > 0) params = { ...params, states: states };
|
||||
if (labels && labels.length > 0) params = { ...params, labels: labels };
|
||||
console.log("params", params);
|
||||
|
||||
let storeParams: any = {};
|
||||
if (priorities && priorities.length > 0) storeParams = { ...storeParams, priority: priorities.split(",") };
|
||||
if (states && states.length > 0) storeParams = { ...storeParams, state: states.split(",") };
|
||||
if (labels && labels.length > 0) storeParams = { ...storeParams, labels: labels.split(",") };
|
||||
|
||||
if (storeParams) updateFilters(projectId, storeParams);
|
||||
setActiveLayout(currentBoard);
|
||||
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
|
||||
if (storeParams) updateFilters(project_slug, storeParams);
|
||||
|
||||
projectStore.setActiveBoard(currentBoard);
|
||||
router.push({
|
||||
pathname: `/${workspace_slug}/${project_slug}`,
|
||||
query: { ...params },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
board,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
workspace_slug,
|
||||
project_slug,
|
||||
router,
|
||||
projectStore,
|
||||
projectStore?.deploySettings,
|
||||
updateFilters,
|
||||
labels,
|
||||
states,
|
||||
priorities,
|
||||
peekId,
|
||||
settings,
|
||||
activeLayout,
|
||||
setActiveLayout,
|
||||
searchParams,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full items-center gap-4 px-5">
|
||||
{/* project detail */}
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
{project_details ? (
|
||||
{projectStore.project ? (
|
||||
<span className="h-7 w-7 flex-shrink-0 grid place-items-center">
|
||||
<ProjectLogo logo={project_details.logo_props} className="text-lg" />
|
||||
<ProjectLogo logo={projectStore.project.logo_props} className="text-lg" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||
@@ -111,18 +115,21 @@ const IssueNavbar: FC<IssueNavbarProps> = observer((props) => {
|
||||
</span>
|
||||
)}
|
||||
<div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium">
|
||||
{project_details?.name || `...`}
|
||||
{projectStore?.project?.name || `...`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* issue search bar */}
|
||||
<div className="w-full">{/* <NavbarSearch /> */}</div>
|
||||
|
||||
{/* issue views */}
|
||||
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
|
||||
<NavbarIssueBoardView layouts={views} />
|
||||
<NavbarIssueBoardView />
|
||||
</div>
|
||||
|
||||
{/* issue filters */}
|
||||
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
|
||||
<IssueFiltersDropdown workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssueFiltersDropdown />
|
||||
</div>
|
||||
|
||||
{/* theming */}
|
||||
@@ -130,14 +137,14 @@ const IssueNavbar: FC<IssueNavbarProps> = observer((props) => {
|
||||
<NavbarTheme />
|
||||
</div>
|
||||
|
||||
{user?.id ? (
|
||||
{user ? (
|
||||
<div className="flex items-center gap-2 rounded border border-custom-border-200 p-2">
|
||||
<Avatar name={user?.display_name} src={user?.avatar ?? undefined} shape="square" size="sm" />
|
||||
<h6 className="text-xs font-medium">{user.display_name}</h6>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-shrink-0">
|
||||
<Link href={`/?next_path=${pathName}`}>
|
||||
<Link href={`/?next_path=${router.asPath}`}>
|
||||
<Button variant="outline-primary">Sign in</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// constants
|
||||
import { issueViews } from "@/constants/data";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
// mobx
|
||||
import { TIssueBoardKeys } from "@/types/issue";
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { TIssueBoardKeys } from "types/issue";
|
||||
|
||||
type NavbarIssueBoardViewProps = {
|
||||
layouts: Record<TIssueBoardKeys, boolean>;
|
||||
};
|
||||
|
||||
export const NavbarIssueBoardView: FC<NavbarIssueBoardViewProps> = observer((props) => {
|
||||
const { layouts } = props;
|
||||
|
||||
const { activeLayout, setActiveLayout } = useProject();
|
||||
export const NavbarIssueBoardView = observer(() => {
|
||||
const {
|
||||
project: { viewOptions, setActiveBoard, activeBoard },
|
||||
}: RootStore = useMobxStore();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||
|
||||
const handleCurrentBoardView = (boardView: string) => {
|
||||
setActiveLayout(boardView as TIssueBoardKeys);
|
||||
setActiveBoard(boardView as TIssueBoardKeys);
|
||||
router.push(`/${workspace_slug}/${project_slug}?board=${boardView}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{layouts &&
|
||||
Object.keys(layouts).map((layoutKey: string) => {
|
||||
if (layouts[layoutKey as TIssueBoardKeys]) {
|
||||
{viewOptions &&
|
||||
Object.keys(viewOptions).map((viewKey: string) => {
|
||||
if (viewOptions[viewKey]) {
|
||||
return (
|
||||
<div
|
||||
key={layoutKey}
|
||||
key={viewKey}
|
||||
className={`flex h-[28px] w-[28px] cursor-pointer items-center justify-center rounded-sm ${
|
||||
layoutKey === activeLayout
|
||||
viewKey === activeBoard
|
||||
? `bg-custom-background-80 text-custom-text-200`
|
||||
: `text-custom-text-300 hover:bg-custom-background-80`
|
||||
}`}
|
||||
onClick={() => handleCurrentBoardView(layoutKey)}
|
||||
title={layoutKey}
|
||||
onClick={() => handleCurrentBoardView(viewKey)}
|
||||
title={viewKey}
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-rounded text-[18px] ${
|
||||
issueViews[layoutKey]?.className ? issueViews[layoutKey]?.className : ``
|
||||
issueViews[viewKey]?.className ? issueViews[viewKey]?.className : ``
|
||||
}`}
|
||||
>
|
||||
{issueViews[layoutKey]?.icon}
|
||||
{issueViews[viewKey]?.icon}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
// next theme
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
@@ -18,6 +16,7 @@ export const NavbarTheme = observer(() => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!theme) return;
|
||||
|
||||
setAppTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
// components
|
||||
import { EditorRefApi } from "@plane/lite-text-editor";
|
||||
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
|
||||
// hooks
|
||||
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
||||
import { useMobxStore, useUser } from "@/hooks/store";
|
||||
import useToast from "@/hooks/use-toast";
|
||||
// types
|
||||
import { Comment } from "@/types/issue";
|
||||
@@ -16,21 +17,22 @@ const defaultValues: Partial<Comment> = {
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const AddComment: React.FC<Props> = observer((props) => {
|
||||
export const AddComment: React.FC<Props> = observer(() => {
|
||||
// const { disabled = false } = props;
|
||||
const { workspaceSlug, projectId } = props;
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query;
|
||||
// store hooks
|
||||
const { workspace } = useProject();
|
||||
const { peekId: issueId, addIssueComment } = useIssueDetails();
|
||||
const { project } = useMobxStore();
|
||||
const { issueDetails: issueDetailStore } = useMobxStore();
|
||||
const { data: currentUser } = useUser();
|
||||
// derived values
|
||||
const workspaceId = workspace?.id;
|
||||
const workspaceId = project.workspace?.id;
|
||||
const issueId = issueDetailStore.peekId;
|
||||
// form info
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -43,9 +45,10 @@ export const AddComment: React.FC<Props> = observer((props) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const onSubmit = async (formData: Comment) => {
|
||||
if (!workspaceSlug || !projectId || !issueId || isSubmitting || !formData.comment_html) return;
|
||||
if (!workspace_slug || !project_slug || !issueId || isSubmitting || !formData.comment_html) return;
|
||||
|
||||
await addIssueComment(workspaceSlug, projectId, issueId, formData)
|
||||
await issueDetailStore
|
||||
.addIssueComment(workspace_slug.toString(), project_slug.toString(), issueId, formData)
|
||||
.then(() => {
|
||||
reset(defaultValues);
|
||||
editorRef.current?.clearEditor();
|
||||
@@ -72,7 +75,7 @@ export const AddComment: React.FC<Props> = observer((props) => {
|
||||
if (currentUser) handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
workspaceId={workspaceId as string}
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceSlug={workspace_slug as string}
|
||||
ref={editorRef}
|
||||
initialValue={
|
||||
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
||||
|
||||
@@ -10,7 +10,9 @@ import { CommentReactions } from "@/components/issues/peek-overview";
|
||||
// helpers
|
||||
import { timeAgo } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
||||
import { useMobxStore, useUser } from "@/hooks/store";
|
||||
// store
|
||||
import { RootStore } from "@/store/root.store";
|
||||
// types
|
||||
import { Comment } from "@/types/issue";
|
||||
|
||||
@@ -21,13 +23,12 @@ type Props = {
|
||||
|
||||
export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
const { comment, workspaceSlug } = props;
|
||||
// store hooks
|
||||
const { workspace } = useProject();
|
||||
const { peekId, deleteIssueComment, updateIssueComment } = useIssueDetails();
|
||||
const { data: currentUser } = useUser();
|
||||
// derived values
|
||||
const workspaceId = workspace?.id;
|
||||
const { project }: RootStore = useMobxStore();
|
||||
const workspaceId = project.workspace?.id;
|
||||
|
||||
// store
|
||||
const { issueDetails: issueDetailStore } = useMobxStore();
|
||||
const { data: currentUser } = useUser();
|
||||
// states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
// refs
|
||||
@@ -43,14 +44,15 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!workspaceSlug || !peekId) return;
|
||||
deleteIssueComment(workspaceSlug, comment.project, peekId, comment.id);
|
||||
if (!workspaceSlug || !issueDetailStore.peekId) return;
|
||||
issueDetailStore.deleteIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id);
|
||||
};
|
||||
|
||||
const handleCommentUpdate = async (formData: Comment) => {
|
||||
if (!workspaceSlug || !peekId) return;
|
||||
updateIssueComment(workspaceSlug, comment.project, peekId, comment.id, formData);
|
||||
if (!workspaceSlug || !issueDetailStore.peekId) return;
|
||||
issueDetailStore.updateIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id, formData);
|
||||
setIsEditing(false);
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
};
|
||||
@@ -133,7 +135,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
</form>
|
||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<LiteTextReadOnlyEditor ref={showEditorRef} initialValue={comment.comment_html} />
|
||||
<CommentReactions commentId={comment.id} projectId={comment.project} workspaceSlug={workspaceSlug} />
|
||||
<CommentReactions commentId={comment.id} projectId={comment.project} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,58 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// ui
|
||||
import { ReactionSelector } from "@/components/ui";
|
||||
// helpers
|
||||
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
|
||||
// hooks
|
||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
import { useMobxStore, useUser } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
commentId: string;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const CommentReactions: React.FC<Props> = observer((props) => {
|
||||
const { commentId, projectId, workspaceSlug } = props;
|
||||
const { commentId, projectId } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug } = router.query;
|
||||
// hooks
|
||||
const { addCommentReaction, removeCommentReaction, details, peekId } = useIssueDetails();
|
||||
const { issueDetails: issueDetailsStore } = useMobxStore();
|
||||
const { data: user } = useUser();
|
||||
|
||||
const commentReactions = peekId ? details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions : [];
|
||||
const peekId = issueDetailsStore.peekId;
|
||||
const commentReactions = peekId
|
||||
? issueDetailsStore.details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions
|
||||
: [];
|
||||
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
|
||||
|
||||
const userReactions = commentReactions?.filter((r) => r.actor_detail.id === user?.id);
|
||||
|
||||
const handleAddReaction = (reactionHex: string) => {
|
||||
if (!workspaceSlug || !projectId || !peekId) return;
|
||||
addCommentReaction(workspaceSlug, projectId, peekId, commentId, reactionHex);
|
||||
if (!workspace_slug || !projectId || !peekId) return;
|
||||
|
||||
issueDetailsStore.addCommentReaction(
|
||||
workspace_slug.toString(),
|
||||
projectId.toString(),
|
||||
peekId,
|
||||
commentId,
|
||||
reactionHex
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveReaction = (reactionHex: string) => {
|
||||
if (!workspaceSlug || !projectId || !peekId) return;
|
||||
removeCommentReaction(workspaceSlug, projectId, peekId, commentId, reactionHex);
|
||||
if (!workspace_slug || !projectId || !peekId) return;
|
||||
|
||||
issueDetailsStore.removeCommentReaction(
|
||||
workspace_slug.toString(),
|
||||
projectId.toString(),
|
||||
peekId,
|
||||
commentId,
|
||||
reactionHex
|
||||
);
|
||||
};
|
||||
|
||||
const handleReactionClick = (reactionHex: string) => {
|
||||
|
||||
@@ -13,12 +13,11 @@ import { IIssue } from "@/types/issue";
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
issueDetails: IIssue | undefined;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
workspace_slug: string;
|
||||
};
|
||||
|
||||
export const FullScreenPeekView: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, issueDetails, workspaceSlug, projectId } = props;
|
||||
const { handleClose, issueDetails } = props;
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-cols-10 divide-x divide-custom-border-200 overflow-hidden">
|
||||
@@ -36,11 +35,7 @@ export const FullScreenPeekView: React.FC<Props> = observer((props) => {
|
||||
<div className="my-5 h-[1] w-full border-t border-custom-border-200" />
|
||||
{/* issue activity/comments */}
|
||||
<div className="w-full pb-5">
|
||||
<PeekOverviewIssueActivity
|
||||
issueDetails={issueDetails}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<PeekOverviewIssueActivity issueDetails={issueDetails} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -2,17 +2,19 @@ import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { MoveRight } from "lucide-react";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
// ui
|
||||
import { Icon } from "@/components/ui";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useIssueDetails } from "@/hooks/store";
|
||||
import useToast from "@/hooks/use-toast";
|
||||
// store
|
||||
import { IPeekMode } from "@/store/issue-detail.store";
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
import { IPeekMode } from "@/store/issue_details";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
// lib
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
import { IIssue } from "types/issue";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
@@ -40,7 +42,7 @@ const peekModes: {
|
||||
export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
const { handleClose } = props;
|
||||
|
||||
const { peekMode, setPeekMode } = useIssueDetails();
|
||||
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@@ -60,19 +62,21 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{peekMode === "side" && (
|
||||
{issueDetailStore.peekMode === "side" && (
|
||||
<button type="button" onClick={handleClose}>
|
||||
<MoveRight className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
<Listbox
|
||||
as="div"
|
||||
value={peekMode}
|
||||
onChange={(val) => setPeekMode(val)}
|
||||
value={issueDetailStore.peekMode}
|
||||
onChange={(val) => issueDetailStore.setPeekMode(val)}
|
||||
className="relative flex-shrink-0 text-left"
|
||||
>
|
||||
<Listbox.Button className={`grid place-items-center ${peekMode === "full" ? "rotate-45" : ""}`}>
|
||||
<Icon iconName={peekModes.find((m) => m.key === peekMode)?.icon ?? ""} />
|
||||
<Listbox.Button
|
||||
className={`grid place-items-center ${issueDetailStore.peekMode === "full" ? "rotate-45" : ""}`}
|
||||
>
|
||||
<Icon iconName={peekModes.find((m) => m.key === issueDetailStore.peekMode)?.icon ?? ""} />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
@@ -117,7 +121,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
</Transition>
|
||||
</Listbox>
|
||||
</div>
|
||||
{(peekMode === "side" || peekMode === "modal") && (
|
||||
{(issueDetailStore.peekMode === "side" || issueDetailStore.peekMode === "modal") && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45 focus:outline-none" tabIndex={1}>
|
||||
<Icon iconName="link" />
|
||||
|
||||
@@ -1,48 +1,44 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
import { Button } from "@plane/ui";
|
||||
// components
|
||||
import { CommentCard, AddComment } from "@/components/issues/peek-overview";
|
||||
import { Icon } from "@/components/ui";
|
||||
// hooks
|
||||
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
||||
import { useMobxStore, useUser } from "@/hooks/store";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
|
||||
type Props = {
|
||||
issueDetails: IIssue;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
export const PeekOverviewIssueActivity: React.FC<Props> = observer(() => {
|
||||
// router
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { workspace_slug } = router.query;
|
||||
// store
|
||||
const { canComment } = useProject();
|
||||
const { details, peekId } = useIssueDetails();
|
||||
const { issueDetails: issueDetailStore, project: projectStore } = useMobxStore();
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
const comments = details[peekId || ""]?.comments || [];
|
||||
const comments = issueDetailStore.details[issueDetailStore.peekId || ""]?.comments || [];
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<h4 className="font-medium">Activity</h4>
|
||||
{workspaceSlug && (
|
||||
{workspace_slug && (
|
||||
<div className="mt-4">
|
||||
<div className="space-y-4">
|
||||
{comments.map((comment: any) => (
|
||||
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspaceSlug?.toString()} />
|
||||
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspace_slug?.toString()} />
|
||||
))}
|
||||
</div>
|
||||
{currentUser ? (
|
||||
<>
|
||||
{canComment && (
|
||||
{projectStore.deploySettings?.comments && (
|
||||
<div className="mt-4">
|
||||
<AddComment disabled={!currentUser} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<AddComment disabled={!currentUser} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -52,7 +48,7 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
||||
<Icon iconName="lock" className="!text-sm" />
|
||||
Sign in to add your comment
|
||||
</p>
|
||||
<Link href={`/?next_path=${pathname}`}>
|
||||
<Link href={`/?next_path=${router.asPath}`}>
|
||||
<Button variant="primary">Sign in</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// lib
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { ReactionSelector } from "@/components/ui";
|
||||
// helpers
|
||||
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
|
||||
// hooks
|
||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
import { useMobxStore, useUser } from "@/hooks/store";
|
||||
|
||||
type IssueEmojiReactionsProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
export const IssueEmojiReactions: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug } = router.query;
|
||||
// store
|
||||
const issueDetailsStore = useIssueDetails();
|
||||
const { issueDetails: issueDetailsStore } = useMobxStore();
|
||||
const { data: user, fetchCurrentUser } = useUser();
|
||||
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
@@ -26,17 +24,20 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||
const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);
|
||||
|
||||
const handleAddReaction = (reactionHex: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
issueDetailsStore.addIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex);
|
||||
if (!workspace_slug || !project_slug || !issueId) return;
|
||||
|
||||
issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||
};
|
||||
|
||||
const handleRemoveReaction = (reactionHex: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
issueDetailsStore.removeIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex);
|
||||
if (!workspace_slug || !project_slug || !issueId) return;
|
||||
|
||||
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||
};
|
||||
|
||||
const handleReactionClick = (reactionHex: string) => {
|
||||
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||
|
||||
if (userReaction) handleRemoveReaction(reactionHex);
|
||||
else handleAddReaction(reactionHex);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { issueGroupFilter, issuePriorityFilter } from "@/constants/data";
|
||||
import { renderFullDate } from "@/helpers/date-time.helper";
|
||||
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
|
||||
// types
|
||||
import { IPeekMode } from "@/store/issue-detail.store";
|
||||
import { IPeekMode } from "@/store/issue_details";
|
||||
// constants
|
||||
import useToast from "hooks/use-toast";
|
||||
import { IIssue } from "types/issue";
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import { useParams } from "next/navigation";
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview";
|
||||
import { useProject } from "@/hooks/store";
|
||||
|
||||
// type IssueReactionsProps = {
|
||||
// workspaceSlug: string;
|
||||
// projectId: string;
|
||||
// };
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
|
||||
export const IssueReactions: React.FC = () => {
|
||||
const { workspace_slug: workspaceSlug, project_id: projectId } = useParams<any>();
|
||||
|
||||
const { canVote, canReact } = useProject();
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
{canVote && (
|
||||
{projectStore?.deploySettings?.votes && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<IssueVotes />
|
||||
@@ -22,9 +14,9 @@ export const IssueReactions: React.FC = () => {
|
||||
<div className="h-8 w-0.5 bg-custom-background-200" />
|
||||
</>
|
||||
)}
|
||||
{canReact && (
|
||||
{projectStore?.deploySettings?.reactions && (
|
||||
<div className="flex items-center gap-2">
|
||||
<IssueEmojiReactions workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssueEmojiReactions />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
import { useMobxStore, useUser } from "@/hooks/store";
|
||||
|
||||
export const IssueVotes: React.FC = observer((props: any) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
// states
|
||||
export const IssueVotes: React.FC = observer(() => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const issueDetailsStore = useIssueDetails();
|
||||
const router = useRouter();
|
||||
|
||||
const { workspace_slug, project_slug } = router.query;
|
||||
|
||||
const { issueDetails: issueDetailsStore } = useMobxStore();
|
||||
const { data: user, fetchCurrentUser } = useUser();
|
||||
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
@@ -25,16 +26,16 @@ export const IssueVotes: React.FC = observer((props: any) => {
|
||||
const isDownVotedByUser = allDownVotes?.some((vote) => vote.actor === user?.id);
|
||||
|
||||
const handleVote = async (e: any, voteValue: 1 | -1) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
if (!workspace_slug || !project_slug || !issueId) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const actionPerformed = votes?.find((vote) => vote.actor === user?.id && vote.vote === voteValue);
|
||||
|
||||
if (actionPerformed)
|
||||
await issueDetailsStore.removeIssueVote(workspaceSlug.toString(), projectId.toString(), issueId);
|
||||
await issueDetailsStore.removeIssueVote(workspace_slug.toString(), project_slug.toString(), issueId);
|
||||
else
|
||||
await issueDetailsStore.addIssueVote(workspaceSlug.toString(), projectId.toString(), issueId, {
|
||||
await issueDetailsStore.addIssueVote(workspace_slug.toString(), project_slug.toString(), issueId, {
|
||||
vote: voteValue,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overview";
|
||||
// store
|
||||
import { useIssue, useIssueDetails } from "@/hooks/store";
|
||||
// lib
|
||||
import { useMobxStore } from "@/hooks/store";
|
||||
|
||||
export const IssuePeekOverview: React.FC = observer((props: any) => {
|
||||
const { workspaceSlug, projectId, peekId, board, priorities, states, labels } = props;
|
||||
export const IssuePeekOverview: React.FC = observer(() => {
|
||||
// states
|
||||
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false);
|
||||
const [isModalPeekOpen, setIsModalPeekOpen] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug, peekId, board, priorities, states, labels } = router.query as {
|
||||
workspace_slug: string;
|
||||
project_slug: string;
|
||||
peekId: string;
|
||||
board: string;
|
||||
priorities: string;
|
||||
states: string;
|
||||
labels: string;
|
||||
};
|
||||
// store
|
||||
const issueDetailStore = useIssueDetails();
|
||||
const issueStore = useIssue();
|
||||
|
||||
const { issueDetails: issueDetailStore, issue: issueStore } = useMobxStore();
|
||||
const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId && peekId && issueStore.issues && issueStore.issues.length > 0) {
|
||||
if (workspace_slug && project_slug && peekId && issueStore.issues && issueStore.issues.length > 0) {
|
||||
if (!issueDetails) {
|
||||
issueDetailStore.fetchIssueDetails(workspaceSlug.toString(), projectId.toString(), peekId.toString());
|
||||
issueDetailStore.fetchIssueDetails(workspace_slug.toString(), project_slug.toString(), peekId.toString());
|
||||
}
|
||||
}
|
||||
}, [workspaceSlug, projectId, issueDetailStore, issueDetails, peekId, issueStore.issues]);
|
||||
}, [workspace_slug, project_slug, issueDetailStore, issueDetails, peekId, issueStore.issues]);
|
||||
|
||||
const handleClose = () => {
|
||||
issueDetailStore.setPeekId(null);
|
||||
@@ -35,8 +45,10 @@ export const IssuePeekOverview: React.FC = observer((props: any) => {
|
||||
if (states && states.length > 0) params.states = states;
|
||||
if (priorities && priorities.length > 0) params.priorities = priorities;
|
||||
if (labels && labels.length > 0) params.labels = labels;
|
||||
// TODO: fix this redirection
|
||||
// router.push( encodeURI(`/${workspaceSlug?.toString()}/${projectId}`, ) { pathname: `/${workspaceSlug?.toString()}/${projectId}`, query: { ...params } });
|
||||
|
||||
router.replace({ pathname: `/${workspace_slug?.toString()}/${project_slug}`, query: { ...params } }, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -68,12 +80,7 @@ export const IssuePeekOverview: React.FC = observer((props: any) => {
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="fixed right-0 top-0 z-20 h-full w-1/2 bg-custom-background-100 shadow-custom-shadow-sm">
|
||||
<SidePeekView
|
||||
handleClose={handleClose}
|
||||
issueDetails={issueDetails}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<SidePeekView handleClose={handleClose} issueDetails={issueDetails} />
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
@@ -107,19 +114,13 @@ export const IssuePeekOverview: React.FC = observer((props: any) => {
|
||||
}`}
|
||||
>
|
||||
{issueDetailStore.peekMode === "modal" && (
|
||||
<SidePeekView
|
||||
handleClose={handleClose}
|
||||
issueDetails={issueDetails}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<SidePeekView handleClose={handleClose} issueDetails={issueDetails} />
|
||||
)}
|
||||
{issueDetailStore.peekMode === "full" && (
|
||||
<FullScreenPeekView
|
||||
workspace_slug={workspace_slug}
|
||||
handleClose={handleClose}
|
||||
issueDetails={issueDetails}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,18 +7,16 @@ import {
|
||||
PeekOverviewIssueDetails,
|
||||
PeekOverviewIssueProperties,
|
||||
} from "@/components/issues/peek-overview";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
|
||||
import { IIssue } from "types/issue";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
issueDetails: IIssue | undefined;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const SidePeekView: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, issueDetails, workspaceSlug, projectId } = props;
|
||||
const { handleClose, issueDetails } = props;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
@@ -39,11 +37,7 @@ export const SidePeekView: React.FC<Props> = observer((props) => {
|
||||
<div className="my-5 h-[1] w-full border-t border-custom-border-200" />
|
||||
{/* issue activity/comments */}
|
||||
<div className="w-full pb-5">
|
||||
<PeekOverviewIssueActivity
|
||||
issueDetails={issueDetails}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<PeekOverviewIssueActivity issueDetails={issueDetails} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
// ui
|
||||
@@ -7,7 +5,7 @@ import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { AuthRoot } from "@/components/accounts";
|
||||
import { AuthRoot, UserLoggedIn } from "@/components/accounts";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
// images
|
||||
@@ -19,15 +17,12 @@ export const AuthView = observer(() => {
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store
|
||||
const { fetchCurrentUser, isLoading } = useUser();
|
||||
const { data: currentUser, fetchCurrentUser, isLoading } = useUser();
|
||||
|
||||
// fetching user information
|
||||
const { isLoading: isSWRLoading } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
|
||||
shouldRetryOnError: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: true,
|
||||
errorRetryCount: 1,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -38,26 +33,30 @@ export const AuthView = observer(() => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative w-screen h-screen overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||
className="w-full h-full object-cover"
|
||||
alt="Plane background pattern"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
{currentUser ? (
|
||||
<UserLoggedIn />
|
||||
) : (
|
||||
<div className="relative w-screen h-screen overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||
className="w-full h-full object-cover"
|
||||
alt="Plane background pattern"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
|
||||
<AuthRoot />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
|
||||
<AuthRoot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { useRouter } from "next/router";
|
||||
// components
|
||||
import { IssueCalendarView } from "@/components/issues/board-views/calendar";
|
||||
import { IssueGanttView } from "@/components/issues/board-views/gantt";
|
||||
@@ -14,31 +11,16 @@ import { IssueSpreadsheetView } from "@/components/issues/board-views/spreadshee
|
||||
import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root";
|
||||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
// mobx store
|
||||
import { useIssue, useUser, useProject, useIssueDetails } from "@/hooks/store";
|
||||
import { useMobxStore, useUser } from "@/hooks/store";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
// assets
|
||||
import SomethingWentWrongImage from "public/something-went-wrong.svg";
|
||||
|
||||
type ProjectDetailsViewProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
peekId: string;
|
||||
};
|
||||
export const ProjectDetailsView = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspace_slug, project_slug, states, labels, priorities, peekId } = router.query;
|
||||
|
||||
export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, peekId } = props;
|
||||
// router
|
||||
const params = useParams();
|
||||
// store hooks
|
||||
const { fetchPublicIssues } = useIssue();
|
||||
const { activeLayout } = useProject();
|
||||
// fetching public issues
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? "PROJECT_PUBLIC_ISSUES" : null,
|
||||
workspaceSlug && projectId ? () => fetchPublicIssues(workspaceSlug, projectId, params) : null
|
||||
);
|
||||
// store hooks
|
||||
const issueStore = useIssue();
|
||||
const issueDetailStore = useIssueDetails();
|
||||
const { issue: issueStore, project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||
const { data: currentUser, fetchCurrentUser } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -48,14 +30,25 @@ export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props)
|
||||
}, [currentUser, fetchCurrentUser]);
|
||||
|
||||
useEffect(() => {
|
||||
if (peekId && workspaceSlug && projectId) {
|
||||
if (workspace_slug && project_slug) {
|
||||
const params = {
|
||||
state: states || null,
|
||||
labels: labels || null,
|
||||
priority: priorities || null,
|
||||
};
|
||||
issueStore.fetchPublicIssues(workspace_slug?.toString(), project_slug.toString(), params);
|
||||
}
|
||||
}, [workspace_slug, project_slug, issueStore, states, labels, priorities]);
|
||||
|
||||
useEffect(() => {
|
||||
if (peekId && workspace_slug && project_slug) {
|
||||
issueDetailStore.setPeekId(peekId.toString());
|
||||
}
|
||||
}, [peekId, issueDetailStore, projectId, workspaceSlug]);
|
||||
}, [peekId, issueDetailStore, project_slug, workspace_slug]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
{workspaceSlug && <IssuePeekOverview />}
|
||||
{workspace_slug && <IssuePeekOverview />}
|
||||
|
||||
{issueStore?.loader && !issueStore.issues ? (
|
||||
<div className="py-10 text-center text-sm text-custom-text-100">Loading...</div>
|
||||
@@ -74,24 +67,24 @@ export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props)
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
activeLayout && (
|
||||
projectStore?.activeBoard && (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{/* applied filters */}
|
||||
<IssueAppliedFilters />
|
||||
|
||||
{activeLayout === "list" && (
|
||||
{projectStore?.activeBoard === "list" && (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<IssueListView workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssueListView />
|
||||
</div>
|
||||
)}
|
||||
{activeLayout === "kanban" && (
|
||||
{projectStore?.activeBoard === "kanban" && (
|
||||
<div className="relative mx-auto h-full w-full p-5">
|
||||
<IssueKanbanView workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssueKanbanView />
|
||||
</div>
|
||||
)}
|
||||
{activeLayout === "calendar" && <IssueCalendarView />}
|
||||
{activeLayout === "spreadsheet" && <IssueSpreadsheetView />}
|
||||
{activeLayout === "gantt" && <IssueGanttView />}
|
||||
{projectStore?.activeBoard === "calendar" && <IssueCalendarView />}
|
||||
{projectStore?.activeBoard === "spreadsheet" && <IssueSpreadsheetView />}
|
||||
{projectStore?.activeBoard === "gantt" && <IssueGanttView />}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ILayoutDisplayFiltersOptions } from "@/types/issue-filters";
|
||||
|
||||
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
[pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions };
|
||||
} = {
|
||||
issues: {
|
||||
list: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
display_properties: null,
|
||||
display_filters: null,
|
||||
extra_options: null,
|
||||
},
|
||||
kanban: {
|
||||
filters: ["priority", "state", "labels"],
|
||||
display_properties: null,
|
||||
display_filters: null,
|
||||
extra_options: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -39,12 +39,12 @@ export enum EAuthenticationErrorCodes {
|
||||
INVALID_EMAIL = "5012",
|
||||
EMAIL_REQUIRED = "5013",
|
||||
// Sign Up
|
||||
USER_ACCOUNT_DEACTIVATED = "5019",
|
||||
USER_ALREADY_EXIST = "5003",
|
||||
REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5015",
|
||||
AUTHENTICATION_FAILED_SIGN_UP = "5006",
|
||||
INVALID_EMAIL_SIGN_UP = "5017",
|
||||
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5023",
|
||||
INVALID_EMAIL_MAGIC_SIGN_UP = "5019",
|
||||
// Sign In
|
||||
USER_DOES_NOT_EXIST = "5004",
|
||||
REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5014",
|
||||
@@ -140,14 +140,12 @@ const errorCodeMessages: {
|
||||
title: `Email and code required`,
|
||||
message: () => `Email and code required. Please try again.`,
|
||||
},
|
||||
|
||||
// sign in
|
||||
|
||||
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
|
||||
title: `User account deactivated`,
|
||||
message: () => <div>Your account is deactivated. Please reach out to support@plane.so</div>,
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
|
||||
// sign in
|
||||
[EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: {
|
||||
title: `User does not exist`,
|
||||
message: (email = undefined) => (
|
||||
@@ -252,6 +250,7 @@ export const authErrorHandler = (
|
||||
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP,
|
||||
EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP,
|
||||
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN,
|
||||
@@ -274,7 +273,6 @@ export const authErrorHandler = (
|
||||
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP,
|
||||
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN,
|
||||
EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED,
|
||||
EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED,
|
||||
];
|
||||
|
||||
if (toastAlertErrorCodes.includes(errorCode))
|
||||
|
||||
@@ -3,9 +3,6 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
|
||||
|
||||
export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || "";
|
||||
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
|
||||
|
||||
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
|
||||
|
||||
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
export * from "./user-mobx-provider";
|
||||
|
||||
export * from "./use-instance";
|
||||
export * from "./use-project";
|
||||
export * from "./use-issue";
|
||||
export * from "./use-user";
|
||||
export * from "./use-user-profile";
|
||||
export * from "./use-issue-details";
|
||||
export * from "./use-issue-filter";
|
||||
export * from "./user";
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// lib
|
||||
import { StoreContext } from "@/lib/app-providers";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import { IInstanceStore } from "@/store/instance.store";
|
||||
|
||||
export const useInstance = (): IInstanceStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
|
||||
if (context === undefined) throw new Error("useInstance must be used within StoreProvider");
|
||||
return context.instance;
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
// lib
|
||||
import { StoreContext } from "@/lib/app-providers";
|
||||
// store
|
||||
import { IIssueDetailStore } from "@/store/issue-detail.store";
|
||||
|
||||
export const useIssueDetails = (): IIssueDetailStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
|
||||
return context.issueDetail;
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
// lib
|
||||
import { StoreContext } from "@/lib/app-providers";
|
||||
// store
|
||||
import { IIssueFilterStore } from "@/store/issue-filters.store";
|
||||
|
||||
export const useIssueFilter = (): IIssueFilterStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
|
||||
return context.issueFilter;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user