Merge branch 'preview' of https://github.com/makeplane/plane into chore/onboarding-events

This commit is contained in:
LAKHAN BAHETI 2024-05-24 13:16:34 +05:30
commit dadb4415c7
122 changed files with 6429 additions and 1624 deletions

59
.eslintrc-staged.js Normal file
View File

@ -0,0 +1,59 @@
/**
* Adds three new lint plugins over the existing configuration:
* This is used to lint staged files only.
* We should remove this file once the entire codebase follows these rules.
*/
module.exports = {
root: true,
extends: [
"custom",
],
parser: "@typescript-eslint/parser",
settings: {
"import/resolver": {
typescript: {},
node: {
moduleDirectory: ["node_modules", "."],
},
},
},
rules: {
"import/order": [
"error",
{
groups: ["builtin", "external", "internal", "parent", "sibling"],
pathGroups: [
{
pattern: "react",
group: "external",
position: "before",
},
{
pattern: "lucide-react",
group: "external",
position: "after",
},
{
pattern: "@headlessui/**",
group: "external",
position: "after",
},
{
pattern: "@plane/**",
group: "external",
position: "after",
},
{
pattern: "@/**",
group: "internal",
},
],
pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
alphabetize: {
order: "asc",
caseInsensitive: true,
},
},
],
},
};

5
.gitignore vendored
View File

@ -81,4 +81,7 @@ tmp/
## packages
dist
.temp/
deploy/selfhost/plane-app/
deploy/selfhost/plane-app/
## Storybook
*storybook.log
output.css

0
.husky/pre-commit Normal file
View File

3
.lintstagedrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"*.{ts,tsx,js,jsx}": ["eslint -c ./.eslintrc-staged.js", "prettier --check"]
}

View File

@ -29,15 +29,21 @@ COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
@ -59,15 +65,21 @@ COPY --from=installer /app/admin/public ./admin/public
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1

View File

@ -1,5 +1,7 @@
import { ReactNode } from "react";
import Link from "next/link";
// helpers
import { SUPPORT_EMAIL } from "./common.helper";
export enum EPageTypes {
PUBLIC = "PUBLIC",
@ -38,6 +40,7 @@ export enum EAuthenticationErrorCodes {
ADMIN_AUTHENTICATION_FAILED = "5175",
ADMIN_USER_ALREADY_EXIST = "5180",
ADMIN_USER_DOES_NOT_EXIST = "5185",
ADMIN_USER_DEACTIVATED = "5190",
}
export type TAuthErrorInfo = {
@ -99,6 +102,10 @@ const errorCodeMessages: {
</div>
),
},
[EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED]: {
title: `User account deactivated`,
message: () => `User account deactivated. Please contact ${!!SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
},
};
export const authErrorHandler = (
@ -106,6 +113,7 @@ export const authErrorHandler = (
email?: string | undefined
): TAuthErrorInfo | undefined => {
const bannerAlertErrorCodes = [
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
@ -113,6 +121,7 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED,
];
if (bannerAlertErrorCodes.includes(errorCode))

View File

@ -10,6 +10,8 @@ export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "";
export const ASSET_PREFIX = ADMIN_BASE_PATH;
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));

View File

@ -2,11 +2,13 @@
import { FC, ReactNode } from "react";
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
// logo/ images
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png";
type TDefaultLayout = {
children: ReactNode;
@ -19,13 +21,16 @@ export const DefaultLayout: FC<TDefaultLayout> = (props) => {
const { resolvedTheme } = useTheme();
const patternBackground = resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern;
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<div className="relative">
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="container h-[110px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<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>
<Link href={`/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
</div>
{!withoutBackground && (

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -33,6 +33,7 @@ class UserSerializer(BaseSerializer):
"is_bot",
"is_password_autoset",
"is_email_verified",
"is_active",
]
extra_kwargs = {"password": {"write_only": True}}

View File

@ -198,46 +198,66 @@ class ModuleIssueViewSet(BaseViewSet):
]
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
# create multiple module inside an issue
# add multiple module inside an issue and remove multiple modules from an issue
def create_issue_modules(self, request, slug, project_id, issue_id):
modules = request.data.get("modules", [])
if not modules:
return Response(
{"error": "Modules are required"},
status=status.HTTP_400_BAD_REQUEST,
)
removed_modules = request.data.get("removed_modules", [])
project = Project.objects.get(pk=project_id)
_ = ModuleIssue.objects.bulk_create(
[
ModuleIssue(
if modules:
_ = ModuleIssue.objects.bulk_create(
[
ModuleIssue(
issue_id=issue_id,
module_id=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for module in modules
],
batch_size=10,
ignore_conflicts=True,
)
# Bulk Update the activity
_ = [
issue_activity.delay(
type="module.activity.created",
requested_data=json.dumps({"module_id": module}),
actor_id=str(request.user.id),
issue_id=issue_id,
module_id=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
for module in modules
],
batch_size=10,
ignore_conflicts=True,
)
# Bulk Update the activity
_ = [
issue_activity.delay(
type="module.activity.created",
requested_data=json.dumps({"module_id": module}),
actor_id=str(request.user.id),
issue_id=issue_id,
]
for module_id in removed_modules:
module_issue = ModuleIssue.objects.get(
workspace__slug=slug,
project_id=project_id,
current_instance=None,
module_id=module_id,
issue_id=issue_id,
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps({"module_id": str(module_id)}),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
{"module_name": module_issue.module.name}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
for module in modules
]
module_issue.delete()
return Response({"message": "success"}, status=status.HTTP_201_CREATED)

View File

@ -1,3 +1,6 @@
# Python imports
# import uuid
# Django imports
from django.db.models import Case, Count, IntegerField, Q, When
from django.contrib.auth import logout
@ -26,6 +29,7 @@ from plane.db.models import (
User,
WorkspaceMember,
WorkspaceMemberInvite,
Session,
)
from plane.license.models import Instance, InstanceAdmin
from plane.utils.cache import cache_response, invalidate_cache
@ -160,12 +164,13 @@ class UserEndpoint(BaseViewSet):
email=user.email,
).delete()
# Deactivate the user
user.is_active = False
# Delete all sessions
Session.objects.filter(user_id=request.user.id).delete()
# Profile updates
profile = Profile.objects.get(user=user)
# Reset onboarding
profile.last_workspace_id = None
profile.is_tour_completed = False
profile.is_onboarded = False
@ -177,7 +182,12 @@ class UserEndpoint(BaseViewSet):
}
profile.save()
# User log out
# Reset password
# user.is_password_autoset = True
# user.set_password(uuid.uuid4().hex)
# Deactivate the user
user.is_active = False
user.last_logout_ip = user_ip(request=request)
user.last_logout_time = timezone.now()
user.save()

View File

@ -85,5 +85,6 @@ class OauthAdapter(Adapter):
"refresh_token_expired_at"
),
"last_connected_at": timezone.now(),
"id_token": self.token_data.get("id_token", ""),
},
)

View File

@ -100,6 +100,7 @@ class GitHubOAuthProvider(OauthAdapter):
if token_response.get("refresh_token_expired_at")
else None
),
"id_token": token_response.get("id_token", ""),
}
)

View File

@ -98,6 +98,7 @@ class GoogleOAuthProvider(OauthAdapter):
if token_response.get("refresh_token_expired_at")
else None
),
"id_token": token_response.get("id_token", ""),
}
)

View File

@ -2,13 +2,12 @@ from django.urls import path
from .views import (
CSRFTokenEndpoint,
EmailCheckSignInEndpoint,
EmailCheckSignUpEndpoint,
ForgotPasswordEndpoint,
SetUserPasswordEndpoint,
ResetPasswordEndpoint,
ChangePasswordEndpoint,
# App
EmailCheckEndpoint,
GitHubCallbackEndpoint,
GitHubOauthInitiateEndpoint,
GoogleCallbackEndpoint,
@ -22,7 +21,7 @@ from .views import (
ForgotPasswordSpaceEndpoint,
ResetPasswordSpaceEndpoint,
# Space
EmailCheckEndpoint,
EmailCheckSpaceEndpoint,
GitHubCallbackSpaceEndpoint,
GitHubOauthInitiateSpaceEndpoint,
GoogleCallbackSpaceEndpoint,
@ -154,18 +153,13 @@ urlpatterns = [
),
# Email Check
path(
"sign-up/email-check/",
EmailCheckSignUpEndpoint.as_view(),
name="email-check-sign-up",
),
path(
"sign-in/email-check/",
EmailCheckSignInEndpoint.as_view(),
name="email-check-sign-in",
"email-check/",
EmailCheckEndpoint.as_view(),
name="email-check",
),
path(
"spaces/email-check/",
EmailCheckEndpoint.as_view(),
EmailCheckSpaceEndpoint.as_view(),
name="email-check",
),
# Password

View File

@ -4,7 +4,7 @@ from .common import (
SetUserPasswordEndpoint,
)
from .app.check import EmailCheckSignInEndpoint, EmailCheckSignUpEndpoint
from .app.check import EmailCheckEndpoint
from .app.email import (
SignInAuthEndpoint,
@ -47,7 +47,7 @@ from .space.magic import (
from .space.signout import SignOutAuthSpaceEndpoint
from .space.check import EmailCheckEndpoint
from .space.check import EmailCheckSpaceEndpoint
from .space.password_management import (
ForgotPasswordSpaceEndpoint,

View File

@ -1,3 +1,6 @@
# Python imports
import os
# Django imports
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
@ -16,8 +19,12 @@ from plane.authentication.adapter.error import (
AUTHENTICATION_ERROR_CODES,
)
from plane.authentication.rate_limit import AuthenticationThrottle
from plane.license.utils.instance_value import (
get_configuration_value,
)
class EmailCheckSignUpEndpoint(APIView):
class EmailCheckEndpoint(APIView):
permission_classes = [
AllowAny,
@ -28,128 +35,99 @@ 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 user already exist
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"USER_ALREADY_EXIST"
],
error_message="USER_ALREADY_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(
{"status": True},
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
(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"),
},
]
)
smtp_configured = bool(EMAIL_HOST)
is_magic_login_enabled = ENABLE_MAGIC_LINK_LOGIN == "1"
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:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
# Check if a user already exists with the given email
existing_user = User.objects.filter(email=email).first()
# 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,
"status": (
"MAGIC_CODE"
if existing_user.is_password_autoset
and smtp_configured
and is_magic_login_enabled
else "CREDENTIAL"
),
},
status=status.HTTP_200_OK,
)
except ValidationError:
raise 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
)
class EmailCheckSignInEndpoint(APIView):
permission_classes = [
AllowAny,
]
throttle_classes = [
AuthenticationThrottle,
]
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 true
return Response(
{
"status": True,
"is_password_autoset": existing_user.is_password_autoset,
},
status=status.HTTP_200_OK,
)
# Raise error
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
error_message="USER_DOES_NOT_EXIST",
)
except ValidationError:
raise 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
)
# Else return response
return Response(
{
"existing": False,
"status": (
"MAGIC_CODE"
if smtp_configured and is_magic_login_enabled
else "CREDENTIAL"
),
},
status=status.HTTP_200_OK,
)

View File

@ -1,6 +1,3 @@
# Python imports
from urllib.parse import urljoin
# Django imports
from django.views import View
from django.contrib.auth import logout
@ -23,9 +20,10 @@ class SignOutAuthEndpoint(View):
user.save()
# Log the user out
logout(request)
url = urljoin(base_host(request=request, is_app=True), "sign-in")
return HttpResponseRedirect(url)
return HttpResponseRedirect(
base_host(request=request, is_app=True)
)
except Exception:
return HttpResponseRedirect(
base_host(request=request, is_app=True), "sign-in"
base_host(request=request, is_app=True)
)

View File

@ -1,3 +1,6 @@
# Python imports
import os
# Django imports
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
@ -16,8 +19,10 @@ from plane.authentication.adapter.error import (
AuthenticationException,
)
from plane.authentication.rate_limit import AuthenticationThrottle
from plane.license.utils.instance_value import get_configuration_value
class EmailCheckEndpoint(APIView):
class EmailCheckSpaceEndpoint(APIView):
permission_classes = [
AllowAny,
@ -42,6 +47,22 @@ class EmailCheckEndpoint(APIView):
status=status.HTTP_400_BAD_REQUEST,
)
(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"),
},
]
)
smtp_configured = bool(EMAIL_HOST)
is_magic_login_enabled = ENABLE_MAGIC_LINK_LOGIN == "1"
email = request.data.get("email", False)
# Return error if email is not present
@ -86,12 +107,25 @@ class EmailCheckEndpoint(APIView):
return Response(
{
"existing": True,
"is_password_autoset": existing_user.is_password_autoset,
"status": (
"MAGIC_CODE"
if existing_user.is_password_autoset
and smtp_configured
and is_magic_login_enabled
else "CREDENTIAL"
),
},
status=status.HTTP_200_OK,
)
# Else return response
return Response(
{"existing": False, "is_password_autoset": False},
{
"existing": False,
"status": (
"MAGIC_CODE"
if smtp_configured and is_magic_login_enabled
else "CREDENTIAL"
),
},
status=status.HTTP_200_OK,
)

View File

@ -0,0 +1,58 @@
# Generated by Django 4.2.11 on 2024-05-22 15:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("db", "0065_auto_20240415_0937"),
]
operations = [
migrations.AddField(
model_name="account",
name="id_token",
field=models.TextField(blank=True),
),
migrations.AddField(
model_name="cycle",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="module",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="issueview",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="inbox",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="dashboard",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="widget",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="issue",
name="description_binary",
field=models.BinaryField(null=True),
),
migrations.AddField(
model_name="team",
name="logo_props",
field=models.JSONField(default=dict),
),
]

