mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: merge conflicts resolved from preview
This commit is contained in:
commit
9c1e32a323
59
.eslintrc-staged.js
Normal file
59
.eslintrc-staged.js
Normal 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
2
.github/workflows/create-sync-pr.yml
vendored
2
.github/workflows/create-sync-pr.yml
vendored
@ -64,6 +64,6 @@ jobs:
|
||||
echo "Pull Request already exists: $PR_EXISTS"
|
||||
else
|
||||
echo "Creating new pull request"
|
||||
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH --title "sync: merge conflicts need to be resolved" --body "")
|
||||
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH --title "sync: community changes" --body "")
|
||||
echo "Pull Request created: $PR_URL"
|
||||
fi
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
0
.husky/pre-commit
Normal file
3
.lintstagedrc.json
Normal file
3
.lintstagedrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"*.{ts,tsx,js,jsx}": ["eslint -c ./.eslintrc-staged.js", "prettier --check"]
|
||||
}
|
@ -82,7 +82,7 @@ COPY apiserver/templates templates/
|
||||
RUN apk --no-cache add "bash~=5.2"
|
||||
COPY apiserver/bin ./bin/
|
||||
|
||||
RUN chmod +x ./bin/takeoff ./bin/worker
|
||||
RUN chmod +x ./bin/*
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
# Expose container port and run entry point script
|
||||
|
@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
import { SWRConfig } from "swr";
|
||||
// ui
|
||||
import { Toast } from "@plane/ui";
|
||||
// constants
|
||||
import { SWR_CONFIG } from "@/constants/swr-config";
|
||||
// helpers
|
||||
import { ASSET_PREFIX } from "@/helpers/common.helper";
|
||||
import { ASSET_PREFIX, resolveGeneralTheme } from "@/helpers/common.helper";
|
||||
// lib
|
||||
import { InstanceProvider } from "@/lib/instance-provider";
|
||||
import { StoreProvider } from "@/lib/store-provider";
|
||||
@ -15,6 +17,9 @@ import { UserProvider } from "@/lib/user-provider";
|
||||
import "./globals.css";
|
||||
|
||||
function RootLayout({ children }: { children: ReactNode }) {
|
||||
// themes
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
@ -26,6 +31,7 @@ function RootLayout({ children }: { children: ReactNode }) {
|
||||
</head>
|
||||
<body className={`antialiased`}>
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
|
||||
<SWRConfig value={SWR_CONFIG}>
|
||||
<StoreProvider>
|
||||
<InstanceProvider>
|
||||
|
@ -42,11 +42,10 @@ RUN apk --no-cache add "bash~=5.2"
|
||||
COPY ./bin ./bin/
|
||||
|
||||
RUN mkdir -p /code/plane/logs
|
||||
RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
|
||||
RUN chmod +x ./bin/*
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
# CMD [ "./bin/takeoff" ]
|
||||
|
||||
|
@ -41,5 +41,5 @@ RUN chmod -R 777 /code
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
CMD [ "./bin/takeoff.local" ]
|
||||
CMD [ "./bin/docker-entrypoint-api-local.sh" ]
|
||||
|
||||
|
0
apiserver/bin/beat → apiserver/bin/docker-entrypoint-beat.sh
Executable file → Normal file
0
apiserver/bin/beat → apiserver/bin/docker-entrypoint-beat.sh
Executable file → Normal file
6
apiserver/bin/docker-entrypoint-migrator.sh
Normal file
6
apiserver/bin/docker-entrypoint-migrator.sh
Normal file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
python manage.py wait_for_db $1
|
||||
|
||||
python manage.py migrate $1
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -1,8 +0,0 @@
|
||||
## Coolify Setup
|
||||
|
||||
Access the `coolify-docker-compose` file [here](https://raw.githubusercontent.com/makeplane/plane/master/deploy/coolify/coolify-docker-compose.yml) or download using using below command
|
||||
|
||||
```
|
||||
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/coolify/coolify-docker-compose.yml
|
||||
|
||||
```
|
@ -1,230 +0,0 @@
|
||||
|
||||
services:
|
||||
web:
|
||||
container_name: web
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-frontend:latest
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
environment:
|
||||
- NEXT_PUBLIC_DEPLOY_URL=$SERVICE_FQDN_SPACE_8082
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
|
||||
space:
|
||||
container_name: space
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-space:latest
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
environment:
|
||||
- SERVICE_FQDN_SPACE_8082=/api
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
api:
|
||||
container_name: api
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/takeoff
|
||||
environment:
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- PGUSER=${PGUSER:-plane}
|
||||
- PGPASSWORD=${PGPASSWORD:-plane}
|
||||
- PGHOST=${PGHOST:-plane-db}
|
||||
- PGDATABASE=${PGDATABASE:-plane}
|
||||
- DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_URL=redis://${REDIS_HOST}:6379/
|
||||
- EMAIL_HOST=${EMAIL_HOST:-""}
|
||||
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
|
||||
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
|
||||
- EMAIL_PORT=${EMAIL_PORT:-587}
|
||||
- EMAIL_FROM=${EMAIL_FROM:-Team Plane <team@mailer.plane.so>}
|
||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
|
||||
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
|
||||
- AWS_REGION=${AWS_REGION:-""}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-}
|
||||
- GPT_ENGINE=${GPT_ENGINE:-gpt-3.5-turbo}
|
||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
||||
- DOCKERIZED=${DOCKERIZED:-1}
|
||||
- USE_MINIO=${USE_MINIO:-1}
|
||||
- NGINX_PORT=${NGINX_PORT:-8082}
|
||||
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
|
||||
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||
- ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1}
|
||||
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
|
||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
- WEB_URL=$SERVICE_FQDN_PLANE_8082
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
worker:
|
||||
container_name: bgworker
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/worker
|
||||
environment:
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- PGUSER=${PGUSER:-plane}
|
||||
- PGPASSWORD=${PGPASSWORD:-plane}
|
||||
- PGHOST=${PGHOST:-plane-db}
|
||||
- PGDATABASE=${PGDATABASE:-plane}
|
||||
- DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_URL=redis://${REDIS_HOST}:6379/
|
||||
- EMAIL_HOST=${EMAIL_HOST:-""}
|
||||
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
|
||||
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
|
||||
- EMAIL_PORT=${EMAIL_PORT:-587}
|
||||
- EMAIL_FROM=${EMAIL_FROM:-Team Plane <team@mailer.plane.so>}
|
||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
|
||||
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
|
||||
- AWS_REGION=${AWS_REGION:-""}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-}
|
||||
- GPT_ENGINE=${GPT_ENGINE:-gpt-3.5-turbo}
|
||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
||||
- DOCKERIZED=${DOCKERIZED:-1}
|
||||
- USE_MINIO=${USE_MINIO:-1}
|
||||
- NGINX_PORT=${NGINX_PORT:-8082}
|
||||
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
|
||||
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
beat-worker:
|
||||
container_name: beatworker
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/beat
|
||||
environment:
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- PGUSER=${PGUSER:-plane}
|
||||
- PGPASSWORD=${PGPASSWORD:-plane}
|
||||
- PGHOST=${PGHOST:-plane-db}
|
||||
- PGDATABASE=${PGDATABASE:-plane}
|
||||
- DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_URL=redis://${REDIS_HOST}:6379/
|
||||
- EMAIL_HOST=${EMAIL_HOST:-""}
|
||||
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
|
||||
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
|
||||
- EMAIL_PORT=${EMAIL_PORT:-587}
|
||||
- EMAIL_FROM=${EMAIL_FROM:-Team Plane <team@mailer.plane.so>}
|
||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
|
||||
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
|
||||
- AWS_REGION=${AWS_REGION:-""}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-}
|
||||
- GPT_ENGINE=${GPT_ENGINE:-gpt-3.5-turbo}
|
||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
||||
- DOCKERIZED=${DOCKERIZED:-1}
|
||||
- USE_MINIO=${USE_MINIO:-1}
|
||||
- NGINX_PORT=${NGINX_PORT:-8082}
|
||||
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
|
||||
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-db:
|
||||
container_name: plane-db
|
||||
image: postgres:15.2-alpine
|
||||
restart: always
|
||||
command: postgres -c 'max_connections=1000'
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER:-plane}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-plane}
|
||||
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
|
||||
|
||||
plane-redis:
|
||||
container_name: plane-redis
|
||||
image: redis:7.2.4-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
plane-minio:
|
||||
container_name: plane-minio
|
||||
image: minio/minio
|
||||
restart: always
|
||||
command: server /export --console-address ":9090"
|
||||
volumes:
|
||||
- uploads:/export
|
||||
environment:
|
||||
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-access-key}
|
||||
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-secret-key}
|
||||
|
||||
createbuckets:
|
||||
image: minio/mc
|
||||
entrypoint: >
|
||||
/bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
|
||||
environment:
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
depends_on:
|
||||
- plane-minio
|
||||
|
||||
# Comment this if you already have a reverse proxy running
|
||||
proxy:
|
||||
container_name: proxy
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-proxy:latest
|
||||
ports:
|
||||
- 8082:80
|
||||
environment:
|
||||
- SERVICE_FQDN_PLANE_8082
|
||||
- NGINX_PORT=${NGINX_PORT:-8082}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
- BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
depends_on:
|
||||
- web
|
||||
- api
|
||||
- space
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
uploads:
|
@ -86,7 +86,7 @@ services:
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/takeoff
|
||||
command: ./bin/docker-entrypoint-api.sh
|
||||
deploy:
|
||||
replicas: ${API_REPLICAS:-1}
|
||||
volumes:
|
||||
@ -101,7 +101,7 @@ services:
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/worker
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
volumes:
|
||||
- logs_worker:/code/plane/logs
|
||||
depends_on:
|
||||
@ -115,7 +115,7 @@ services:
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/beat
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
volumes:
|
||||
- logs_beat-worker:/code/plane/logs
|
||||
depends_on:
|
||||
@ -129,9 +129,7 @@ services:
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: no
|
||||
command: >
|
||||
sh -c "python manage.py wait_for_db &&
|
||||
python manage.py migrate"
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
volumes:
|
||||
- logs_migrator:/code/plane/logs
|
||||
depends_on:
|
||||
|
@ -6,7 +6,6 @@ volumes:
|
||||
redisdata:
|
||||
uploads:
|
||||
pgdata:
|
||||
|
||||
|
||||
services:
|
||||
plane-redis:
|
||||
@ -16,7 +15,7 @@ services:
|
||||
- dev_env
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
|
||||
plane-minio:
|
||||
image: minio/minio
|
||||
restart: unless-stopped
|
||||
@ -30,7 +29,7 @@ services:
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||
|
||||
|
||||
plane-db:
|
||||
image: postgres:15.2-alpine
|
||||
restart: unless-stopped
|
||||
@ -98,13 +97,13 @@ services:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apiserver:/code
|
||||
command: ./bin/takeoff.local
|
||||
command: ./bin/docker-entrypoint-api.sh
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: ./apiserver
|
||||
@ -116,7 +115,7 @@ services:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apiserver:/code
|
||||
command: ./bin/worker
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@ -135,7 +134,7 @@ services:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apiserver:/code
|
||||
command: ./bin/beat
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@ -154,9 +153,7 @@ services:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apiserver:/code
|
||||
command: >
|
||||
sh -c "python manage.py wait_for_db --settings=plane.settings.local &&
|
||||
python manage.py migrate --settings=plane.settings.local"
|
||||
command: ./bin/docker-entrypoint-migrator.sh --settings=plane.settings.local
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
|
@ -45,7 +45,7 @@ services:
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: ./bin/takeoff
|
||||
command: ./bin/docker-entrypoint-api.sh
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@ -60,7 +60,7 @@ services:
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: ./bin/worker
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@ -76,7 +76,7 @@ services:
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: ./bin/beat
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@ -92,9 +92,7 @@ services:
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: no
|
||||
command: >
|
||||
sh -c "python manage.py wait_for_db &&
|
||||
python manage.py migrate"
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
|
@ -1,4 +1,4 @@
|
||||
[supervisord] ## This is the main process for the Supervisor
|
||||
[supervisord] ## This is the main process for the Supervisor
|
||||
nodaemon=true
|
||||
|
||||
[program:node]
|
||||
@ -10,7 +10,7 @@ stdout_logfile=/var/log/node.out.log
|
||||
|
||||
[program:python]
|
||||
directory=/code
|
||||
command=sh bin/takeoff
|
||||
command=sh bin/docker-entrypoint-api.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/python.err.log
|
||||
|
@ -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",
|
||||
|
@ -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"),
|
||||
|
3
packages/types/src/auth.d.ts
vendored
3
packages/types/src/auth.d.ts
vendored
@ -5,8 +5,7 @@ export interface IEmailCheckData {
|
||||
}
|
||||
|
||||
export interface IEmailCheckResponse {
|
||||
is_password_autoset: boolean;
|
||||
status: boolean;
|
||||
status: "MAGIC_CODE" | "CREDENTIAL";
|
||||
existing: boolean;
|
||||
}
|
||||
|
||||
|
28
packages/ui/.storybook/main.ts
Normal file
28
packages/ui/.storybook/main.ts
Normal 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;
|
14
packages/ui/.storybook/preview.ts
Normal file
14
packages/ui/.storybook/preview.ts
Normal 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;
|
@ -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"
|
||||
|
19
packages/ui/src/avatar/avatar.stories.tsx
Normal file
19
packages/ui/src/avatar/avatar.stories.tsx
Normal 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" },
|
||||
};
|
@ -17,7 +17,7 @@ export const DragHandle = forwardRef<HTMLButtonElement | null, IDragHandle>((pro
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`mr-1 p-[2px] flex flex-shrink-0 rounded bg-custom-background-90 text-custom-sidebar-text-200 group-hover:opacity-100 cursor-grab ${
|
||||
className={` p-[2px] flex flex-shrink-0 rounded bg-custom-background-90 text-custom-sidebar-text-200 group-hover:opacity-100 cursor-grab ${
|
||||
isDragging ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
onContextMenu={(e) => {
|
||||
|
650
packages/ui/styles/globals.css
Normal file
650
packages/ui/styles/globals.css
Normal 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;
|
||||
}
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -12,6 +12,7 @@ import { UserAvatar } from "@/components/issues/navbar/user-avatar";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useProject, useIssueFilter, useIssueDetails } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
// types
|
||||
import { TIssueLayout } from "@/types/issue";
|
||||
|
||||
@ -39,6 +40,8 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
|
||||
// derived values
|
||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId && settings) {
|
||||
const viewsAcceptable: string[] = [];
|
||||
@ -111,7 +114,7 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
|
||||
<NavbarTheme />
|
||||
</div>
|
||||
|
||||
<UserAvatar />
|
||||
{!isInIframe && <UserAvatar />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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.",
|
||||
})
|
||||
|
@ -11,6 +11,7 @@ import { CommentReactions } from "@/components/issues/peek-overview";
|
||||
import { timeAgo } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
// types
|
||||
import { Comment } from "@/types/issue";
|
||||
|
||||
@ -25,6 +26,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
const { workspace } = useProject();
|
||||
const { peekId, deleteIssueComment, updateIssueComment } = useIssueDetails();
|
||||
const { data: currentUser } = useUser();
|
||||
const isInIframe = useIsInIframe();
|
||||
// derived values
|
||||
const workspaceId = workspace?.id;
|
||||
|
||||
@ -138,7 +140,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentUser?.id === comment?.actor_detail?.id && (
|
||||
{!isInIframe && currentUser?.id === comment?.actor_detail?.id && (
|
||||
<Menu as="div" className="relative w-min text-left">
|
||||
<Menu.Button
|
||||
type="button"
|
||||
|
@ -5,10 +5,12 @@ import { Tooltip } from "@plane/ui";
|
||||
// ui
|
||||
import { ReactionSelector } from "@/components/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
type Props = {
|
||||
commentId: string;
|
||||
@ -30,6 +32,7 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
|
||||
// hooks
|
||||
const { addCommentReaction, removeCommentReaction, details, peekId } = useIssueDetails();
|
||||
const { data: user } = useUser();
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
const commentReactions = peekId ? details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions : [];
|
||||
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
|
||||
@ -58,15 +61,17 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
<ReactionSelector
|
||||
onSelect={(value) => {
|
||||
if (user) handleReactionClick(value);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
position="top"
|
||||
selected={userReactions?.map((r) => r.reaction)}
|
||||
size="md"
|
||||
/>
|
||||
{!isInIframe && (
|
||||
<ReactionSelector
|
||||
onSelect={(value) => {
|
||||
if (user) handleReactionClick(value);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
position="top"
|
||||
selected={userReactions?.map((r) => r.reaction)}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
|
||||
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||
const reactions = groupedReactions?.[reaction] ?? [];
|
||||
@ -89,14 +94,20 @@ export const CommentReactions: React.FC<Props> = observer((props) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isInIframe) return;
|
||||
if (user) handleReactionClick(reaction);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
commentReactions?.some((r) => r?.actor_detail?.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
className={cn(
|
||||
`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
commentReactions?.some((r) => r?.actor_detail?.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`,
|
||||
{
|
||||
"cursor-default": isInIframe,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
|
@ -3,16 +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 useToast from "@/hooks/use-toast";
|
||||
// store
|
||||
import { IPeekMode } from "@/store/issue-detail.store";
|
||||
import useClipboardWritePermission from "@/hooks/use-clipboard-write-permission";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
import { IIssue, IPeekMode } from "@/types/issue";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
@ -41,15 +40,14 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
const { handleClose } = props;
|
||||
|
||||
const { peekMode, setPeekMode } = useIssueDetails();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
const isClipboardWriteAllowed = useClipboardWritePermission();
|
||||
|
||||
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",
|
||||
});
|
||||
@ -117,7 +115,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
</Transition>
|
||||
</Listbox>
|
||||
</div>
|
||||
{(peekMode === "side" || peekMode === "modal") && (
|
||||
{isClipboardWriteAllowed && (peekMode === "side" || peekMode === "modal") && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45 focus:outline-none" tabIndex={1}>
|
||||
<Icon iconName="link" className="text-[1rem]" />
|
||||
|
@ -8,6 +8,7 @@ import { CommentCard, AddComment } from "@/components/issues/peek-overview";
|
||||
import { Icon } from "@/components/ui";
|
||||
// hooks
|
||||
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
|
||||
@ -25,6 +26,7 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
||||
const { canComment } = useProject();
|
||||
const { details, peekId } = useIssueDetails();
|
||||
const { data: currentUser } = useUser();
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
const comments = details[peekId || ""]?.comments || [];
|
||||
|
||||
@ -38,25 +40,26 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
||||
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspaceSlug?.toString()} />
|
||||
))}
|
||||
</div>
|
||||
{currentUser ? (
|
||||
<>
|
||||
{canComment && (
|
||||
<div className="mt-4">
|
||||
<AddComment disabled={!currentUser} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4 flex items-center justify-between gap-2 rounded border border-custom-border-300 bg-custom-background-80 px-2 py-2.5">
|
||||
<p className="flex gap-2 overflow-hidden break-words text-sm text-custom-text-200">
|
||||
<Icon iconName="lock" className="!text-sm" />
|
||||
Sign in to add your comment
|
||||
</p>
|
||||
<Link href={`/?next_path=${pathname}`}>
|
||||
<Button variant="primary">Sign in</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{!isInIframe &&
|
||||
(currentUser ? (
|
||||
<>
|
||||
{canComment && (
|
||||
<div className="mt-4">
|
||||
<AddComment disabled={!currentUser} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4 flex items-center justify-between gap-2 rounded border border-custom-border-300 bg-custom-background-80 px-2 py-2.5">
|
||||
<p className="flex gap-2 overflow-hidden break-words text-sm text-custom-text-200">
|
||||
<Icon iconName="lock" className="!text-sm" />
|
||||
Sign in to add your comment
|
||||
</p>
|
||||
<Link href={`/?next_path=${pathname}`}>
|
||||
<Button variant="primary">Sign in</Button>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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",
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useParams } from "next/navigation";
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview";
|
||||
import { useProject } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
// type IssueReactionsProps = {
|
||||
// workspaceSlug: string;
|
||||
@ -11,6 +12,7 @@ export const IssueReactions: React.FC = () => {
|
||||
const { workspace_slug: workspaceSlug, project_id: projectId } = useParams<any>();
|
||||
|
||||
const { canVote, canReact } = useProject();
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
@ -21,7 +23,7 @@ export const IssueReactions: React.FC = () => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{canReact && (
|
||||
{!isInIframe && canReact && (
|
||||
<div className="flex items-center gap-2">
|
||||
<IssueEmojiReactions workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
|
@ -5,9 +5,11 @@ import { observer } from "mobx-react-lite";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
type TIssueVotes = {
|
||||
workspaceSlug: string;
|
||||
@ -32,6 +34,8 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
const issueDetailsStore = useIssueDetails();
|
||||
const { data: user, fetchCurrentUser } = useUser();
|
||||
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
|
||||
const votes = issueId ? issueDetailsStore.details[issueId]?.votes : [];
|
||||
@ -94,12 +98,18 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
onClick={(e) => {
|
||||
if (isInIframe) return;
|
||||
if (user) handleVote(e, 1);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 h-7 focus:outline-none ${
|
||||
isUpVotedByUser ? "border-custom-primary-200 text-custom-primary-200" : "border-custom-border-300"
|
||||
}`}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 h-7 focus:outline-none",
|
||||
{
|
||||
"border-custom-primary-200 text-custom-primary-200": isUpVotedByUser,
|
||||
"border-custom-border-300": !isUpVotedByUser,
|
||||
"cursor-default": isInIframe,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-rounded !m-0 !p-0 text-base">arrow_upward_alt</span>
|
||||
<span className="text-sm font-normal transition-opacity ease-in-out">{allUpVotes.length}</span>
|
||||
@ -128,12 +138,18 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
onClick={(e) => {
|
||||
if (isInIframe) return;
|
||||
if (user) handleVote(e, -1);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex items-center justify-center gap-x-1 h-7 overflow-hidden rounded border px-2 focus:outline-none ${
|
||||
isDownVotedByUser ? "border-red-600 text-red-600" : "border-custom-border-300"
|
||||
}`}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-1 h-7 overflow-hidden rounded border px-2 focus:outline-none",
|
||||
{
|
||||
"border-red-600 text-red-600": isDownVotedByUser,
|
||||
"border-custom-border-300": !isDownVotedByUser,
|
||||
"cursor-default": isInIframe,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-rounded !m-0 !p-0 text-base">arrow_downward_alt</span>
|
||||
<span className="text-sm font-normal transition-opacity ease-in-out">{allDownVotes.length}</span>
|
||||
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
@ -15,3 +15,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";
|
||||
|
28
space/hooks/use-clipboard-write-permission.tsx
Normal file
28
space/hooks/use-clipboard-write-permission.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const useClipboardWritePermission = () => {
|
||||
const [isClipboardWriteAllowed, setClipboardWriteAllowed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkClipboardWriteAccess = () => {
|
||||
navigator.permissions
|
||||
.query({ name: "clipboard-write" as PermissionName })
|
||||
.then((result) => {
|
||||
if (result.state === "granted") {
|
||||
setClipboardWriteAllowed(true);
|
||||
} else {
|
||||
setClipboardWriteAllowed(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setClipboardWriteAllowed(false);
|
||||
});
|
||||
};
|
||||
|
||||
checkClipboardWriteAccess();
|
||||
}, []);
|
||||
|
||||
return isClipboardWriteAllowed;
|
||||
};
|
||||
|
||||
export default useClipboardWritePermission;
|
17
space/hooks/use-is-in-iframe.tsx
Normal file
17
space/hooks/use-is-in-iframe.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const useIsInIframe = () => {
|
||||
const [isInIframe, setIsInIframe] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkIfInIframe = () => {
|
||||
setIsInIframe(window.self !== window.top);
|
||||
};
|
||||
|
||||
checkIfInIframe();
|
||||
}, []);
|
||||
|
||||
return isInIframe;
|
||||
};
|
||||
|
||||
export default useIsInIframe;
|
@ -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;
|
20
space/lib/toast-provider.tsx
Normal file
20
space/lib/toast-provider.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
2
space/types/issue.d.ts
vendored
2
space/types/issue.d.ts
vendored
@ -66,6 +66,8 @@ export interface IIssue {
|
||||
votes: IVote[];
|
||||
}
|
||||
|
||||
export type IPeekMode = "side" | "modal" | "full";
|
||||
|
||||
export interface IIssueState {
|
||||
id: string;
|
||||
name: string;
|
||||
|
@ -20,6 +20,7 @@
|
||||
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||
"NEXT_PUBLIC_POSTHOG_HOST",
|
||||
"NEXT_PUBLIC_POSTHOG_DEBUG",
|
||||
"NEXT_PUBLIC_SUPPORT_EMAIL",
|
||||
"SENTRY_AUTH_TOKEN"
|
||||
],
|
||||
"pipeline": {
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -37,67 +37,84 @@ 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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@ -108,8 +125,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)
|
||||
@ -121,6 +147,7 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
if (!authMode) return <></>;
|
||||
return (
|
||||
<div className="relative flex flex-col space-y-6">
|
||||
<AuthHeader
|
||||
@ -138,23 +165,16 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
|
||||
<AuthUniqueCodeForm
|
||||
mode={authMode}
|
||||
email={email}
|
||||
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);
|
||||
|
@ -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)}
|
||||
|
@ -20,7 +20,6 @@ import { AuthService } from "@/services/auth.service";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
isPasswordAutoset: boolean;
|
||||
isSMTPConfigured: boolean;
|
||||
mode: EAuthModes;
|
||||
handleEmailClear: () => void;
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -45,7 +45,7 @@ export const ScopeAndDemand: React.FC<Props> = (props) => {
|
||||
|
||||
// scope data
|
||||
const pendingIssues = defaultAnalytics?.pending_issue_user ?? [];
|
||||
const pendingUnAssignedIssues = pendingIssues?.filter((issue) => issue.assignees__id === null);
|
||||
const pendingUnAssignedIssuesUser = pendingIssues?.find((issue) => issue.assignees__id === null);
|
||||
const pendingAssignedIssues = pendingIssues?.filter((issue) => issue.assignees__id !== null);
|
||||
|
||||
return (
|
||||
@ -56,7 +56,7 @@ export const ScopeAndDemand: React.FC<Props> = (props) => {
|
||||
<div className={`grid grid-cols-1 gap-5 ${fullScreen ? "md:grid-cols-2" : ""}`}>
|
||||
<AnalyticsDemand defaultAnalytics={defaultAnalytics} />
|
||||
<AnalyticsScope
|
||||
pendingUnAssignedIssues={pendingUnAssignedIssues}
|
||||
pendingUnAssignedIssuesUser={pendingUnAssignedIssuesUser}
|
||||
pendingAssignedIssues={pendingAssignedIssues}
|
||||
/>
|
||||
<AnalyticsLeaderBoard
|
||||
|
@ -6,19 +6,21 @@ import emptyBarGraph from "public/empty-state/empty_bar_graph.svg";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
pendingUnAssignedIssues: IDefaultAnalyticsUser[];
|
||||
pendingUnAssignedIssuesUser: IDefaultAnalyticsUser | undefined;
|
||||
pendingAssignedIssues: IDefaultAnalyticsUser[];
|
||||
};
|
||||
|
||||
export const AnalyticsScope: React.FC<Props> = ({ pendingUnAssignedIssues, pendingAssignedIssues }) => (
|
||||
export const AnalyticsScope: React.FC<Props> = ({ pendingUnAssignedIssuesUser, pendingAssignedIssues }) => (
|
||||
<div className="rounded-[10px] border border-custom-border-200 p-3">
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h6 className=" text-base font-medium">Pending issues</h6>
|
||||
<div className="relative flex items-center py-1 px-3 rounded-md gap-2 text-xs text-custom-primary-100 bg-custom-primary-100/10">
|
||||
Unassigned: {pendingUnAssignedIssues.length}
|
||||
</div>
|
||||
{pendingUnAssignedIssuesUser && (
|
||||
<div className="relative flex items-center py-1 px-3 rounded-md gap-2 text-xs text-custom-primary-100 bg-custom-primary-100/10">
|
||||
Unassigned: {pendingUnAssignedIssuesUser.count}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pendingAssignedIssues && pendingAssignedIssues.length > 0 ? (
|
||||
|
@ -8,8 +8,7 @@ import { ContrastIcon } from "@plane/ui";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store";
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
// local components and constants
|
||||
import { DropdownButton } from "../buttons";
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
|
||||
@ -57,32 +56,18 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedName = value ? getCycleNameById(value) : null;
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose && onClose();
|
||||
};
|
||||
const { handleClose, handleKeyDown, handleOnClick } = useDropdown({
|
||||
dropdownRef,
|
||||
isOpen,
|
||||
onClose,
|
||||
setIsOpen,
|
||||
});
|
||||
|
||||
const dropdownOnChange = (val: string | null) => {
|
||||
onChange(val);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
|
@ -4,15 +4,14 @@ import { DateRange, DayPicker, Matcher } from "react-day-picker";
|
||||
import { usePopper } from "react-popper";
|
||||
import { ArrowRight, CalendarDays } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// hooks
|
||||
// components
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
// hooks
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
// components
|
||||
import { DropdownButton } from "./buttons";
|
||||
// types
|
||||
import { TButtonVariants } from "./types";
|
||||
@ -105,6 +104,13 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const { handleKeyDown, handleOnClick } = useDropdown({
|
||||
dropdownRef,
|
||||
isOpen,
|
||||
onOpen,
|
||||
setIsOpen,
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
@ -115,21 +121,6 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
const disabledDays: Matcher[] = [];
|
||||
if (minDate) disabledDays.push({ before: minDate });
|
||||
if (maxDate) disabledDays.push({ after: maxDate });
|
||||
|
@ -7,14 +7,13 @@ import { Combobox } from "@headlessui/react";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate, getDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
// components
|
||||
import { DropdownButton } from "./buttons";
|
||||
// types
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||
import { TDropdownProps } from "./types";
|
||||
// constants
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||
// types
|
||||
import { TDropdownProps } from "./types";
|
||||
|
||||
type Props = TDropdownProps & {
|
||||
clearIconClassName?: string;
|
||||
@ -76,34 +75,22 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose && onClose();
|
||||
};
|
||||
const { handleClose, handleKeyDown, handleOnClick } = useDropdown({
|
||||
dropdownRef,
|
||||
isOpen,
|
||||
onClose,
|
||||
onOpen,
|
||||
setIsOpen,
|
||||
});
|
||||
|
||||
const dropdownOnChange = (val: Date | null) => {
|
||||
onChange(val);
|
||||
if (closeOnSelect) handleClose();
|
||||
if (closeOnSelect) {
|
||||
handleClose();
|
||||
referenceElement?.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
const disabledDays: Matcher[] = [];
|
||||
if (minDate) disabledDays.push({ before: minDate });
|
||||
if (maxDate) disabledDays.push({ after: maxDate });
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { Fragment, ReactNode, useRef, useState } from "react";
|
||||
import sortBy from "lodash/sortBy";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePopper } from "react-popper";
|
||||
@ -8,8 +8,7 @@ import { Combobox } from "@headlessui/react";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppRouter, useEstimate } from "@/hooks/store";
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
// components
|
||||
import { DropdownButton } from "./buttons";
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||
@ -106,50 +105,26 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
|
||||
|
||||
const onOpen = () => {
|
||||
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
|
||||
const onOpen = async () => {
|
||||
if (!activeEstimate && workspaceSlug) await fetchProjectEstimates(workspaceSlug, projectId);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose && onClose();
|
||||
};
|
||||
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
|
||||
dropdownRef,
|
||||
inputRef,
|
||||
isOpen,
|
||||
onClose,
|
||||
onOpen,
|
||||
query,
|
||||
setIsOpen,
|
||||
setQuery,
|
||||
});
|
||||
|
||||
const dropdownOnChange = (val: number | null) => {
|
||||
onChange(val);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (query !== "" && e.key === "Escape") {
|
||||
e.stopPropagation();
|
||||
setQuery("");
|
||||
}
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
|
@ -7,8 +7,7 @@ import { Combobox } from "@headlessui/react";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
// components
|
||||
import { DropdownButton } from "../buttons";
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
|
||||
@ -62,32 +61,18 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
if (multiple) comboboxProps.multiple = true;
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose && onClose();
|
||||
};
|
||||
const { handleClose, handleKeyDown, handleOnClick } = useDropdown({
|
||||
dropdownRef,
|
||||
isOpen,
|
||||
onClose,
|
||||
setIsOpen,
|
||||
});
|
||||
|
||||
const dropdownOnChange = (val: string & string[]) => {
|
||||
onChange(val);
|
||||
if (!multiple) handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
|
@ -2,19 +2,18 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ChevronDown, X } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// hooks
|
||||
// ui
|
||||
import { DiceIcon, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useModule } from "@/hooks/store";
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// components
|
||||
import { DropdownButton } from "../buttons";
|
||||
// icons
|
||||
// helpers
|
||||
// types
|
||||
import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants";
|
||||
// types
|
||||
import { TDropdownProps } from "../types";
|
||||
// constants
|
||||
import { ModuleOptions } from "./module-options";
|
||||
@ -178,32 +177,19 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const { getModuleNameById } = useModule();
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose && onClose();
|
||||
};
|
||||
const { handleClose, handleKeyDown, handleOnClick } = useDropdown({
|
||||
dropdownRef,
|
||||
inputRef,
|
||||
isOpen,
|
||||
onClose,
|
||||
setIsOpen,
|
||||
});
|
||||
|
||||
const dropdownOnChange = (val: string & string[]) => {
|
||||
onChange(val);
|
||||
if (!multiple) handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
const comboboxProps: any = {
|
||||
value,
|
||||
onChange: dropdownOnChange,
|
||||
|
@ -1,22 +1,23 @@
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { TIssuePriorities } from "@plane/types";
|
||||
// hooks
|
||||
import { PriorityIcon, Tooltip } from "@plane/ui";
|
||||
import { ISSUE_PRIORITIES } from "@/constants/issue";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// icons
|
||||
// helpers
|
||||
// types
|
||||
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
|
||||
import { TDropdownProps } from "./types";
|
||||
import { TIssuePriorities } from "@plane/types";
|
||||
// ui
|
||||
import { PriorityIcon, Tooltip } from "@plane/ui";
|
||||
// constants
|
||||
import { ISSUE_PRIORITIES } from "@/constants/issue";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// constants
|
||||
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
|
||||
// types
|
||||
import { TDropdownProps } from "./types";
|
||||
|
||||
type Props = TDropdownProps & {
|
||||
button?: ReactNode;
|
||||
@ -328,38 +329,20 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
const filteredOptions =
|
||||
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose && onClose();
|
||||
};
|
||||
|
||||
const dropdownOnChange = (val: TIssuePriorities) => {
|
||||
onChange(val);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (query !== "" && e.key === "Escape") {
|
||||
e.stopPropagation();
|
||||
setQuery("");
|
||||
}
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
|
||||
dropdownRef,
|
||||
inputRef,
|
||||
isOpen,
|
||||
onClose,
|
||||
query,
|
||||
setIsOpen,
|
||||
setQuery,
|
||||
});
|
||||
|
||||
const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant)
|
||||
? BorderButton
|
||||
@ -367,12 +350,6 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
? BackgroundButton
|
||||
: TransparentButton;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
|
@ -1,22 +1,23 @@
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// types
|
||||
import { IProject } from "@plane/types";
|
||||
// hooks
|
||||
// components
|
||||
import { ProjectLogo } from "@/components/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
// components
|
||||
import { DropdownButton } from "./buttons";
|
||||
// helpers
|
||||
// types
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||
import { TDropdownProps } from "./types";
|
||||
// constants
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||
// types
|
||||
import { TDropdownProps } from "./types";
|
||||
|
||||
type Props = TDropdownProps & {
|
||||
button?: ReactNode;
|
||||
@ -96,37 +97,21 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedProject = value ? getProjectById(value) : null;
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
};
|
||||
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
|
||||
dropdownRef,
|
||||
inputRef,
|
||||
isOpen,
|
||||
onClose,
|
||||
query,
|
||||
setIsOpen,
|
||||
setQuery,
|
||||
});
|
||||
|
||||
const dropdownOnChange = (val: string) => {
|
||||
onChange(val);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@ -203,6 +188,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
onKeyDown={searchInputKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||
|
@ -3,20 +3,19 @@ import { observer } from "mobx-react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// hooks
|
||||
// ui
|
||||
import { Spinner, StateGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppRouter, useProjectState } from "@/hooks/store";
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
// components
|
||||
import { DropdownButton } from "./buttons";
|
||||
// icons
|
||||
// helpers
|
||||
// types
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||
import { TDropdownProps } from "./types";
|
||||
// constants
|
||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||
// types
|
||||
import { TDropdownProps } from "./types";
|
||||
|
||||
type Props = TDropdownProps & {
|
||||
button?: ReactNode;
|
||||
@ -99,51 +98,28 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
setStateLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
|
||||
dropdownRef,
|
||||
inputRef,
|
||||
isOpen,
|
||||
onClose,
|
||||
onOpen,
|
||||
query,
|
||||
setIsOpen,
|
||||
setQuery,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) onOpen();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [projectId]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose && onClose();
|
||||
};
|
||||
|
||||
const dropdownOnChange = (val: string) => {
|
||||
onChange(val);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (query !== "" && e.key === "Escape") {
|
||||
e.stopPropagation();
|
||||
setQuery("");
|
||||
}
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -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 && (
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
|
||||
key={`${issueId}`}
|
||||
defaultHeight="3rem"
|
||||
root={containerRef}
|
||||
classNames={`relative ${isLastChild ? "" : "border-b border-b-custom-border-200"}`}
|
||||
classNames={`relative ${isLastChild && !isExpanded ? "" : "border-b border-b-custom-border-200"}`}
|
||||
>
|
||||
<IssueBlock
|
||||
issueId={issueId}
|
||||
|
@ -189,7 +189,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||
</div>
|
||||
</div>
|
||||
{displayProperties && displayProperties?.key && (
|
||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||
<div className="pl-1 flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||
{projectIdentifier}-{issue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
|
@ -179,7 +179,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;
|
||||
|
||||
@ -189,7 +189,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-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1">
|
||||
@ -211,7 +212,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}
|
||||
@ -228,7 +229,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
canEditProperties={canEditProperties}
|
||||
containerRef={containerRef}
|
||||
isDragAllowed={isDragAllowed}
|
||||
canDropOverIssue={canDropOverIssue}
|
||||
canDropOverIssue={!canOverlayBeVisible}
|
||||
selectionHelpers={selectionHelpers}
|
||||
/>
|
||||
)}
|
||||
|
@ -152,11 +152,11 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center gap-2 p-3 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>
|
||||
|
@ -72,6 +72,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
setQuery("");
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -57,6 +57,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
const handleClose = () => {
|
||||
if (isDropdownOpen) setIsDropdownOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
setQuery("");
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -399,6 +399,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={parentIssueId}
|
||||
rootIssueId={parentIssueId}
|
||||
spacingLeft={10}
|
||||
disabled={!disabled}
|
||||
handleIssueCrudState={handleIssueCrudState}
|
||||
|
@ -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))
|
||||
|
@ -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) => {
|
||||
|
76
web/hooks/use-dropdown.ts
Normal file
76
web/hooks/use-dropdown.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { useEffect } from "react";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
|
||||
type TArguments = {
|
||||
dropdownRef: React.RefObject<HTMLDivElement>;
|
||||
inputRef?: React.RefObject<HTMLInputElement | null>;
|
||||
isOpen: boolean;
|
||||
onClose?: () => void;
|
||||
onOpen?: () => Promise<void> | void;
|
||||
query?: string;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setQuery?: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
export const useDropdown = (args: TArguments) => {
|
||||
const { dropdownRef, inputRef, isOpen, onClose, onOpen, query, setIsOpen, setQuery } = args;
|
||||
|
||||
/**
|
||||
* @description clear the search input when the user presses the escape key, if the search input is not empty
|
||||
* @param {React.KeyboardEvent<HTMLInputElement>} e
|
||||
*/
|
||||
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (query !== "" && e.key === "Escape") {
|
||||
e.stopPropagation();
|
||||
setQuery?.("");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description close the dropdown, clear the search input, and call the onClose callback
|
||||
*/
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose?.();
|
||||
setQuery?.("");
|
||||
};
|
||||
|
||||
// toggle the dropdown, call the onOpen callback if the dropdown is closed, and call the onClose callback if the dropdown is open
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen?.();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose?.();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
/**
|
||||
* @description toggle the dropdown on click
|
||||
* @param {React.MouseEvent<HTMLButtonElement, MouseEvent>} e
|
||||
*/
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
// close the dropdown when the user clicks outside of the dropdown
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
// focus the search input when the dropdown is open
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef?.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef, isOpen]);
|
||||
|
||||
return {
|
||||
handleClose,
|
||||
handleKeyDown,
|
||||
handleOnClick,
|
||||
searchInputKeyDown,
|
||||
};
|
||||
};
|
@ -93,7 +93,7 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
|
||||
|
||||
if (pageType === EPageTypes.ONBOARDING) {
|
||||
if (!currentUser?.id) {
|
||||
router.push("/sign-in");
|
||||
router.push("/");
|
||||
return <></>;
|
||||
} else {
|
||||
if (currentUser && currentUserProfile?.id && isUserOnboard) {
|
||||
@ -106,7 +106,7 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
|
||||
|
||||
if (pageType === EPageTypes.SET_PASSWORD) {
|
||||
if (!currentUser?.id) {
|
||||
router.push("/sign-in");
|
||||
router.push("/");
|
||||
return <></>;
|
||||
} else {
|
||||
if (currentUser && !currentUser?.is_password_autoset && currentUserProfile?.id && isUserOnboard) {
|
||||
@ -125,7 +125,7 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
|
||||
return <></>;
|
||||
}
|
||||
} else {
|
||||
router.push("/sign-in");
|
||||
router.push("/");
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,21 @@ const nextConfig = {
|
||||
return [
|
||||
{
|
||||
source: "/accounts/sign-up",
|
||||
destination: "/sign-up",
|
||||
permanent: true
|
||||
},
|
||||
{
|
||||
source: "/sign-in",
|
||||
destination: "/",
|
||||
permanent: true
|
||||
},
|
||||
{
|
||||
source: "/register",
|
||||
destination: "/sign-up",
|
||||
permanent: true
|
||||
},
|
||||
{
|
||||
source: "/login",
|
||||
destination: "/",
|
||||
permanent: true
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
|
||||
>
|
||||
{resendTimerCode > 0 ? `Resend in ${resendTimerCode} seconds` : "Send reset link"}
|
||||
</Button>
|
||||
<Link href="/sign-in" className={cn("w-full", getButtonStyling("link-neutral", "lg"))}>
|
||||
<Link href="/" className={cn("w-full", getButtonStyling("link-neutral", "lg"))}>
|
||||
Back to sign in
|
||||
</Link>
|
||||
</form>
|
||||
|
@ -8,7 +8,7 @@ import { useTheme } from "next-themes";
|
||||
import { AuthRoot } from "@/components/account";
|
||||
import { PageHead } from "@/components/core";
|
||||
// constants
|
||||
import { NAVIGATE_TO_SIGNIN } from "@/constants/event-tracker";
|
||||
import { NAVIGATE_TO_SIGNUP } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
@ -31,7 +31,7 @@ const HomePage: NextPageWithLayout = observer(() => {
|
||||
|
||||
return (
|
||||
<div className="relative w-screen h-screen overflow-hidden">
|
||||
<PageHead title="Sign Up" />
|
||||
<PageHead title="Log in or Sign up to continue" />
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||
@ -46,18 +46,18 @@ const HomePage: NextPageWithLayout = observer(() => {
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
|
||||
Already have an account?{" "}
|
||||
New to Plane?{" "}
|
||||
<Link
|
||||
href="/sign-in"
|
||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
|
||||
href="/sign-up"
|
||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
|
||||
className="font-semibold text-custom-primary-100 hover:underline"
|
||||
>
|
||||
Sign In
|
||||
Create an account
|
||||
</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">
|
||||
<AuthRoot authMode={EAuthModes.SIGN_UP} />
|
||||
<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 authMode={EAuthModes.SIGN_IN} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@ import { useTheme } from "next-themes";
|
||||
import { AuthRoot } from "@/components/account";
|
||||
import { PageHead } from "@/components/core";
|
||||
// constants
|
||||
import { NAVIGATE_TO_SIGNUP } from "@/constants/event-tracker";
|
||||
import { NAVIGATE_TO_SIGNIN } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
@ -33,7 +33,7 @@ const SignInPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
return (
|
||||
<div className="relative w-screen h-screen overflow-hidden">
|
||||
<PageHead title="Sign In" />
|
||||
<PageHead title="Sign up or Log in to continue" />
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||
@ -48,18 +48,18 @@ const SignInPage: NextPageWithLayout = observer(() => {
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
|
||||
New to Plane?{" "}
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
|
||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
|
||||
className="font-semibold text-custom-primary-100 hover:underline"
|
||||
>
|
||||
Create an account
|
||||
Log in
|
||||
</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">
|
||||
<AuthRoot authMode={EAuthModes.SIGN_IN} />
|
||||
<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 authMode={EAuthModes.SIGN_UP} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -18,15 +18,8 @@ export class AuthService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
signUpEmailCheck = async (data: IEmailCheckData): Promise<IEmailCheckResponse> =>
|
||||
this.post("/auth/sign-up/email-check/", data, { headers: {} })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
||||
signInEmailCheck = async (data: IEmailCheckData): Promise<IEmailCheckResponse> =>
|
||||
this.post("/auth/sign-in/email-check/", data, { headers: {} })
|
||||
emailCheck = async (data: IEmailCheckData): Promise<IEmailCheckResponse> =>
|
||||
this.post("/auth/email-check/", data, { headers: {} })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
@ -97,7 +97,7 @@ export class ModuleService extends APIService {
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: { modules: string[] }
|
||||
data: { modules: string[]; removed_modules?: string[] }
|
||||
): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/modules/`, data)
|
||||
.then((response) => response?.data)
|
||||
|
@ -431,15 +431,11 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
||||
this.rootStore.issues.updateIssue(issueId, { module_ids: uniq(currentModuleIds) });
|
||||
}
|
||||
|
||||
//Perform API calls
|
||||
if (!isEmpty(addModuleIds)) {
|
||||
await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, {
|
||||
modules: addModuleIds,
|
||||
});
|
||||
}
|
||||
if (!isEmpty(removeModuleIds)) {
|
||||
await this.moduleService.removeModulesFromIssueBulk(workspaceSlug, projectId, issueId, removeModuleIds);
|
||||
}
|
||||
//Perform API call
|
||||
await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, {
|
||||
modules: addModuleIds,
|
||||
removed_modules: removeModuleIds,
|
||||
});
|
||||
} catch (error) {
|
||||
// revert the issue back to its original module ids
|
||||
set(this.rootStore.issues.issuesMap, [issueId, "module_ids"], originalModuleIds);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user