fix: merge conflicts resolved from preview

This commit is contained in:
Aaryan Khandelwal 2024-05-22 18:29:53 +05:30
commit 9c1e32a323
102 changed files with 6485 additions and 1814 deletions

59
.eslintrc-staged.js Normal file
View File

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

View File

@ -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
View File

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

0
.husky/pre-commit Normal file
View File

3
.lintstagedrc.json Normal file
View File

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

View File

@ -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

View File

@ -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>

View File

@ -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" ]

View File

@ -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" ]

View File

View File

@ -0,0 +1,6 @@
#!/bin/bash
set -e
python manage.py wait_for_db $1
python manage.py migrate $1

View File

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

View File

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

View File

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

View File

@ -1,3 +1,6 @@
# Python imports
import os
# Django imports
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
@ -16,8 +19,12 @@ from plane.authentication.adapter.error import (
AUTHENTICATION_ERROR_CODES,
)
from plane.authentication.rate_limit import AuthenticationThrottle
from plane.license.utils.instance_value import (
get_configuration_value,
)
class EmailCheckSignUpEndpoint(APIView):
class EmailCheckEndpoint(APIView):
permission_classes = [
AllowAny,
@ -28,128 +35,99 @@ class EmailCheckSignUpEndpoint(APIView):
]
def post(self, request):
try:
# Check instance configuration
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INSTANCE_NOT_CONFIGURED"
],
error_message="INSTANCE_NOT_CONFIGURED",
)
email = request.data.get("email", False)
# Return error if email is not present
if not email:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
error_message="EMAIL_REQUIRED",
)
# Validate email
validate_email(email)
existing_user = User.objects.filter(email=email).first()
if existing_user:
# check if the account is the deactivated
if not existing_user.is_active:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"USER_ACCOUNT_DEACTIVATED"
],
error_message="USER_ACCOUNT_DEACTIVATED",
)
# Raise user already exist
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"USER_ALREADY_EXIST"
],
error_message="USER_ALREADY_EXIST",
)
# Check instance configuration
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INSTANCE_NOT_CONFIGURED"
],
error_message="INSTANCE_NOT_CONFIGURED",
)
return Response(
{"status": True},
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
(EMAIL_HOST, ENABLE_MAGIC_LINK_LOGIN) = get_configuration_value(
[
{
"key": "EMAIL_HOST",
"default": os.environ.get("EMAIL_HOST", ""),
},
{
"key": "ENABLE_MAGIC_LINK_LOGIN",
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
},
]
)
smtp_configured = bool(EMAIL_HOST)
is_magic_login_enabled = ENABLE_MAGIC_LINK_LOGIN == "1"
email = request.data.get("email", False)
# Return error if email is not present
if not email:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
error_message="EMAIL_REQUIRED",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
# Validate email
try:
validate_email(email)
except ValidationError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
# Check if a user already exists with the given email
existing_user = User.objects.filter(email=email).first()
# If existing user
if existing_user:
if not existing_user.is_active:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"USER_ACCOUNT_DEACTIVATED"
],
error_message="USER_ACCOUNT_DEACTIVATED",
)
return Response(
exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
)
return Response(
{
"existing": True,
"status": (
"MAGIC_CODE"
if existing_user.is_password_autoset
and smtp_configured
and is_magic_login_enabled
else "CREDENTIAL"
),
},
status=status.HTTP_200_OK,
)
except ValidationError:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
except AuthenticationException as e:
return Response(
e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
)
class EmailCheckSignInEndpoint(APIView):
permission_classes = [
AllowAny,
]
throttle_classes = [
AuthenticationThrottle,
]
def post(self, request):
try:
# Check instance configuration
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INSTANCE_NOT_CONFIGURED"
],
error_message="INSTANCE_NOT_CONFIGURED",
)
email = request.data.get("email", False)
# Return error if email is not present
if not email:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
error_message="EMAIL_REQUIRED",
)
# Validate email
validate_email(email)
existing_user = User.objects.filter(email=email).first()
# If existing user
if existing_user:
# Raise different exception when user is not active
if not existing_user.is_active:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"USER_ACCOUNT_DEACTIVATED"
],
error_message="USER_ACCOUNT_DEACTIVATED",
)
# Return true
return Response(
{
"status": True,
"is_password_autoset": existing_user.is_password_autoset,
},
status=status.HTTP_200_OK,
)
# Raise error
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
error_message="USER_DOES_NOT_EXIST",
)
except ValidationError:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
except AuthenticationException as e:
return Response(
e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
)
# Else return response
return Response(
{
"existing": False,
"status": (
"MAGIC_CODE"
if smtp_configured and is_magic_login_enabled
else "CREDENTIAL"
),
},
status=status.HTTP_200_OK,
)

View File

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

View File

@ -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
```

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

@ -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 />}
</>
);
});

View File

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

View File

@ -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"

View File

@ -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

View File

@ -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]" />

View File

@ -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>

View File

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

View File

@ -1,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>

View File

@ -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>

View File

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

View File

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

View File

@ -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";

View 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;

View 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;

View File

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

View File

@ -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}
</>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -37,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);

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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 ? (

View File

@ -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"

View File

@ -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 });

View File

@ -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 });

View File

@ -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"

View File

@ -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"

View File

@ -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,

View File

@ -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"

View File

@ -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">

View File

@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}

View File

@ -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>
)}

View File

@ -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}
/>
)}

View File

@ -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>

View File

@ -72,6 +72,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
setQuery("");
onClose && onClose();
};

View File

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

View File

@ -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 = () => {

View File

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

View File

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

View File

@ -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}

View File

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

View File

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

76
web/hooks/use-dropdown.ts Normal file
View 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,
};
};

View File

@ -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 <></>;
}
}

View File

@ -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
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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)

View File

@ -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