View File

@ -70,6 +70,7 @@ class Cycle(ProjectBaseModel):
external_id = models.CharField(max_length=255, blank=True, null=True)
progress_snapshot = models.JSONField(default=dict)
archived_at = models.DateTimeField(null=True)
logo_props = models.JSONField(default=dict)
class Meta:
verbose_name = "Cycle"

View File

@ -31,6 +31,7 @@ class Dashboard(BaseModel):
verbose_name="Dashboard Type",
default="home",
)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the dashboard"""
@ -53,6 +54,7 @@ class Widget(TimeAuditModel):
)
key = models.CharField(max_length=255)
filters = models.JSONField(default=dict)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the widget"""

View File

@ -12,6 +12,7 @@ class Inbox(ProjectBaseModel):
)
is_default = models.BooleanField(default=False)
view_props = models.JSONField(default=dict)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the Inbox"""

View File

@ -128,6 +128,7 @@ class Issue(ProjectBaseModel):
description = models.JSONField(blank=True, default=dict)
description_html = models.TextField(blank=True, default="<p></p>")
description_stripped = models.TextField(blank=True, null=True)
description_binary = models.BinaryField(null=True)
priority = models.CharField(
max_length=30,
choices=PRIORITY_CHOICES,

View File

@ -93,6 +93,7 @@ class Module(ProjectBaseModel):
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
archived_at = models.DateTimeField(null=True)
logo_props = models.JSONField(default=dict)
class Meta:
unique_together = ["name", "project"]

View File

@ -189,6 +189,7 @@ class Account(TimeAuditModel):
refresh_token = models.TextField(null=True, blank=True)
refresh_token_expired_at = models.DateTimeField(null=True)
last_connected_at = models.DateTimeField(default=timezone.now)
id_token = models.TextField(blank=True)
metadata = models.JSONField(default=dict)
class Meta:

View File

@ -52,6 +52,7 @@ def get_default_display_properties():
}
# DEPRECATED TODO: - Remove in next release
class GlobalView(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
@ -87,7 +88,6 @@ class GlobalView(BaseModel):
return f"{self.name} <{self.workspace.name}>"
# DEPRECATED TODO: - Remove in next release
class IssueView(WorkspaceBaseModel):
name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True)
@ -101,6 +101,7 @@ class IssueView(WorkspaceBaseModel):
default=1, choices=((0, "Private"), (1, "Public"))
)
sort_order = models.FloatField(default=65535)
logo_props = models.JSONField(default=dict)
class Meta:
verbose_name = "Issue View"

View File

@ -244,6 +244,7 @@ class Team(BaseModel):
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="workspace_team"
)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the team"""

View File

@ -97,7 +97,7 @@ services:
- dev_env
volumes:
- ./apiserver:/code
command: ./bin/docker-entrypoint-api.sh
command: ./bin/docker-entrypoint-api-local.sh
env_file:
- ./apiserver/.env
depends_on:

View File

@ -53,7 +53,6 @@ http {
}
location /spaces/ {
rewrite ^/spaces/?$ /spaces/login break;
proxy_http_version 1.1;
proxy_set_header Upgrade ${dollar}http_upgrade;
proxy_set_header Connection "upgrade";

View File

@ -53,7 +53,6 @@ http {
}
location /spaces/ {
rewrite ^/spaces/?$ /spaces/login break;
proxy_http_version 1.1;
proxy_set_header Upgrade ${dollar}http_upgrade;
proxy_set_header Connection "upgrade";

View File

@ -21,11 +21,15 @@
"start": "turbo run start",
"lint": "turbo run lint",
"clean": "turbo run clean",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"prepare": "husky"
},
"devDependencies": {
"autoprefixer": "^10.4.15",
"eslint-config-custom": "*",
"eslint-plugin-prettier": "^5.1.3",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"postcss": "^8.4.29",
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",

View File

@ -16,6 +16,7 @@ module.exports = {
"./ui/**/*.tsx",
"../packages/ui/**/*.{js,ts,jsx,tsx}",
"../packages/editor/**/src/**/*.{js,ts,jsx,tsx}",
"!../packages/ui/**/*.stories{js,ts,jsx,tsx}",
],
},
theme: {
@ -108,6 +109,7 @@ module.exports = {
100: convertToRGB("--color-text-100"),
200: convertToRGB("--color-text-200"),
300: convertToRGB("--color-text-300"),
350: convertToRGB("--color-text-350"),
400: convertToRGB("--color-text-400"),
500: convertToRGB("--color-text-500"),
600: convertToRGB("--color-text-600"),

View File

@ -5,8 +5,7 @@ export interface IEmailCheckData {
}
export interface IEmailCheckResponse {
is_password_autoset: boolean;
status: boolean;
status: "MAGIC_CODE" | "CREDENTIAL";
existing: boolean;
}

View File

@ -0,0 +1,28 @@
import type { StorybookConfig } from "@storybook/react-webpack5";
import { join, dirname } from "path";
/**
* This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
*/
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, "package.json")));
}
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
getAbsolutePath("@storybook/addon-webpack5-compiler-swc"),
getAbsolutePath("@storybook/addon-onboarding"),
getAbsolutePath("@storybook/addon-links"),
getAbsolutePath("@storybook/addon-essentials"),
getAbsolutePath("@chromatic-com/storybook"),
getAbsolutePath("@storybook/addon-interactions"),
"@storybook/addon-styling-webpack"
],
framework: {
name: getAbsolutePath("@storybook/react-webpack5"),
options: {},
},
};
export default config;

View File

@ -0,0 +1,14 @@
import type { Preview } from "@storybook/react";
import "../styles/output.css";
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

@ -14,7 +14,10 @@
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts --external react --minify",
"dev": "tsup src/index.ts --format esm,cjs --watch --dts --external react",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"postcss": "postcss styles/globals.css -o styles/output.css --watch"
},
"dependencies": {
"@blueprintjs/core": "^4.16.3",
@ -30,14 +33,30 @@
"tailwind-merge": "^2.0.0"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.4.0",
"@storybook/addon-essentials": "^8.1.1",
"@storybook/addon-interactions": "^8.1.1",
"@storybook/addon-links": "^8.1.1",
"@storybook/addon-onboarding": "^8.1.1",
"@storybook/addon-styling-webpack": "^1.0.0",
"@storybook/addon-webpack5-compiler-swc": "^1.0.2",
"@storybook/blocks": "^8.1.1",
"@storybook/react": "^8.1.1",
"@storybook/react-webpack5": "^8.1.1",
"@storybook/test": "^8.1.1",
"@types/node": "^20.5.2",
"@types/react": "^18.2.42",
"@types/react-color": "^3.0.9",
"@types/react-dom": "^18.2.17",
"autoprefixer": "^10.4.19",
"classnames": "^2.3.2",
"eslint-config-custom": "*",
"postcss-cli": "^11.0.0",
"postcss-nested": "^6.0.1",
"react": "^18.2.0",
"storybook": "^8.1.1",
"tailwind-config-custom": "*",
"tailwindcss": "^3.4.3",
"tsconfig": "*",
"tsup": "^5.10.1",
"typescript": "4.7.4"

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { Avatar } from "./avatar";
const meta: Meta<typeof Avatar> = {
title: "Avatar",
component: Avatar,
};
export default meta;
type Story = StoryObj<typeof Avatar>;
export const Default: Story = {
args: { name: "John Doe" },
};
export const Large: Story = {
args: { name: "John Doe" },
};

View File

@ -44,9 +44,9 @@ export const buttonStyling: IButtonStyling = {
disabled: `cursor-not-allowed !bg-custom-primary-60 hover:bg-custom-primary-60`,
},
"accent-primary": {
default: `bg-custom-primary-10 text-custom-primary-100`,
hover: `hover:bg-custom-primary-20 hover:text-custom-primary-200`,
pressed: `focus:bg-custom-primary-20`,
default: `bg-custom-primary-100/20 text-custom-primary-100`,
hover: `hover:bg-custom-primary-100/10 hover:text-custom-primary-200`,
pressed: `focus:bg-custom-primary-100/10`,
disabled: `cursor-not-allowed !text-custom-primary-60`,
},
"outline-primary": {

View File

@ -0,0 +1,650 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.text-1\.5xl {
font-size: 1.375rem;
line-height: 1.875rem;
}
.text-2\.5xl {
font-size: 1.75rem;
line-height: 2.25rem;
}
}
@layer base {
html {
font-family: "Inter", sans-serif;
}
:root {
color-scheme: light !important;
--color-primary-10: 236, 241, 255;
--color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
--color-primary-40: 178, 200, 255;
--color-primary-50: 159, 187, 255;
--color-primary-60: 140, 173, 255;
--color-primary-70: 121, 159, 255;
--color-primary-80: 101, 145, 255;
--color-primary-90: 82, 132, 255;
--color-primary-100: 63, 118, 255;
--color-primary-200: 57, 106, 230;
--color-primary-300: 50, 94, 204;
--color-primary-400: 44, 83, 179;
--color-primary-500: 38, 71, 153;
--color-primary-600: 32, 59, 128;
--color-primary-700: 25, 47, 102;
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 247, 247, 247; /* secondary bg */
--color-background-80: 232, 232, 232; /* tertiary bg */
--color-text-100: 23, 23, 23; /* primary text */
--color-text-200: 58, 58, 58; /* secondary text */
--color-text-300: 82, 82, 82; /* tertiary text */
--color-text-400: 163, 163, 163; /* placeholder text */
--color-scrollbar: 163, 163, 163; /* scrollbar thumb */
--color-border-100: 245, 245, 245; /* subtle border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */
--color-shadow-2xs: 0px 0px 1px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.06),
0px 1px 2px 0px rgba(23, 23, 23, 0.14);
--color-shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12),
0px 1px 8px -1px rgba(16, 24, 40, 0.1);
--color-shadow-sm: 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02),
0px 1px 12px 0px rgba(0, 0, 0, 0.12);
--color-shadow-rg: 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08),
0px 1px 12px 0px rgba(16, 24, 40, 0.04);
--color-shadow-md: 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12),
0px 1px 16px 0px rgba(16, 24, 40, 0.12);
--color-shadow-lg: 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
0px 1px 24px 0px rgba(16, 24, 40, 0.12);
--color-shadow-xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16),
0px 0px 52px 0px rgba(16, 24, 40, 0.16);
--color-shadow-2xl: 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 12px 24px 0px rgba(16, 24, 40, 0.12),
0px 1px 32px 0px rgba(16, 24, 40, 0.12);
--color-shadow-3xl: 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12),
0px 1px 48px 0px rgba(16, 24, 40, 0.12);
--color-shadow-4xl: 0px 8px 40px 0px rgba(0, 0, 61, 0.05), 0px 12px 32px -16px rgba(0, 0, 0, 0.05);
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
--color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
--color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
--color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
--color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
--color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
--color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
--color-sidebar-shadow-2xs: var(--color-shadow-2xs);
--color-sidebar-shadow-xs: var(--color-shadow-xs);
--color-sidebar-shadow-sm: var(--color-shadow-sm);
--color-sidebar-shadow-rg: var(--color-shadow-rg);
--color-sidebar-shadow-md: var(--color-shadow-md);
--color-sidebar-shadow-lg: var(--color-shadow-lg);
--color-sidebar-shadow-xl: var(--color-shadow-xl);
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
--color-sidebar-shadow-4xl: var(--color-shadow-4xl);
}
[data-theme="light"],
[data-theme="light-contrast"] {
color-scheme: light !important;
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 247, 247, 247; /* secondary bg */
--color-background-80: 232, 232, 232; /* tertiary bg */
}
[data-theme="light"] {
--color-text-100: 23, 23, 23; /* primary text */
--color-text-200: 58, 58, 58; /* secondary text */
--color-text-300: 82, 82, 82; /* tertiary text */
--color-text-400: 163, 163, 163; /* placeholder text */
--color-scrollbar: 163, 163, 163; /* scrollbar thumb */
--color-border-100: 245, 245, 245; /* subtle border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */
/* onboarding colors */
--gradient-onboarding-100: linear-gradient(106deg, #f2f6ff 29.8%, #e1eaff 99.34%);
--gradient-onboarding-200: linear-gradient(129deg, rgba(255, 255, 255, 0) -22.23%, rgba(255, 255, 255, 0.8) 62.98%);
--gradient-onboarding-300: linear-gradient(164deg, #fff 4.25%, rgba(255, 255, 255, 0.06) 93.5%);
--gradient-onboarding-400: linear-gradient(129deg, rgba(255, 255, 255, 0) -22.23%, rgba(255, 255, 255, 0.8) 62.98%);
--color-onboarding-text-100: 23, 23, 23;
--color-onboarding-text-200: 58, 58, 58;
--color-onboarding-text-300: 82, 82, 82;
--color-onboarding-text-400: 163, 163, 163;
--color-onboarding-background-100: 236, 241, 255;
--color-onboarding-background-200: 255, 255, 255;
--color-onboarding-background-300: 236, 241, 255;
--color-onboarding-background-400: 177, 206, 250;
--color-onboarding-border-100: 229, 229, 229;
--color-onboarding-border-200: 217, 228, 255;
--color-onboarding-border-300: 229, 229, 229, 0.5;
--color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(126, 139, 171, 0.1);
/* toast theme */
--color-toast-success-text: 62, 155, 79;
--color-toast-error-text: 220, 62, 66;
--color-toast-warning-text: 255, 186, 24;
--color-toast-info-text: 51, 88, 212;
--color-toast-loading-text: 28, 32, 36;
--color-toast-secondary-text: 128, 131, 141;
--color-toast-tertiary-text: 96, 100, 108;
--color-toast-success-background: 253, 253, 254;
--color-toast-error-background: 255, 252, 252;
--color-toast-warning-background: 254, 253, 251;
--color-toast-info-background: 253, 253, 254;
--color-toast-loading-background: 253, 253, 254;
--color-toast-success-border: 218, 241, 219;
--color-toast-error-border: 255, 219, 220;
--color-toast-warning-border: 255, 247, 194;
--color-toast-info-border: 210, 222, 255;
--color-toast-loading-border: 224, 225, 230;
}
[data-theme="light-contrast"] {
--color-text-100: 11, 11, 11; /* primary text */
--color-text-200: 38, 38, 38; /* secondary text */
--color-text-300: 58, 58, 58; /* tertiary text */
--color-text-400: 115, 115, 115; /* placeholder text */
--color-scrollbar: 115, 115, 115; /* scrollbar thumb */
--color-border-100: 34, 34, 34; /* subtle border= 1 */
--color-border-200: 38, 38, 38; /* subtle border- 2 */
--color-border-300: 46, 46, 46; /* strong border- 1 */
--color-border-400: 58, 58, 58; /* strong border- 2 */
}
[data-theme="dark"],
[data-theme="dark-contrast"] {
color-scheme: dark !important;
--color-background-100: 25, 25, 25; /* primary bg */
--color-background-90: 32, 32, 32; /* secondary bg */
--color-background-80: 44, 44, 44; /* tertiary bg */
--color-shadow-2xs: 0px 0px 1px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.5);
--color-shadow-xs: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
--color-shadow-sm: 0px 0px 4px 0px rgba(0, 0, 0, 0.2), 0px 2px 6px 0px rgba(0, 0, 0, 0.5);
--color-shadow-rg: 0px 0px 6px 0px rgba(0, 0, 0, 0.2), 0px 4px 6px 0px rgba(0, 0, 0, 0.5);
--color-shadow-md: 0px 2px 8px 0px rgba(0, 0, 0, 0.2), 0px 4px 8px 0px rgba(0, 0, 0, 0.5);
--color-shadow-lg: 0px 4px 12px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.55);
--color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
--color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
--color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
}
[data-theme="dark"] {
--color-text-100: 229, 229, 229; /* primary text */
--color-text-200: 163, 163, 163; /* secondary text */
--color-text-300: 115, 115, 115; /* tertiary text */
--color-text-400: 82, 82, 82; /* placeholder text */
--color-scrollbar: 82, 82, 82; /* scrollbar thumb */
--color-border-100: 34, 34, 34; /* subtle border= 1 */
--color-border-200: 38, 38, 38; /* subtle border- 2 */
--color-border-300: 46, 46, 46; /* strong border- 1 */
--color-border-400: 58, 58, 58; /* strong border- 2 */
/* onboarding colors */
--gradient-onboarding-100: linear-gradient(106deg, #18191b 25.17%, #18191b 99.34%);
--gradient-onboarding-200: linear-gradient(129deg, rgba(47, 49, 53, 0.8) -22.23%, rgba(33, 34, 37, 0.8) 62.98%);
--gradient-onboarding-300: linear-gradient(167deg, rgba(47, 49, 53, 0.45) 19.22%, #212225 98.48%);
--color-onboarding-text-100: 237, 238, 240;
--color-onboarding-text-200: 176, 180, 187;
--color-onboarding-text-300: 118, 123, 132;
--color-onboarding-text-400: 105, 110, 119;
--color-onboarding-background-100: 54, 58, 64;
--color-onboarding-background-200: 40, 42, 45;
--color-onboarding-background-300: 40, 42, 45;
--color-onboarding-background-400: 67, 72, 79;
--color-onboarding-border-100: 54, 58, 64;
--color-onboarding-border-200: 54, 58, 64;
--color-onboarding-border-300: 34, 35, 38, 0.5;
--color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(39, 44, 56, 0.1);
/* toast theme */
--color-toast-success-text: 178, 221, 181;
--color-toast-error-text: 206, 44, 49;
--color-toast-warning-text: 255, 186, 24;
--color-toast-info-text: 141, 164, 239;
--color-toast-loading-text: 255, 255, 255;
--color-toast-secondary-text: 185, 187, 198;
--color-toast-tertiary-text: 139, 141, 152;
--color-toast-success-background: 46, 46, 46;
--color-toast-error-background: 46, 46, 46;
--color-toast-warning-background: 46, 46, 46;
--color-toast-info-background: 46, 46, 46;
--color-toast-loading-background: 46, 46, 46;
--color-toast-success-border: 42, 126, 59;
--color-toast-error-border: 100, 23, 35;
--color-toast-warning-border: 79, 52, 34;
--color-toast-info-border: 58, 91, 199;
--color-toast-loading-border: 96, 100, 108;
}
[data-theme="dark-contrast"] {
--color-text-100: 250, 250, 250; /* primary text */
--color-text-200: 241, 241, 241; /* secondary text */
--color-text-300: 212, 212, 212; /* tertiary text */
--color-text-400: 115, 115, 115; /* placeholder text */
--color-scrollbar: 115, 115, 115; /* scrollbar thumb */
--color-border-100: 245, 245, 245; /* subtle border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */
}
[data-theme="light"],
[data-theme="dark"],
[data-theme="light-contrast"],
[data-theme="dark-contrast"] {
--color-primary-10: 236, 241, 255;
--color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
--color-primary-40: 178, 200, 255;
--color-primary-50: 159, 187, 255;
--color-primary-60: 140, 173, 255;
--color-primary-70: 121, 159, 255;
--color-primary-80: 101, 145, 255;
--color-primary-90: 82, 132, 255;
--color-primary-100: 63, 118, 255;
--color-primary-200: 57, 106, 230;
--color-primary-300: 50, 94, 204;
--color-primary-400: 44, 83, 179;
--color-primary-500: 38, 71, 153;
--color-primary-600: 32, 59, 128;
--color-primary-700: 25, 47, 102;
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
--color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
--color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
--color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
--color-sidebar-border-200: var(--color-border-200); /* subtle sidebar border- 2 */
--color-sidebar-border-300: var(--color-border-300); /* strong sidebar border- 1 */
--color-sidebar-border-400: var(--color-border-400); /* strong sidebar border- 2 */
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-variant-ligatures: none;
-webkit-font-variant-ligatures: none;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
body {
color: rgba(var(--color-text-100));
}
/* scrollbar style */
::-webkit-scrollbar {
display: none;
}
.tags-input-container {
border: 2px solid #000;
padding: 0.5em;
border-radius: 3px;
width: min(80vw, 600px);
margin-top: 1em;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
}
.tag-item {
background-color: rgb(218, 216, 216);
display: inline-block;
padding: 0.5em 0.75em;
border-radius: 20px;
}
.tag-item .close {
height: 20px;
width: 20px;
background-color: rgb(48, 48, 48);
color: #fff;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
margin-left: 0.5em;
font-size: 18px;
cursor: pointer;
}
.tags-input {
flex-grow: 1;
padding: 0.5em 0;
border: none;
outline: none;
}
/* emoji icon picker */
.conical-gradient {
background: conic-gradient(
from 180deg at 50% 50%,
#ff6b00 0deg,
#f7ae59 70.5deg,
#3f76ff 151.12deg,
#05c3ff 213deg,
#18914f 289.87deg,
#f6f172 329.25deg,
#ff6b00 360deg
);
}
/* progress bar */
.progress-bar {
fill: currentColor;
color: rgba(var(--color-sidebar-background-100));
}
/* lineclamp */
.lineclamp {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
/* popover2 styling */
.bp4-popover2-transition-container {
z-index: 1 !important;
}
::-webkit-input-placeholder,
::placeholder,
:-ms-input-placeholder {
color: rgb(var(--color-text-400));
}
.bp4-overlay-content {
z-index: 555 !important;
}
.disable-scroll {
overflow: hidden !important;
}
.vertical-lr {
writing-mode: vertical-lr;
-webkit-writing-mode: vertical-lr;
-ms-writing-mode: vertical-lr;
width: fit-content;
}
div.web-view-spinner {
position: relative;
width: 54px;
height: 54px;
display: inline-block;
margin-left: 50%;
margin-right: 50%;
padding: 10px;
border-radius: 10px;
}
div.web-view-spinner div {
width: 6%;
height: 16%;
background: rgb(var(--color-text-400));
position: absolute;
left: 49%;
top: 43%;
opacity: 0;
border-radius: 50px;
-webkit-border-radius: 50px;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
-webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
animation: fade 1s linear infinite;
-webkit-animation: fade 1s linear infinite;
}
@keyframes fade {
from {
opacity: 1;
}
to {
opacity: 0.25;
}
}
@-webkit-keyframes fade {
from {
opacity: 1;
}
to {
opacity: 0.25;
}
}
div.web-view-spinner div.bar1 {
transform: rotate(0deg) translate(0, -130%);
-webkit-transform: rotate(0deg) translate(0, -130%);
animation-delay: 0s;
-webkit-animation-delay: 0s;
}
div.web-view-spinner div.bar2 {
transform: rotate(30deg) translate(0, -130%);
-webkit-transform: rotate(30deg) translate(0, -130%);
animation-delay: -0.9167s;
-webkit-animation-delay: -0.9167s;
}
div.web-view-spinner div.bar3 {
transform: rotate(60deg) translate(0, -130%);
-webkit-transform: rotate(60deg) translate(0, -130%);
animation-delay: -0.833s;
-webkit-animation-delay: -0.833s;
}
div.web-view-spinner div.bar4 {
transform: rotate(90deg) translate(0, -130%);
-webkit-transform: rotate(90deg) translate(0, -130%);
animation-delay: -0.7497s;
-webkit-animation-delay: -0.7497s;
}
div.web-view-spinner div.bar5 {
transform: rotate(120deg) translate(0, -130%);
-webkit-transform: rotate(120deg) translate(0, -130%);
animation-delay: -0.667s;
-webkit-animation-delay: -0.667s;
}
div.web-view-spinner div.bar6 {
transform: rotate(150deg) translate(0, -130%);
-webkit-transform: rotate(150deg) translate(0, -130%);
animation-delay: -0.5837s;
-webkit-animation-delay: -0.5837s;
}
div.web-view-spinner div.bar7 {
transform: rotate(180deg) translate(0, -130%);
-webkit-transform: rotate(180deg) translate(0, -130%);
animation-delay: -0.5s;
-webkit-animation-delay: -0.5s;
}
div.web-view-spinner div.bar8 {
transform: rotate(210deg) translate(0, -130%);
-webkit-transform: rotate(210deg) translate(0, -130%);
animation-delay: -0.4167s;
-webkit-animation-delay: -0.4167s;
}
div.web-view-spinner div.bar9 {
transform: rotate(240deg) translate(0, -130%);
-webkit-transform: rotate(240deg) translate(0, -130%);
animation-delay: -0.333s;
-webkit-animation-delay: -0.333s;
}
div.web-view-spinner div.bar10 {
transform: rotate(270deg) translate(0, -130%);
-webkit-transform: rotate(270deg) translate(0, -130%);
animation-delay: -0.2497s;
-webkit-animation-delay: -0.2497s;
}
div.web-view-spinner div.bar11 {
transform: rotate(300deg) translate(0, -130%);
-webkit-transform: rotate(300deg) translate(0, -130%);
animation-delay: -0.167s;
-webkit-animation-delay: -0.167s;
}
div.web-view-spinner div.bar12 {
transform: rotate(330deg) translate(0, -130%);
-webkit-transform: rotate(330deg) translate(0, -130%);
animation-delay: -0.0833s;
-webkit-animation-delay: -0.0833s;
}
@-moz-document url-prefix() {
* {
scrollbar-width: none;
}
.vertical-scrollbar,
.horizontal-scrollbar {
scrollbar-width: initial;
scrollbar-color: rgba(96, 100, 108, 0.1) transparent;
}
.vertical-scrollbar:hover,
.horizontal-scrollbar:hover {
scrollbar-color: rgba(96, 100, 108, 0.25) transparent;
}
.vertical-scrollbar:active,
.horizontal-scrollbar:active {
scrollbar-color: rgba(96, 100, 108, 0.7) transparent;
}
}
.vertical-scrollbar {
overflow-y: auto;
}
.horizontal-scrollbar {
overflow-x: auto;
}
.vertical-scrollbar::-webkit-scrollbar,
.horizontal-scrollbar::-webkit-scrollbar {
display: block;
}
.vertical-scrollbar::-webkit-scrollbar-track,
.horizontal-scrollbar::-webkit-scrollbar-track {
background-color: transparent;
border-radius: 9999px;
}
.vertical-scrollbar::-webkit-scrollbar-thumb,
.horizontal-scrollbar::-webkit-scrollbar-thumb {
background-clip: padding-box;
background-color: rgba(96, 100, 108, 0.1);
border-radius: 9999px;
}
.vertical-scrollbar:hover::-webkit-scrollbar-thumb,
.horizontal-scrollbar:hover::-webkit-scrollbar-thumb {
background-color: rgba(96, 100, 108, 0.25);
}
.vertical-scrollbar::-webkit-scrollbar-thumb:hover,
.horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: rgba(96, 100, 108, 0.5);
}
.vertical-scrollbar::-webkit-scrollbar-thumb:active,
.horizontal-scrollbar::-webkit-scrollbar-thumb:active {
background-color: rgba(96, 100, 108, 0.7);
}
.vertical-scrollbar::-webkit-scrollbar-corner,
.horizontal-scrollbar::-webkit-scrollbar-corner {
background-color: transparent;
}
.vertical-scrollbar-margin-top-md::-webkit-scrollbar-track {
margin-top: 44px;
}
/* scrollbar sm size */
.scrollbar-sm::-webkit-scrollbar {
height: 12px;
width: 12px;
}
.scrollbar-sm::-webkit-scrollbar-thumb {
border: 3px solid rgba(0, 0, 0, 0);
}
/* scrollbar md size */
.scrollbar-md::-webkit-scrollbar {
height: 14px;
width: 14px;
}
.scrollbar-md::-webkit-scrollbar-thumb {
border: 3px solid rgba(0, 0, 0, 0);
}
/* scrollbar lg size */
.scrollbar-lg::-webkit-scrollbar {
height: 16px;
width: 16px;
}
.scrollbar-lg::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
}
/* highlight class */
.highlight {
border: 1px solid rgb(var(--color-primary-100)) !important;
}
.highlight-with-line {
border-left: 5px solid rgb(var(--color-primary-100)) !important;
background: rgb(var(--color-background-80));
}
/* By applying below class, the autofilled text in form fields will not have the default autofill background color and styles applied by WebKit browsers */
.disable-autofill-style:-webkit-autofill,
.disable-autofill-style:-webkit-autofill:hover,
.disable-autofill-style:-webkit-autofill:focus,
.disable-autofill-style:-webkit-autofill:active {
-webkit-background-clip: text;
}

View File

@ -1 +1,5 @@
module.exports = require("tailwind-config-custom/tailwind.config");
const config = require("tailwind-config-custom/tailwind.config");
config.content.files = ["./src/**/*.{js,ts,jsx,tsx}"];
module.exports = config;

View File

@ -29,12 +29,21 @@ COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
RUN yarn turbo run build --filter=space
# *****************************************************************************
@ -54,12 +63,21 @@ COPY --from=installer /app/space/public ./space/public
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1

View File

@ -6,6 +6,7 @@ import { InstanceProvider } from "@/lib/instance-provider";
import { StoreProvider } from "@/lib/store-provider";
// styles
import "@/styles/globals.css";
import { ToastProvider } from "@/lib/toast-provider";
export const metadata: Metadata = {
title: "Plane Deploy | Make your Plane boards public with one-click",
@ -34,7 +35,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</head>
<body>
<StoreProvider>
<InstanceProvider>{children}</InstanceProvider>
<ToastProvider>
<InstanceProvider>{children}</InstanceProvider>
</ToastProvider>
</StoreProvider>
</body>
</html>

View File

@ -1,20 +0,0 @@
"use client";
import Image from "next/image";
import { useTheme } from "next-themes";
// assets
import LogoSpinnerDark from "@/public/images/logo-spinner-dark.gif";
import LogoSpinnerLight from "@/public/images/logo-spinner-light.gif";
export default function LogoSpinner() {
const { resolvedTheme } = useTheme();
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerDark : LogoSpinnerLight;
return (
<div className="h-screen w-full flex min-h-[600px] justify-center items-center">
<div className="flex items-center justify-center">
<Image src={logoSrc} alt="logo" className="w-[82px] h-[82px] mr-2" />
</div>
</div>
);
}

View File

@ -1,5 +1,6 @@
"use client";
import { observer } from "mobx-react-lite";
// components
import { UserLoggedIn } from "@/components/account";
import { LogoSpinner } from "@/components/common";
@ -7,7 +8,7 @@ import { AuthView } from "@/components/views";
// hooks
import { useUser } from "@/hooks/store";
export default function HomePage() {
const HomePage = observer(() => {
const { data: currentUser, isAuthenticated, isLoading } = useUser();
if (isLoading) return <LogoSpinner />;
@ -15,4 +16,6 @@ export default function HomePage() {
if (currentUser && isAuthenticated) return <UserLoggedIn />;
return <AuthView />;
}
});
export default HomePage;

View File

@ -94,7 +94,7 @@ export const AuthRoot: FC = observer(() => {
await authService
.emailCheck(data)
.then(async (response) => {
let currentAuthMode: EAuthModes = EAuthModes.SIGN_UP;
let currentAuthMode: EAuthModes = response.existing ? EAuthModes.SIGN_IN : EAuthModes.SIGN_UP;
if (response.existing) {
currentAuthMode = EAuthModes.SIGN_IN;
setAuthMode(() => EAuthModes.SIGN_IN);

View File

@ -6,7 +6,7 @@ import { UserAvatar } from "@/components/issues/navbar/user-avatar";
// hooks
import { useUser } from "@/hooks/store";
// assets
import PlaneLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.svg";
import PlaneLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import UserLoggedInImage from "@/public/user-logged-in.svg";
export const UserLoggedIn = () => {

View File

@ -2,28 +2,33 @@
import { FC } from "react";
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
// ui
import { Button } from "@plane/ui";
// helper
import { GOD_MODE_URL } from "@/helpers/common.helper";
import { GOD_MODE_URL, SPACE_BASE_PATH } from "@/helpers/common.helper";
// images
import PlaneTakeOffImage from "@/public/instance/plane-takeoff.png";
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png";
export const InstanceNotReady: FC = () => {
const { resolvedTheme } = useTheme();
const patternBackground = resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern;
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<div className="relative">
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="container h-[110px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<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>
<Link href={`${SPACE_BASE_PATH}/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
</div>

View File

@ -1,12 +1,14 @@
import React, { useRef } from "react";
import { observer } from "mobx-react-lite";
import { useForm, Controller } from "react-hook-form";
// components
// editor
import { EditorRefApi } from "@plane/lite-text-editor";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// editor components
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
// hooks
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
import useToast from "@/hooks/use-toast";
// types
import { Comment } from "@/types/issue";
@ -39,8 +41,6 @@ export const AddComment: React.FC<Props> = observer((props) => {
formState: { isSubmitting },
reset,
} = useForm<Comment>({ defaultValues });
// toast alert
const { setToastAlert } = useToast();
const onSubmit = async (formData: Comment) => {
if (!workspaceSlug || !projectId || !issueId || isSubmitting || !formData.comment_html) return;
@ -51,8 +51,8 @@ export const AddComment: React.FC<Props> = observer((props) => {
editorRef.current?.clearEditor();
})
.catch(() =>
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Comment could not be posted. Please try again.",
})

View File

@ -3,17 +3,15 @@ import { observer } from "mobx-react-lite";
import { MoveRight } from "lucide-react";
import { Listbox, Transition } from "@headlessui/react";
// ui
import { setToast, TOAST_TYPE } from "@plane/ui";
import { Icon } from "@/components/ui";
// helpers
import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks
import { useIssueDetails } from "@/hooks/store";
import useClipboardWritePermission from "@/hooks/use-clipboard-write-permission";
import useToast from "@/hooks/use-toast";
// store
import { IPeekMode } from "@/store/issue-detail.store";
// types
import { IIssue } from "@/types/issue";
import { IIssue, IPeekMode } from "@/types/issue";
type Props = {
handleClose: () => void;
@ -44,14 +42,12 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
const { peekMode, setPeekMode } = useIssueDetails();
const isClipboardWriteAllowed = useClipboardWritePermission();
const { setToastAlert } = useToast();
const handleCopyLink = () => {
const urlToCopy = window.location.href;
copyTextToClipboard(urlToCopy).then(() => {
setToastAlert({
type: "success",
setToast({
type: TOAST_TYPE.INFO,
title: "Link copied!",
message: "Issue link copied to clipboard",
});

View File

@ -1,17 +1,15 @@
// hooks
// ui
import { StateGroupIcon } from "@plane/ui";
import { StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
// icons
import { Icon } from "@/components/ui";
// helpers
// constants
import { issueGroupFilter, issuePriorityFilter } from "@/constants/issue";
// helpers
import { renderFullDate } from "@/helpers/date-time.helper";
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
// types
import { IPeekMode } from "@/store/issue-detail.store";
// constants
import useToast from "hooks/use-toast";
import { IIssue } from "types/issue";
import { IIssue, IPeekMode } from "@/types/issue";
// components
import { dueDateIconDetails } from "../board-views/block-due-date";
type Props = {
@ -20,8 +18,6 @@ type Props = {
};
export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mode }) => {
const { setToastAlert } = useToast();
const state = issueDetails.state_detail;
const stateGroup = issueGroupFilter(state.group);
@ -33,8 +29,8 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
const urlToCopy = window.location.href;
copyTextToClipboard(urlToCopy).then(() => {
setToastAlert({
type: "success",
setToast({
type: TOAST_TYPE.INFO,
title: "Link copied!",
message: "Issue link copied to clipboard",
});

View File

@ -1,61 +0,0 @@
import React from "react";
import { AlertTriangle, CheckCircle, Info, X, XCircle } from "lucide-react";
// hooks
import useToast from "hooks/use-toast";
// icons
const ToastAlerts = () => {
const { alerts, removeAlert } = useToast();
if (!alerts) return null;
return (
<div className="pointer-events-none fixed right-5 top-5 z-50 h-full w-80 space-y-5 overflow-hidden">
{alerts.map((alert) => (
<div className="relative overflow-hidden rounded-md text-white" key={alert.id}>
<div className="absolute right-1 top-1">
<button
type="button"
className="pointer-events-auto inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2"
onClick={() => removeAlert(alert.id)}
>
<span className="sr-only">Dismiss</span>
<X className="h-5 w-5" aria-hidden="true" />
</button>
</div>
<div
className={`px-2 py-4 ${
alert.type === "success"
? "bg-[#06d6a0]"
: alert.type === "error"
? "bg-[#ef476f]"
: alert.type === "warning"
? "bg-[#e98601]"
: "bg-[#1B9aaa]"
}`}
>
<div className="flex items-center gap-x-3">
<div className="flex-shrink-0">
{alert.type === "success" ? (
<CheckCircle className="h-8 w-8" aria-hidden="true" />
) : alert.type === "error" ? (
<XCircle className="h-8 w-8" />
) : alert.type === "warning" ? (
<AlertTriangle className="h-8 w-8" aria-hidden="true" />
) : (
<Info className="h-8 w-8" />
)}
</div>
<div>
<p className="font-semibold">{alert.title}</p>
{alert.message && <p className="mt-1 text-xs">{alert.message}</p>}
</div>
</div>
</div>
</div>
))}
</div>
);
};
export default ToastAlerts;

View File

@ -2,18 +2,24 @@
import { observer } from "mobx-react-lite";
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
// components
import { AuthRoot } from "@/components/account";
// helpers
import { SPACE_BASE_PATH } from "@/helpers/common.helper";
// images
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "@/public/plane-logos/blue-without-text-new.png";
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png";
export const AuthView = observer(() => {
// hooks
const { resolvedTheme } = useTheme();
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<div className="relative w-screen h-screen overflow-hidden">
<div className="absolute inset-0 z-0">
@ -26,11 +32,12 @@ export const AuthView = observer(() => {
<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>
<Link href={`${SPACE_BASE_PATH}/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
</div>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 lg:pt-28 transition-all">
<div className="flex flex-col justify-center flex-grow container h-[100vh-60px] mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 transition-all">
<AuthRoot />
</div>
</div>

View File

@ -1,97 +0,0 @@
import React, { createContext, useCallback, useReducer } from "react";
// uuid
import { v4 as uuid } from "uuid";
// components
import ToastAlert from "@/components/ui/toast-alert";
export const toastContext = createContext<ContextType>({} as ContextType);
// types
type ToastAlert = {
id: string;
title: string;
message?: string;
type: "success" | "error" | "warning" | "info";
};
type ReducerActionType = {
type: "SET_TOAST_ALERT" | "REMOVE_TOAST_ALERT";
payload: ToastAlert;
};
type ContextType = {
alerts?: ToastAlert[];
removeAlert: (id: string) => void;
setToastAlert: (data: {
title: string;
type?: "success" | "error" | "warning" | "info" | undefined;
message?: string | undefined;
}) => void;
};
type StateType = {
toastAlerts?: ToastAlert[];
};
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
export const initialState: StateType = {
toastAlerts: [],
};
export const reducer: ReducerFunctionType = (state, action) => {
const { type, payload } = action;
switch (type) {
case "SET_TOAST_ALERT":
return {
...state,
toastAlerts: [...(state.toastAlerts ?? []), payload],
};
case "REMOVE_TOAST_ALERT":
return {
...state,
toastAlerts: state.toastAlerts?.filter((toastAlert) => toastAlert.id !== payload.id),
};
default: {
return state;
}
}
};
export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const removeAlert = useCallback((id: string) => {
dispatch({
type: "REMOVE_TOAST_ALERT",
payload: { id, title: "", message: "", type: "success" },
});
}, []);
const setToastAlert = useCallback(
(data: { title: string; type?: "success" | "error" | "warning" | "info"; message?: string }) => {
const id = uuid();
const { title, type, message } = data;
dispatch({
type: "SET_TOAST_ALERT",
payload: { id, title, message, type: type ?? "success" },
});
const timer = setTimeout(() => {
removeAlert(id);
clearTimeout(timer);
}, 3000);
},
[removeAlert]
);
return (
<toastContext.Provider value={{ setToastAlert, removeAlert, alerts: state.toastAlerts }}>
<ToastAlert />
{children}
</toastContext.Provider>
);
};

View File

@ -1,5 +1,7 @@
import { ReactNode } from "react";
import Link from "next/link";
// helpers
import { SUPPORT_EMAIL } from "./common.helper";
export enum EPageTypes {
INIT = "INIT",
@ -152,7 +154,7 @@ const errorCodeMessages: {
// sign in
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
title: `User account deactivated`,
message: () => <div>Your account is deactivated. Contact support@plane.so.</div>,
message: () => `User account deactivated. Please contact ${!!SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
},
[EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: {

View File

@ -8,6 +8,8 @@ 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 SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "";
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}`);
@ -15,3 +17,6 @@ export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}`);
export const ASSET_PREFIX = SPACE_BASE_PATH;
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
export const resolveGeneralTheme = (resolvedTheme: string | undefined) =>
resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system";

View File

@ -1,9 +0,0 @@
import { useContext } from "react";
import { toastContext } from "@/contexts/toast.context";
const useToast = () => {
const toastContextData = useContext(toastContext);
return toastContextData;
};
export default useToast;

View File

@ -3,17 +3,21 @@
import { ReactNode } from "react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common";
import { InstanceFailureView } from "@/components/instance";
// helpers
import { SPACE_BASE_PATH } from "@/helpers/common.helper";
// hooks
import { useInstance, useUser } from "@/hooks/store";
// assets
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png";
export const InstanceProvider = observer(({ children }: { children: ReactNode }) => {
const { fetchInstanceInfo, instance, error } = useInstance();
@ -27,7 +31,11 @@ export const InstanceProvider = observer(({ children }: { children: ReactNode })
revalidateIfStale: false,
errorRetryCount: 0,
});
useSWR("CURRENT_USER", () => fetchCurrentUser());
useSWR("CURRENT_USER", () => fetchCurrentUser(), {
shouldRetryOnError: false,
revalidateOnFocus: false,
revalidateIfStale: false,
});
if (!instance && !error)
return (
@ -36,14 +44,16 @@ export const InstanceProvider = observer(({ children }: { children: ReactNode })
</div>
);
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
if (error) {
return (
<div className="relative">
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="container h-[110px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<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>
<Link href={`${SPACE_BASE_PATH}/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
</div>
<div className="absolute inset-0 z-0">

View File

@ -0,0 +1,20 @@
"use client";
import { ReactNode } from "react";
import { useTheme } from "next-themes"
// ui
import { Toast } from "@plane/ui";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
export const ToastProvider = ({ children }: { children: ReactNode }) => {
// themes
const { resolvedTheme } = useTheme();
return (
<>
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
{children}
</>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,17 +0,0 @@
<svg width="133" height="30" viewBox="0 0 133 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_372_3489)">
<path d="M35.7598 0.00235398H44.7151C46.4036 -0.0301787 48.0819 0.274777 49.6538 0.899752C51.0897 1.48694 52.3232 2.48876 53.2005 3.7804C54.1414 5.27171 54.6058 7.02009 54.5305 8.78788C54.6025 10.5948 54.1394 12.3823 53.2005 13.921C52.338 15.271 51.1056 16.3375 49.6538 16.9901C48.0968 17.6881 46.409 18.0371 44.7062 18.0131H40.3882V30.0024H35.7598V0.00235398ZM40.3882 14.1812H43.4472C44.5022 14.1924 45.5515 14.0225 46.5505 13.6787C47.4425 13.3684 48.2171 12.7851 48.7672 12.0095C49.3447 11.1092 49.6299 10.0487 49.5829 8.97633C49.6547 7.88162 49.368 6.79348 48.7672 5.88031C48.2009 5.1354 47.4202 4.58669 46.5328 4.30986C45.5354 3.98896 44.4937 3.83143 43.4472 3.84322H40.3882V14.1812Z" fill="#262626"/>
<path d="M63.6538 30.0023H58.9102V0.00234985H63.6538V30.0023Z" fill="#262626"/>
<path d="M83.1961 30.0023V26.0717C82.9541 26.623 82.6146 27.1249 82.1941 27.5524C81.5462 28.2456 80.7841 28.8194 79.942 29.2485C78.8993 29.7731 77.7457 30.0319 76.5816 30.0023C74.9896 30.0315 73.4209 29.6134 72.0497 28.7943C70.6785 27.9753 69.5585 26.7874 68.8144 25.3628C68.0116 23.8079 67.6091 22.0734 67.644 20.3194C67.6069 18.5595 68.0094 16.8186 68.8144 15.2581C69.5577 13.8315 70.677 12.6411 72.048 11.819C73.419 10.9969 74.9882 10.5752 76.5816 10.6006C77.7134 10.57 78.8375 10.7969 79.8711 11.2647C80.698 11.6453 81.4539 12.1675 82.1055 12.8082C82.5675 13.2374 82.9256 13.7687 83.1518 14.3607V11.139H87.8333V29.9844L83.1961 30.0023ZM72.299 20.3194C72.2745 21.3974 72.5412 22.4617 73.0704 23.3975C73.548 24.2312 74.2333 24.9235 75.0579 25.4053C75.8825 25.8872 76.8175 26.1417 77.7697 26.1435C78.7342 26.1735 79.6872 25.9254 80.5177 25.4281C81.3482 24.9308 82.0218 24.2047 82.4602 23.3347C82.9374 22.3947 83.1751 21.349 83.1518 20.2925C83.1763 19.233 82.9385 18.1841 82.4602 17.2413C81.9964 16.419 81.3267 15.7349 80.5183 15.2581C79.6888 14.7549 78.7367 14.4969 77.7697 14.5133C76.8353 14.5149 75.9179 14.7656 75.1097 15.2401C74.2725 15.73 73.5757 16.4321 73.0881 17.2772C72.5606 18.2015 72.288 19.2521 72.299 20.3194Z" fill="#262626"/>
<path d="M103.536 10.6096C104.757 10.611 105.964 10.8677 107.082 11.3634C108.238 11.8795 109.223 12.7205 109.92 13.7864C110.714 15.0781 111.104 16.5829 111.037 18.1029V30.0023H106.293V18.9733C106.348 18.3184 106.273 17.6591 106.074 17.0337C105.874 16.4082 105.554 15.8291 105.132 15.3299C104.739 14.9381 104.272 14.6317 103.758 14.4296C103.244 14.2274 102.695 14.1337 102.144 14.1543C101.323 14.1599 100.521 14.3994 99.8295 14.8453C99.0977 15.3167 98.4893 15.9602 98.0562 16.7209C97.5907 17.539 97.3518 18.469 97.3646 19.4131V30.0023H92.6387V11.157H97.3646V14.244C97.5688 13.6234 97.9368 13.0711 98.4286 12.6467C99.081 12.0457 99.8309 11.563 100.645 11.2198C101.557 10.8175 102.541 10.6097 103.536 10.6096Z" fill="#262626"/>
<path d="M118.973 21.4322C118.987 22.346 119.221 23.2426 119.656 24.0436C120.095 24.8189 120.753 25.4438 121.545 25.8384C122.494 26.3044 123.541 26.5293 124.595 26.4935C125.5 26.5093 126.4 26.3635 127.255 26.0628C127.932 25.8094 128.569 25.4556 129.143 25.0128C129.56 24.6821 129.93 24.2957 130.243 23.8642L132.335 26.7448C131.854 27.3909 131.273 27.9545 130.615 28.414C129.834 28.9511 128.963 29.3402 128.044 29.5626C126.808 29.8643 125.538 30.0031 124.267 29.9754C122.476 30.0226 120.705 29.599 119.124 28.746C117.69 27.949 116.513 26.7492 115.737 25.291C114.909 23.68 114.498 21.8834 114.54 20.0682C114.532 18.3948 114.924 16.7444 115.684 15.2581C116.41 13.8364 117.524 12.6558 118.894 11.857C120.412 11.0607 122.093 10.6338 123.803 10.6105C125.513 10.5871 127.205 10.9678 128.744 11.7224C130.081 12.4518 131.174 13.5669 131.883 14.9261C132.667 16.483 133.051 18.2141 133 19.9605C133 20.0771 133 20.3284 133 20.6963C133.005 20.9431 132.984 21.1897 132.938 21.4322H118.973ZM128.567 17.9503C128.53 17.3776 128.37 16.8201 128.097 16.317C127.762 15.6679 127.264 15.12 126.652 14.7287C125.854 14.2334 124.926 13.9955 123.992 14.0466C123.03 14.0014 122.076 14.2282 121.234 14.7017C120.661 15.0464 120.172 15.5175 119.804 16.08C119.435 16.6424 119.197 17.2817 119.106 17.9503H128.567Z" fill="#262626"/>
<path d="M29.127 0.271576H9.70898V10.0981H19.418V19.9246H29.127V0.271576Z" fill="#3F76FF"/>
<path d="M9.709 10.0981H0V19.9066H9.709V10.0981Z" fill="#3F76FF"/>
<path d="M19.4266 19.9246H9.73535V29.7511H19.4266V19.9246Z" fill="#3F76FF"/>
</g>
<defs>
<clipPath id="clip0_372_3489">
<rect width="133" height="30" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,17 +0,0 @@
<svg width="133" height="30" viewBox="0 0 133 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_372_3489)">
<path d="M35.7598 0.00235398H44.7151C46.4036 -0.0301787 48.0819 0.274777 49.6538 0.899752C51.0897 1.48694 52.3232 2.48876 53.2005 3.7804C54.1414 5.27171 54.6058 7.02009 54.5305 8.78788C54.6025 10.5948 54.1394 12.3823 53.2005 13.921C52.338 15.271 51.1056 16.3375 49.6538 16.9901C48.0968 17.6881 46.409 18.0371 44.7062 18.0131H40.3882V30.0024H35.7598V0.00235398ZM40.3882 14.1812H43.4472C44.5022 14.1924 45.5515 14.0225 46.5505 13.6787C47.4425 13.3684 48.2171 12.7851 48.7672 12.0095C49.3447 11.1092 49.6299 10.0487 49.5829 8.97633C49.6547 7.88162 49.368 6.79348 48.7672 5.88031C48.2009 5.1354 47.4202 4.58669 46.5328 4.30986C45.5354 3.98896 44.4937 3.83143 43.4472 3.84322H40.3882V14.1812Z" fill="#ffffff"/>
<path d="M63.6538 30.0023H58.9102V0.00234985H63.6538V30.0023Z" fill="#ffffff"/>
<path d="M83.1961 30.0023V26.0717C82.9541 26.623 82.6146 27.1249 82.1941 27.5524C81.5462 28.2456 80.7841 28.8194 79.942 29.2485C78.8993 29.7731 77.7457 30.0319 76.5816 30.0023C74.9896 30.0315 73.4209 29.6134 72.0497 28.7943C70.6785 27.9753 69.5585 26.7874 68.8144 25.3628C68.0116 23.8079 67.6091 22.0734 67.644 20.3194C67.6069 18.5595 68.0094 16.8186 68.8144 15.2581C69.5577 13.8315 70.677 12.6411 72.048 11.819C73.419 10.9969 74.9882 10.5752 76.5816 10.6006C77.7134 10.57 78.8375 10.7969 79.8711 11.2647C80.698 11.6453 81.4539 12.1675 82.1055 12.8082C82.5675 13.2374 82.9256 13.7687 83.1518 14.3607V11.139H87.8333V29.9844L83.1961 30.0023ZM72.299 20.3194C72.2745 21.3974 72.5412 22.4617 73.0704 23.3975C73.548 24.2312 74.2333 24.9235 75.0579 25.4053C75.8825 25.8872 76.8175 26.1417 77.7697 26.1435C78.7342 26.1735 79.6872 25.9254 80.5177 25.4281C81.3482 24.9308 82.0218 24.2047 82.4602 23.3347C82.9374 22.3947 83.1751 21.349 83.1518 20.2925C83.1763 19.233 82.9385 18.1841 82.4602 17.2413C81.9964 16.419 81.3267 15.7349 80.5183 15.2581C79.6888 14.7549 78.7367 14.4969 77.7697 14.5133C76.8353 14.5149 75.9179 14.7656 75.1097 15.2401C74.2725 15.73 73.5757 16.4321 73.0881 17.2772C72.5606 18.2015 72.288 19.2521 72.299 20.3194Z" fill="#ffffff"/>
<path d="M103.536 10.6096C104.757 10.611 105.964 10.8677 107.082 11.3634C108.238 11.8795 109.223 12.7205 109.92 13.7864C110.714 15.0781 111.104 16.5829 111.037 18.1029V30.0023H106.293V18.9733C106.348 18.3184 106.273 17.6591 106.074 17.0337C105.874 16.4082 105.554 15.8291 105.132 15.3299C104.739 14.9381 104.272 14.6317 103.758 14.4296C103.244 14.2274 102.695 14.1337 102.144 14.1543C101.323 14.1599 100.521 14.3994 99.8295 14.8453C99.0977 15.3167 98.4893 15.9602 98.0562 16.7209C97.5907 17.539 97.3518 18.469 97.3646 19.4131V30.0023H92.6387V11.157H97.3646V14.244C97.5688 13.6234 97.9368 13.0711 98.4286 12.6467C99.081 12.0457 99.8309 11.563 100.645 11.2198C101.557 10.8175 102.541 10.6097 103.536 10.6096Z" fill="#ffffff"/>
<path d="M118.973 21.4322C118.987 22.346 119.221 23.2426 119.656 24.0436C120.095 24.8189 120.753 25.4438 121.545 25.8384C122.494 26.3044 123.541 26.5293 124.595 26.4935C125.5 26.5093 126.4 26.3635 127.255 26.0628C127.932 25.8094 128.569 25.4556 129.143 25.0128C129.56 24.6821 129.93 24.2957 130.243 23.8642L132.335 26.7448C131.854 27.3909 131.273 27.9545 130.615 28.414C129.834 28.9511 128.963 29.3402 128.044 29.5626C126.808 29.8643 125.538 30.0031 124.267 29.9754C122.476 30.0226 120.705 29.599 119.124 28.746C117.69 27.949 116.513 26.7492 115.737 25.291C114.909 23.68 114.498 21.8834 114.54 20.0682C114.532 18.3948 114.924 16.7444 115.684 15.2581C116.41 13.8364 117.524 12.6558 118.894 11.857C120.412 11.0607 122.093 10.6338 123.803 10.6105C125.513 10.5871 127.205 10.9678 128.744 11.7224C130.081 12.4518 131.174 13.5669 131.883 14.9261C132.667 16.483 133.051 18.2141 133 19.9605C133 20.0771 133 20.3284 133 20.6963C133.005 20.9431 132.984 21.1897 132.938 21.4322H118.973ZM128.567 17.9503C128.53 17.3776 128.37 16.8201 128.097 16.317C127.762 15.6679 127.264 15.12 126.652 14.7287C125.854 14.2334 124.926 13.9955 123.992 14.0466C123.03 14.0014 122.076 14.2282 121.234 14.7017C120.661 15.0464 120.172 15.5175 119.804 16.08C119.435 16.6424 119.197 17.2817 119.106 17.9503H128.567Z" fill="#ffffff"/>
<path d="M29.127 0.271576H9.70898V10.0981H19.418V19.9246H29.127V0.271576Z" fill="#3F76FF"/>
<path d="M9.709 10.0981H0V19.9066H9.709V10.0981Z" fill="#3F76FF"/>
<path d="M19.4266 19.9246H9.73535V29.7511H19.4266V19.9246Z" fill="#3F76FF"/>
</g>
<defs>
<clipPath id="clip0_372_3489">
<rect width="133" height="30" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -5,9 +5,7 @@ import IssueService from "@/services/issue.service";
// store types
import { RootStore } from "@/store/root.store";
// types
import { IIssue, IVote } from "@/types/issue";
export type IPeekMode = "side" | "modal" | "full";
import { IIssue, IPeekMode, IVote } from "@/types/issue";
export interface IIssueDetailStore {
loader: boolean;

View File

@ -66,6 +66,8 @@ export interface IIssue {
votes: IVote[];
}
export type IPeekMode = "side" | "modal" | "full";
export interface IIssueState {
id: string;
name: string;

View File

@ -20,6 +20,7 @@
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG",
"NEXT_PUBLIC_SUPPORT_EMAIL",
"SENTRY_AUTH_TOKEN"
],
"pipeline": {

View File

@ -45,6 +45,9 @@ ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
@ -74,12 +77,15 @@ ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces"
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1

View File

@ -21,30 +21,30 @@ type TAuthHeader = {
const Titles = {
[EAuthModes.SIGN_IN]: {
[EAuthSteps.EMAIL]: {
header: "Sign in to Plane",
subHeader: "Get back to your projects and make progress",
header: "Log in or Sign up",
subHeader: "",
},
[EAuthSteps.PASSWORD]: {
header: "Sign in to Plane",
subHeader: "Get back to your projects and make progress",
header: "Log in or Sign up",
subHeader: "Log in using your password.",
},
[EAuthSteps.UNIQUE_CODE]: {
header: "Sign in to Plane",
subHeader: "Get back to your projects and make progress",
header: "Log in or Sign up",
subHeader: "Log in using your unique code.",
},
},
[EAuthModes.SIGN_UP]: {
[EAuthSteps.EMAIL]: {
header: "Create your account",
subHeader: "Start tracking your projects with Plane",
header: "Sign up or Log in",
subHeader: "",
},
[EAuthSteps.PASSWORD]: {
header: "Create your account",
subHeader: "Progress, visualize, and measure work how it works best for you.",
header: "Sign up or Log in",
subHeader: "Sign up using your password",
},
[EAuthSteps.UNIQUE_CODE]: {
header: "Create your account",
subHeader: "Progress, visualize, and measure work how it works best for you.",
header: "Sign up or Log in",
subHeader: "Sign up using your unique code",
},
},
};

View File

@ -37,68 +37,85 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
const router = useRouter();
const { email: emailParam, invitation_id, slug: workspaceSlug, error_code } = router.query;
// props
const { authMode } = props;
const { authMode: currentAuthMode } = props;
// states
const [authMode, setAuthMode] = useState<EAuthModes | undefined>(undefined);
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
const [isPasswordAutoset, setIsPasswordAutoset] = useState(true);
const [isExistingEmail, setIsExistingEmail] = useState(false);
// hooks
const { config } = useInstance();
useEffect(() => {
if (error_code) {
if (!authMode && currentAuthMode) setAuthMode(currentAuthMode);
}, [currentAuthMode, authMode]);
useEffect(() => {
if (error_code && authMode) {
const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes);
if (errorhandler) {
if (
[
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN,
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP,
].includes(errorhandler.code)
)
// password error handler
if ([EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP].includes(errorhandler.code)) {
setAuthMode(EAuthModes.SIGN_UP);
setAuthStep(EAuthSteps.PASSWORD);
}
if ([EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN].includes(errorhandler.code)) {
setAuthMode(EAuthModes.SIGN_IN);
setAuthStep(EAuthSteps.PASSWORD);
}
// magic_code error handler
if (
[
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN,
EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP,
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP,
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN,
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
].includes(errorhandler.code)
)
) {
setAuthMode(EAuthModes.SIGN_UP);
setAuthStep(EAuthSteps.UNIQUE_CODE);
}
if (
[
EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN,
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN,
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
].includes(errorhandler.code)
) {
setAuthMode(EAuthModes.SIGN_IN);
setAuthStep(EAuthSteps.UNIQUE_CODE);
}
setErrorInfo(errorhandler);
}
}
}, [error_code, authMode]);
const isSMTPConfigured = config?.is_smtp_configured || false;
const isMagicLoginEnabled = config?.is_magic_login_enabled || false;
const isEmailPasswordEnabled = config?.is_email_password_enabled || false;
// submit handler- email verification
const handleEmailVerification = async (data: IEmailCheckData) => {
setEmail(data.email);
const emailCheckRequest =
authMode === EAuthModes.SIGN_IN ? authService.signInEmailCheck(data) : authService.signUpEmailCheck(data);
await emailCheckRequest
setErrorInfo(undefined);
await authService
.emailCheck(data)
.then(async (response) => {
if (authMode === EAuthModes.SIGN_IN) {
if (response.is_password_autoset) {
if (response.existing) {
if (currentAuthMode === EAuthModes.SIGN_UP) setAuthMode(EAuthModes.SIGN_IN);
if (response.status === "MAGIC_CODE") {
setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email);
} else if (isEmailPasswordEnabled) {
setIsPasswordAutoset(false);
} else if (response.status === "CREDENTIAL") {
setAuthStep(EAuthSteps.PASSWORD);
}
} else {
if (isSMTPConfigured && isMagicLoginEnabled) {
if (currentAuthMode === EAuthModes.SIGN_IN) setAuthMode(EAuthModes.SIGN_UP);
if (response.status === "MAGIC_CODE") {
setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email);
} else if (isEmailPasswordEnabled) {
} else if (response.status === "CREDENTIAL") {
setAuthStep(EAuthSteps.PASSWORD);
}
}
@ -110,8 +127,17 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
});
};
const handleEmailClear = () => {
setAuthMode(currentAuthMode);
setErrorInfo(undefined);
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
router.push(currentAuthMode === EAuthModes.SIGN_IN ? `/` : "/sign-up", undefined, { shallow: true });
};
// generating the unique code
const generateEmailUniqueCode = async (email: string): Promise<{ code: string } | undefined> => {
if (!isSMTPConfigured) return;
const payload = { email: email };
return await authService
.generateUniqueCode(payload)
@ -123,6 +149,7 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
});
};
if (!authMode) return <></>;
return (
<div className="relative flex flex-col space-y-6">
<AuthHeader
@ -141,23 +168,16 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
mode={authMode}
email={email}
isExistingEmail={isExistingEmail}
handleEmailClear={() => {
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
}}
handleEmailClear={handleEmailClear}
generateEmailUniqueCode={generateEmailUniqueCode}
/>
)}
{authStep === EAuthSteps.PASSWORD && (
<AuthPasswordForm
mode={authMode}
isPasswordAutoset={isPasswordAutoset}
isSMTPConfigured={isSMTPConfigured}
email={email}
handleEmailClear={() => {
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
}}
handleEmailClear={handleEmailClear}
handleAuthStep={(step: EAuthSteps) => {
if (step === EAuthSteps.UNIQUE_CODE) generateEmailUniqueCode(email);
setAuthStep(step);

View File

@ -57,7 +57,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="name@company.com"
placeholder="name@example.com"
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}

View File

@ -25,7 +25,6 @@ import { AuthService } from "@/services/auth.service";
type Props = {
email: string;
isPasswordAutoset: boolean;
isSMTPConfigured: boolean;
mode: EAuthModes;
handleEmailClear: () => void;

View File

@ -8,8 +8,7 @@ type TOAuthOptionProps = {
isSignUp?: boolean;
};
export const OAuthOptions: React.FC<TOAuthOptionProps> = observer((props) => {
const { isSignUp = false } = props;
export const OAuthOptions: React.FC<TOAuthOptionProps> = observer(() => {
// hooks
const { config } = useInstance();
@ -17,8 +16,6 @@ export const OAuthOptions: React.FC<TOAuthOptionProps> = observer((props) => {
if (!isOAuthEnabled) return null;
const oauthProviderButtonText = `Sign ${isSignUp ? "up" : "in"} with`;
return (
<>
<div className="mt-4 flex items-center">
@ -29,10 +26,10 @@ export const OAuthOptions: React.FC<TOAuthOptionProps> = observer((props) => {
<div className={`mt-7 grid gap-4 overflow-hidden`}>
{config?.is_google_enabled && (
<div className="flex h-[42px] items-center !overflow-hidden">
<GoogleOAuthButton text={`${oauthProviderButtonText} Google`} />
<GoogleOAuthButton text="Continue with Google" />
</div>
)}
{config?.is_github_enabled && <GithubOAuthButton text={`${oauthProviderButtonText} Github`} />}
{config?.is_github_enabled && <GithubOAuthButton text="Continue with Github" />}
</div>
</>
);

View File

@ -1,5 +1,6 @@
import { FC } from "react";
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
import { Button } from "@plane/ui";
// helpers
@ -9,19 +10,23 @@ import PlaneTakeOffImage from "@/public/plane-takeoff.png";
// assets
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png";
export const InstanceNotReady: FC = () => {
const { resolvedTheme } = useTheme();
const patternBackground = resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern;
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<div className="relative">
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="container h-[110px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<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>
<Link href={`/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
</div>

View File

@ -13,11 +13,12 @@ type Props = {
handleClose: () => void;
dataId?: string | null | undefined;
data?: TIssue;
isSubIssue?: boolean;
onSubmit?: () => Promise<void>;
};
export const DeleteIssueModal: React.FC<Props> = (props) => {
const { dataId, data, isOpen, handleClose, onSubmit } = props;
const { dataId, data, isOpen, handleClose, isSubIssue = false, onSubmit } = props;
// states
const [isDeleting, setIsDeleting] = useState(false);
// store hooks
@ -44,6 +45,11 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
if (onSubmit)
await onSubmit()
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: `${isSubIssue ? "Sub-issue" : "Issue"} deleted successfully`,
});
onClose();
})
.catch(() => {

View File

@ -249,9 +249,9 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
className="w-full"
customButtonClassName="w-full"
customButton={
<div className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100">
<div className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-text-350 hover:text-custom-text-300">
<PlusIcon className="h-3.5 w-3.5 stroke-2 flex-shrink-0" />
<span className="text-sm font-medium flex-shrink-0 text-custom-primary-100">New Issue</span>
<span className="text-sm font-medium flex-shrink-0">New Issue</span>
</div>
}
>

View File

@ -55,6 +55,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES;
const additionalPath = activeLayout ?? "list";
const emptyStateSize = isEmptyFilters ? "lg" : "sm";
return (
<>
@ -70,6 +71,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
<EmptyState
type={emptyStateType}
additionalPath={additionalPath}
size={emptyStateSize}
primaryButtonOnClick={
isEmptyFilters
? undefined

View File

@ -96,14 +96,16 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
return (
<div className="flex justify-between gap-4 p-4">
<AppliedFiltersList
appliedFilters={appliedFilters ?? {}}
handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter}
labels={projectLabels ?? []}
states={projectStates}
alwaysAllowEditing
/>
<div>
<AppliedFiltersList
appliedFilters={appliedFilters ?? {}}
handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter}
labels={projectLabels ?? []}
states={projectStates}
alwaysAllowEditing
/>
</div>
{!areFiltersEqual && (
<div>

View File

@ -163,11 +163,11 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
) : (
<button
type="button"
className="sticky bottom-0 z-[1] flex w-full cursor-pointer items-center gap-2 border-t-[1px] border-custom-border-200 bg-custom-background-100 px-3 pt-2 text-custom-primary-100"
className="sticky bottom-0 z-[1] flex w-full cursor-pointer items-center gap-2 border-t-[1px] border-custom-border-200 bg-custom-background-100 px-3 pt-2 text-custom-text-350 hover:text-custom-text-300"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
<span className="text-sm font-medium">New Issue</span>
</button>
)}
</>

View File

@ -5,7 +5,7 @@ import { cn } from "@/helpers/common.helper";
type Props = {
dragColumnOrientation: "justify-start" | "justify-center" | "justify-end";
canDropOverIssue: boolean;
canOverlayBeVisible: boolean;
isDropDisabled: boolean;
dropErrorMessage?: string;
orderBy: TIssueOrderByOptions | undefined;
@ -13,10 +13,16 @@ type Props = {
};
export const GroupDragOverlay = (props: Props) => {
const { dragColumnOrientation, canDropOverIssue, isDropDisabled, dropErrorMessage, orderBy, isDraggingOverColumn } =
props;
const {
dragColumnOrientation,
canOverlayBeVisible,
isDropDisabled,
dropErrorMessage,
orderBy,
isDraggingOverColumn,
} = props;
const shouldOverlay = isDraggingOverColumn && (!canDropOverIssue || isDropDisabled);
const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible;
const readableOrderBy = ISSUE_ORDER_BY_OPTIONS.find((orderByObj) => orderByObj.key === orderBy)?.title;
return (
@ -24,16 +30,16 @@ export const GroupDragOverlay = (props: Props) => {
className={cn(
`absolute top-0 left-0 h-full w-full items-center text-sm font-medium text-custom-text-300 rounded bg-custom-background-overlay ${dragColumnOrientation}`,
{
"flex flex-col border-[1px] border-custom-border-300 z-[2]": shouldOverlay,
"flex flex-col border-[1px] border-custom-border-300 z-[2]": shouldOverlayBeVisible,
},
{ hidden: !shouldOverlay }
{ hidden: !shouldOverlayBeVisible }
)}
>
<div
className={cn(
"p-3 mt-8 flex flex-col rounded items-center",
"p-3 my-8 flex flex-col rounded items-center",
{
"text-custom-text-200": shouldOverlay,
"text-custom-text-200": shouldOverlayBeVisible,
},
{
"text-custom-text-error": isDropDisabled,

View File

@ -25,15 +25,16 @@ import { KanbanStoreType } from "./base-kanban-root";
import { HeaderGroupByCard } from "./headers/group-by-card";
import { KanbanGroup } from "./kanban-group";
export interface IGroupByKanBan {
export interface IKanBan {
issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: TIssueGroupByOptions | undefined;
group_by: TIssueGroupByOptions | undefined;
orderBy: TIssueOrderByOptions | undefined;
sub_group_id: string;
isDragDisabled: boolean;
isDropDisabled?: boolean;
dropErrorMessage?: string | undefined;
sub_group_id?: string;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
quickActions: TRenderQuickActions;
kanbanFilters: TIssueKanbanFilters;
@ -56,7 +57,7 @@ export interface IGroupByKanBan {
subGroupIssueHeaderCount?: (listId: string) => number;
}
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
export const KanBan: React.FC<IKanBan> = observer((props) => {
const {
issuesMap,
issueIds,
@ -64,7 +65,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
sub_group_by,
group_by,
sub_group_id = "null",
isDragDisabled,
updateIssue,
quickActions,
kanbanFilters,
@ -81,6 +81,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
showEmptyGroup = true,
subGroupIssueHeaderCount,
orderBy,
isDropDisabled,
dropErrorMessage,
} = props;
const member = useMember();
@ -89,6 +91,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const cycle = useCycle();
const moduleInfo = useModule();
const projectState = useProjectState();
const issueKanBanView = useKanbanView();
const isDragDisabled = !issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by);
const list = getGroupByColumns(
group_by as GroupByColumnTypes,
@ -175,8 +180,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
orderBy={orderBy}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
isDropDisabled={!!subList.isDropDisabled}
dropErrorMessage={subList.dropErrorMessage}
isDropDisabled={!!subList.isDropDisabled || !!isDropDisabled}
dropErrorMessage={subList.dropErrorMessage ?? dropErrorMessage}
updateIssue={updateIssue}
quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate}
@ -194,90 +199,3 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
</div>
);
});
export interface IKanBan {
issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: TIssueGroupByOptions | undefined;
group_by: TIssueGroupByOptions | undefined;
orderBy: TIssueOrderByOptions | undefined;
sub_group_id?: string;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
quickActions: TRenderQuickActions;
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
showEmptyGroup: boolean;
enableQuickIssueCreate?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: TIssue,
viewId?: string
) => Promise<TIssue | undefined>;
viewId?: string;
disableIssueCreation?: boolean;
storeType: KanbanStoreType;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
subGroupIssueHeaderCount?: (listId: string) => number;
}
export const KanBan: React.FC<IKanBan> = observer((props) => {
const {
issuesMap,
issueIds,
displayProperties,
sub_group_by,
group_by,
sub_group_id = "null",
updateIssue,
quickActions,
kanbanFilters,
handleKanbanFilters,
enableQuickIssueCreate,
quickAddCallback,
viewId,
disableIssueCreation,
storeType,
addIssuesToView,
canEditProperties,
scrollableContainerRef,
handleOnDrop,
showEmptyGroup,
subGroupIssueHeaderCount,
orderBy,
} = props;
const issueKanBanView = useKanbanView();
return (
<GroupByKanBan
issuesMap={issuesMap}
issueIds={issueIds}
displayProperties={displayProperties}
group_by={group_by}
sub_group_by={sub_group_by}
orderBy={orderBy}
sub_group_id={sub_group_id}
isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)}
updateIssue={updateIssue}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
storeType={storeType}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef}
handleOnDrop={handleOnDrop}
showEmptyGroup={showEmptyGroup}
subGroupIssueHeaderCount={subGroupIssueHeaderCount}
/>
);
});

View File

@ -187,8 +187,8 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
return preloadedData;
};
const canDropOverIssue = orderBy === "sort_order";
const shouldOverlay = isDraggingOverColumn && (!canDropOverIssue || isDropDisabled);
const canOverlayBeVisible = orderBy !== "sort_order" || isDropDisabled;
const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible;
return (
<div
@ -196,13 +196,13 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
className={cn(
"relative h-full transition-all min-h-[120px]",
{ "bg-custom-background-80 rounded": isDraggingOverColumn },
{ "vertical-scrollbar scrollbar-md": !sub_group_by && !shouldOverlay }
{ "vertical-scrollbar scrollbar-md": !sub_group_by && !shouldOverlayBeVisible }
)}
ref={columnRef}
>
<GroupDragOverlay
dragColumnOrientation={sub_group_by ? "justify-start": "justify-center" }
canDropOverIssue={canDropOverIssue}
canOverlayBeVisible={canOverlayBeVisible}
isDropDisabled={isDropDisabled}
dropErrorMessage={dropErrorMessage}
orderBy={orderBy}
@ -219,7 +219,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
quickActions={quickActions}
canEditProperties={canEditProperties}
scrollableContainerRef={sub_group_by ? scrollableContainerRef : columnRef}
canDropOverIssue={canDropOverIssue}
canDropOverIssue={!canOverlayBeVisible}
/>
{enableQuickIssueCreate && !disableIssueCreation && (

View File

@ -151,11 +151,11 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
</div>
) : (
<div
className="flex w-full cursor-pointer items-center gap-2 p-3 py-1.5 text-custom-primary-100"
className="flex w-full cursor-pointer items-center gap-2 p-3 py-1.5 text-custom-text-350 hover:text-custom-text-300"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
<span className="text-sm font-medium">New Issue</span>
</div>
)}
</>

View File

@ -219,6 +219,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
scrollableContainerRef={scrollableContainerRef}
handleOnDrop={handleOnDrop}
orderBy={orderBy}
isDropDisabled={_list.isDropDisabled}
dropErrorMessage={_list.dropErrorMessage}
subGroupIssueHeaderCount={(groupByListId: string) =>
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
}

View File

@ -176,7 +176,7 @@ export const ListGroup = observer((props: Props) => {
const is_list = group_by === null ? true : false;
const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by);
const canDropOverIssue = orderBy === "sort_order";
const canOverlayBeVisible = orderBy !== "sort_order" || !!group.isDropDisabled;
const issueCount: number = is_list ? issueIds?.length ?? 0 : issueIds?.[group.id]?.length ?? 0;
@ -186,7 +186,8 @@ export const ListGroup = observer((props: Props) => {
<div
ref={groupRef}
className={cn(`relative flex flex-shrink-0 flex-col border-[1px] border-transparent`, {
"border-custom-primary-100 ": isDraggingOverColumn,
"border-custom-primary-100": isDraggingOverColumn,
"border-custom-error-200": isDraggingOverColumn && !!group.isDropDisabled,
})}
>
<div className="sticky top-0 z-[3] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 pl-5 py-1">
@ -205,7 +206,7 @@ export const ListGroup = observer((props: Props) => {
<div className="relative">
<GroupDragOverlay
dragColumnOrientation={dragColumnOrientation}
canDropOverIssue={canDropOverIssue}
canOverlayBeVisible={canOverlayBeVisible}
isDropDisabled={!!group.isDropDisabled}
dropErrorMessage={group.dropErrorMessage}
orderBy={orderBy}
@ -222,7 +223,7 @@ export const ListGroup = observer((props: Props) => {
canEditProperties={canEditProperties}
containerRef={containerRef}
isDragAllowed={isDragAllowed}
canDropOverIssue={canDropOverIssue}
canDropOverIssue={!canOverlayBeVisible}
/>
)}

View File

@ -152,11 +152,11 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
</div>
) : (
<div
className="flex w-full cursor-pointer items-center gap-3 p-6 py-3 text-custom-primary-100"
className="flex w-full cursor-pointer items-center gap-2.5 p-6 py-3 text-custom-text-350 hover:text-custom-text-300"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
<span className="text-sm font-medium">New Issue</span>
</div>
)}
</div>

View File

@ -226,11 +226,11 @@ export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) =>
<div className="flex items-center">
<button
type="button"
className="flex items-center gap-x-[6px] rounded-md px-2 pt-3 text-custom-primary-100"
className="flex items-center gap-x-[6px] rounded-md px-2 pt-3 text-custom-text-350 hover:text-custom-text-300"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
<span className="text-sm font-medium">New Issue</span>
</button>
</div>
)}

View File

@ -162,7 +162,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Issue created successfully.",
message: `${is_draft_issue ? "Draft issue" : "Issue"} created successfully.`,
});
captureIssueEvent({
eventName: ISSUE_CREATED,
@ -178,7 +178,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Issue could not be created. Please try again.",
message: `${is_draft_issue ? "Draft issue" : "Issue"} could not be created. Please try again.`,
});
captureIssueEvent({
eventName: ISSUE_CREATED,

View File

@ -18,6 +18,7 @@ export interface ISubIssues {
workspaceSlug: string;
projectId: string;
parentIssueId: string;
rootIssueId: string;
spacingLeft: number;
disabled: boolean;
handleIssueCrudState: (
@ -34,6 +35,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
workspaceSlug,
projectId,
parentIssueId,
rootIssueId,
issueId,
spacingLeft = 10,
disabled,
@ -70,6 +72,10 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
if (!issue) return <></>;
// check if current issue is the root issue
const isCurrentIssueRoot = issueId === rootIssueId;
return (
<div key={issueId}>
{issue && (
@ -78,7 +84,8 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
style={{ paddingLeft: `${spacingLeft}px` }}
>
<div className="h-[22px] w-[22px] flex-shrink-0">
{subIssueCount > 0 && (
{/* disable the chevron when current issue is also the root issue*/}
{subIssueCount > 0 && !isCurrentIssueRoot && (
<>
{subIssueHelpers.preview_loader.includes(issue.id) ? (
<div className="flex h-full w-full cursor-not-allowed items-center justify-center rounded-sm bg-custom-background-80 transition-all">
@ -206,11 +213,13 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
</div>
)}
{subIssueHelpers.issue_visibility.includes(issueId) && subIssueCount > 0 && (
{/* should not expand the current issue if it is also the root issue*/}
{subIssueHelpers.issue_visibility.includes(issueId) && subIssueCount > 0 && !isCurrentIssueRoot && (
<IssueList
workspaceSlug={workspaceSlug}
projectId={issue.project_id}
parentIssueId={issue.id}
rootIssueId={rootIssueId}
spacingLeft={spacingLeft + 22}
disabled={disabled}
handleIssueCrudState={handleIssueCrudState}

View File

@ -12,6 +12,7 @@ export interface IIssueList {
workspaceSlug: string;
projectId: string;
parentIssueId: string;
rootIssueId: string;
spacingLeft: number;
disabled: boolean;
handleIssueCrudState: (
@ -27,6 +28,7 @@ export const IssueList: FC<IIssueList> = observer((props) => {
workspaceSlug,
projectId,
parentIssueId,
rootIssueId,
spacingLeft = 10,
disabled,
handleIssueCrudState,
@ -50,6 +52,7 @@ export const IssueList: FC<IIssueList> = observer((props) => {
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssueId={parentIssueId}
rootIssueId={rootIssueId}
issueId={issueId}
spacingLeft={spacingLeft}
disabled={disabled}

View File

@ -1,20 +1,22 @@
import { FC, useCallback, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
// icons
import { Plus, ChevronRight, Loader, Pencil } from "lucide-react";
// types
import { IUser, TIssue } from "@plane/types";
// hooks
// ui
import { CircularProgressIndicator, CustomMenu, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { ExistingIssuesListModal } from "@/components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
// helpers
import { cn } from "@/helpers/common.helper";
import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks
import { useEventTracker, useIssueDetail } from "@/hooks/store";
// components
// local components
import { IssueList } from "./issues-list";
// ui
// helpers
// types
export interface ISubIssuesRoot {
workspaceSlug: string;
@ -248,11 +250,6 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
try {
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
await deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Error!",
message: "Issue deleted successfully",
});
captureIssueEvent({
eventName: "Sub-issue deleted",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
@ -399,6 +396,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssueId={parentIssueId}
rootIssueId={parentIssueId}
spacingLeft={10}
disabled={!disabled}
handleIssueCrudState={handleIssueCrudState}
@ -534,6 +532,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
issueCrudState?.delete?.issue?.id as string
)
}
isSubIssue
/>
)}
</>

View File

@ -6,7 +6,7 @@ import { useTheme } from "next-themes";
// types
import { IWorkspaceMemberInvitation, TOnboardingSteps } from "@plane/types";
// components
import { Invitations, OnboardingHeader, SwitchOrDeleteAccountDropdown, CreateWorkspace } from "@/components/onboarding";
import { Invitations, OnboardingHeader, SwitchAccountDropdown, CreateWorkspace } from "@/components/onboarding";
// hooks
import { useUser } from "@/hooks/store";
// assets
@ -55,7 +55,7 @@ export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
<div className="flex items-center justify-between">
<OnboardingHeader currentStep={totalSteps - 1} totalSteps={totalSteps} />
<div className="shrink-0 lg:hidden">
<SwitchOrDeleteAccountDropdown />
<SwitchAccountDropdown />
</div>
</div>
<div className="flex flex-col w-full items-center justify-center p-8 mt-6">
@ -79,7 +79,7 @@ export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
</div>
</div>
<div className="hidden lg:block relative w-2/5 h-screen overflow-hidden px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<SwitchOrDeleteAccountDropdown />
<SwitchAccountDropdown />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? CreateJoinWorkspaceDark : CreateJoinWorkspace}

View File

@ -5,6 +5,6 @@ export * from "./profile-setup";
export * from "./create-workspace";
export * from "./invitations";
export * from "./step-indicator";
export * from "./switch-or-delete-account-dropdown";
export * from "./switch-delete-account-modal";
export * from "./switch-account-dropdown";
export * from "./switch-account-modal";
export * from "./header";

View File

@ -34,7 +34,7 @@ import InviteMembersDark from "public/onboarding/invite-members-dark.svg";
import InviteMembersLight from "public/onboarding/invite-members-light.svg";
// components
import { OnboardingHeader } from "./header";
import { SwitchOrDeleteAccountDropdown } from "./switch-or-delete-account-dropdown";
import { SwitchAccountDropdown } from "./switch-account-dropdown";
type Props = {
finishOnboarding: () => Promise<void>;
@ -366,7 +366,7 @@ export const InviteMembers: React.FC<Props> = (props) => {
{/* Since this will always be the last step */}
<OnboardingHeader currentStep={totalSteps} totalSteps={totalSteps} />
<div className="shrink-0 lg:hidden">
<SwitchOrDeleteAccountDropdown />
<SwitchAccountDropdown />
</div>
</div>
<div className="flex flex-col w-full items-center justify-center p-8 mt-6 md:w-4/5 mx-auto">
@ -433,7 +433,7 @@ export const InviteMembers: React.FC<Props> = (props) => {
</div>
</div>
<div className="hidden lg:block relative w-2/5 h-screen overflow-hidden px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<SwitchOrDeleteAccountDropdown />
<SwitchAccountDropdown />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? InviteMembersDark : InviteMembersLight}

View File

@ -11,7 +11,7 @@ import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { PasswordStrengthMeter } from "@/components/account";
import { UserImageUploadModal } from "@/components/core";
import { OnboardingHeader, SwitchOrDeleteAccountDropdown } from "@/components/onboarding";
import { OnboardingHeader, SwitchAccountDropdown } from "@/components/onboarding";
// constants
import { E_ONBOARDING, USER_DETAILS, E_ONBOARDING_STEP_1,E_ONBOARDING_STEP_2 } from "@/constants/event-tracker";
// helpers
@ -280,7 +280,7 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
<div className="flex items-center justify-between">
<OnboardingHeader currentStep={isCurrentStepUserPersonalization ? 2 : 1} totalSteps={totalSteps} />
<div className="shrink-0 lg:hidden">
<SwitchOrDeleteAccountDropdown fullName={`${watch("first_name")} ${watch("last_name")}`} />
<SwitchAccountDropdown fullName={`${watch("first_name")} ${watch("last_name")}`} />
</div>
</div>
<div className="flex flex-col w-full items-center justify-center p-8 mt-6">
@ -571,7 +571,7 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
</div>
</div>
<div className="hidden lg:block relative w-2/5 h-screen overflow-hidden px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<SwitchOrDeleteAccountDropdown fullName={`${watch("first_name")} ${watch("last_name")}`} />
<SwitchAccountDropdown fullName={`${watch("first_name")} ${watch("last_name")}`} />
<div className="absolute inset-0 z-0">
{profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION ? (
<Image

View File

@ -9,16 +9,16 @@ import { cn } from "@/helpers/common.helper";
// hooks
import { useUser } from "@/hooks/store";
// components
import { SwitchOrDeleteAccountModal } from "./switch-delete-account-modal";
import { SwitchAccountModal } from "./switch-account-modal";
type TSwithOrDeleteAccountDropdownProps = {
type TSwitchAccountDropdownProps = {
fullName?: string;
};
export const SwitchOrDeleteAccountDropdown: FC<TSwithOrDeleteAccountDropdownProps> = observer((props) => {
export const SwitchAccountDropdown: FC<TSwitchAccountDropdownProps> = observer((props) => {
const { fullName } = props;
// states
const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false);
const [showSwitchAccountModal, setShowSwitchAccountModal] = useState(false);
// store hooks
const { data: user } = useUser();
@ -30,7 +30,7 @@ export const SwitchOrDeleteAccountDropdown: FC<TSwithOrDeleteAccountDropdownProp
return (
<div className="flex w-full shrink-0 justify-end">
<SwitchOrDeleteAccountModal isOpen={showDeleteAccountModal} onClose={() => setShowDeleteAccountModal(false)} />
<SwitchAccountModal isOpen={showSwitchAccountModal} onClose={() => setShowSwitchAccountModal(false)} />
<div className="flex items-center gap-x-2 pr-4 z-10">
{user?.avatar && (
<Avatar
@ -64,7 +64,7 @@ export const SwitchOrDeleteAccountDropdown: FC<TSwithOrDeleteAccountDropdownProp
"bg-custom-background-80": active,
})
}
onClick={() => setShowDeleteAccountModal(true)}
onClick={() => setShowSwitchAccountModal(true)}
>
Wrong e-mail address?
</Menu.Item>

View File

@ -1,34 +1,31 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import { mutate } from "swr";
import { Trash2 } from "lucide-react";
import { ArrowRightLeft } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { useUser } from "@/hooks/store";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { useUser } from "@/hooks/store";
type Props = {
isOpen: boolean;
onClose: () => void;
};
export const SwitchOrDeleteAccountModal: React.FC<Props> = (props) => {
export const SwitchAccountModal: React.FC<Props> = (props) => {
const { isOpen, onClose } = props;
// states
const [switchingAccount, setSwitchingAccount] = useState(false);
const [isDeactivating, setIsDeactivating] = useState(false);
// router
const router = useRouter();
// store hooks
const { signOut, deactivateAccount } = useUser();
const { data: userData, signOut } = useUser();
const { setTheme } = useTheme();
const handleClose = () => {
setSwitchingAccount(false);
setIsDeactivating(false);
onClose();
};
@ -51,32 +48,6 @@ export const SwitchOrDeleteAccountModal: React.FC<Props> = (props) => {
.finally(() => setSwitchingAccount(false));
};
const handleDeactivateAccount = async () => {
setIsDeactivating(true);
await deactivateAccount()
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Account deleted successfully.",
});
mutate("CURRENT_USER_DETAILS", null);
signOut();
setTheme("system");
router.push("/");
handleClose();
})
.catch((err: any) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error,
})
)
.finally(() => setIsDeactivating(false));
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
@ -104,32 +75,30 @@ export const SwitchOrDeleteAccountModal: React.FC<Props> = (props) => {
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div>
<div className="flex items-center gap-x-4">
<div className="grid place-items-center rounded-full bg-red-500/20 p-4">
<Trash2 className="h-6 w-6 text-red-600" aria-hidden="true" />
<div className="p-6 pb-1">
<div className="flex gap-x-4">
<div className="flex items-start">
<div className="grid place-items-center rounded-full bg-custom-primary-100/20 p-4">
<ArrowRightLeft className="h-5 w-5 text-custom-primary-100" aria-hidden="true" />
</div>
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100">
Not the right workspace?
</Dialog.Title>
</div>
<div className="mt-6 px-4">
<ul className="list-disc text-base font-normal text-onboarding-text-300">
<li>Delete this account if you have another and won{"'"}t use this account.</li>
<li>Switch to another account if you{"'"}d like to come back to this account another time.</li>
</ul>
<div className="flex flex-col py-3 gap-y-6">
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100">
Switch account
</Dialog.Title>
{userData?.email && (
<div className="text-base font-normal text-onboarding-text-200">
If you have signed up via <span className="text-custom-primary-100">{userData.email}</span>{" "}
un-intentionally, you can switch your account to a different one from here.
</div>
)}
</div>
</div>
</div>
<div className="mb-2 flex items-center justify-end gap-3 p-4 sm:px-6">
<Button variant="neutral-primary" onClick={handleSwitchAccount} disabled={switchingAccount}>
<Button variant="accent-primary" onClick={handleSwitchAccount} disabled={switchingAccount}>
{switchingAccount ? "Switching..." : "Switch account"}
</Button>
<Button variant="outline-danger" onClick={handleDeactivateAccount} loading={isDeactivating}>
{isDeactivating ? "Deleting..." : "Delete account"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>

View File

@ -1,5 +1,7 @@
import { ReactNode } from "react";
import Link from "next/link";
// helpers
import { SUPPORT_EMAIL } from "./common.helper";
export enum EPageTypes {
PUBLIC = "PUBLIC",
@ -34,6 +36,9 @@ export enum EAuthenticationErrorCodes {
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",
@ -45,7 +50,6 @@ export enum EAuthenticationErrorCodes {
INVALID_EMAIL_MAGIC_SIGN_UP = "5050",
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055",
// Sign In
USER_ACCOUNT_DEACTIVATED = "5019",
USER_DOES_NOT_EXIST = "5060",
AUTHENTICATION_FAILED_SIGN_IN = "5065",
REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070",
@ -82,6 +86,9 @@ export enum EAuthenticationErrorCodes {
ADMIN_AUTHENTICATION_FAILED = "5175",
ADMIN_USER_ALREADY_EXIST = "5180",
ADMIN_USER_DOES_NOT_EXIST = "5185",
ADMIN_USER_DEACTIVATED = "5190",
// Rate limit
RATE_LIMIT_EXCEEDED = "5900",
}
export type TAuthErrorInfo = {
@ -99,10 +106,30 @@ const errorCodeMessages: {
title: `Instance not configured`,
message: () => `Instance not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.INVALID_EMAIL]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
[EAuthenticationErrorCodes.EMAIL_REQUIRED]: {
title: `Email required`,
message: () => `Email required. Please try again.`,
},
[EAuthenticationErrorCodes.SIGNUP_DISABLED]: {
title: `Sign up disabled`,
message: () => `Sign up disabled. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.MAGIC_LINK_LOGIN_DISABLED]: {
title: `Magic link login disabled`,
message: () => `Magic link login disabled. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.PASSWORD_LOGIN_DISABLED]: {
title: `Password login disabled`,
message: () => `Password login disabled. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
title: `User account deactivated`,
message: () => `User account deactivated. Please contact ${!!SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
},
[EAuthenticationErrorCodes.INVALID_PASSWORD]: {
title: `Invalid password`,
message: () => `Invalid password. Please try again.`,
@ -112,16 +139,6 @@ const errorCodeMessages: {
message: () => `SMTP not configured. Please contact your administrator.`,
},
// email check in both sign up and sign in
[EAuthenticationErrorCodes.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`,
@ -159,12 +176,6 @@ const errorCodeMessages: {
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) => (
@ -324,6 +335,14 @@ const errorCodeMessages: {
</div>
),
},
[EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED]: {
title: `Admin user deactivated`,
message: () => <div>Your account is deactivated</div>,
},
[EAuthenticationErrorCodes.RATE_LIMIT_EXCEEDED]: {
title: "",
message: () => `Rate limit exceeded. Please try again later.`,
},
};
export const authErrorHandler = (
@ -335,6 +354,9 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.INVALID_EMAIL,
EAuthenticationErrorCodes.EMAIL_REQUIRED,
EAuthenticationErrorCodes.SIGNUP_DISABLED,
EAuthenticationErrorCodes.MAGIC_LINK_LOGIN_DISABLED,
EAuthenticationErrorCodes.PASSWORD_LOGIN_DISABLED,
EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED,
EAuthenticationErrorCodes.INVALID_PASSWORD,
EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED,
EAuthenticationErrorCodes.USER_ALREADY_EXIST,
@ -362,6 +384,7 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
EAuthenticationErrorCodes.MISSING_PASSWORD,
EAuthenticationErrorCodes.INVALID_NEW_PASSWORD,
EAuthenticationErrorCodes.PASSWORD_ALREADY_SET,
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
@ -372,7 +395,8 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED,
EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED,
EAuthenticationErrorCodes.RATE_LIMIT_EXCEEDED,
];
if (bannerAlertErrorCodes.includes(errorCode))

View File

@ -9,6 +9,8 @@ export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || "";
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "";
export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}/`);
export const debounce = (func: any, wait: number, immediate: boolean = false) => {

Some files were not shown because too many files have changed in this diff Show More