mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into develop
This commit is contained in:
commit
3c80b87365
@ -14,6 +14,11 @@ PGHOST="plane-db"
|
|||||||
PGDATABASE="plane"
|
PGDATABASE="plane"
|
||||||
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||||
|
|
||||||
|
# Oauth variables
|
||||||
|
GOOGLE_CLIENT_ID=""
|
||||||
|
GITHUB_CLIENT_ID=""
|
||||||
|
GITHUB_CLIENT_SECRET=""
|
||||||
|
|
||||||
# Redis Settings
|
# Redis Settings
|
||||||
REDIS_HOST="plane-redis"
|
REDIS_HOST="plane-redis"
|
||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
@ -50,7 +55,6 @@ NGINX_PORT=80
|
|||||||
# SignUps
|
# SignUps
|
||||||
ENABLE_SIGNUP="1"
|
ENABLE_SIGNUP="1"
|
||||||
|
|
||||||
|
|
||||||
# Enable Email/Password Signup
|
# Enable Email/Password Signup
|
||||||
ENABLE_EMAIL_PASSWORD="1"
|
ENABLE_EMAIL_PASSWORD="1"
|
||||||
|
|
||||||
|
@ -21,7 +21,8 @@ from plane.db.models import (
|
|||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from .cycle import CycleSerializer, CycleLiteSerializer
|
from .cycle import CycleSerializer, CycleLiteSerializer
|
||||||
from .module import ModuleSerializer, ModuleLiteSerializer
|
from .module import ModuleSerializer, ModuleLiteSerializer
|
||||||
|
from .user import UserLiteSerializer
|
||||||
|
from .state import StateLiteSerializer
|
||||||
|
|
||||||
class IssueSerializer(BaseSerializer):
|
class IssueSerializer(BaseSerializer):
|
||||||
assignees = serializers.ListField(
|
assignees = serializers.ListField(
|
||||||
@ -331,12 +332,23 @@ class ModuleIssueSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class IssueExpandSerializer(BaseSerializer):
|
class LabelLiteSerializer(BaseSerializer):
|
||||||
# Serialize the related cycle. It's a OneToOne relation.
|
|
||||||
cycle = CycleLiteSerializer(source="issue_cycle.cycle", read_only=True)
|
|
||||||
|
|
||||||
# Serialize the related module. It's a OneToOne relation.
|
class Meta:
|
||||||
|
model = Label
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"color",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class IssueExpandSerializer(BaseSerializer):
|
||||||
|
cycle = CycleLiteSerializer(source="issue_cycle.cycle", read_only=True)
|
||||||
module = ModuleLiteSerializer(source="issue_module.module", read_only=True)
|
module = ModuleLiteSerializer(source="issue_module.module", read_only=True)
|
||||||
|
labels = LabelLiteSerializer(read_only=True, many=True)
|
||||||
|
assignees = UserLiteSerializer(read_only=True, many=True)
|
||||||
|
state = StateLiteSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
|
@ -11,10 +11,6 @@ class UserLiteSerializer(BaseSerializer):
|
|||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
"avatar",
|
"avatar",
|
||||||
"is_bot",
|
|
||||||
"display_name",
|
"display_name",
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = fields
|
||||||
"id",
|
|
||||||
"is_bot",
|
|
||||||
]
|
|
@ -169,8 +169,8 @@ class ChangePasswordSerializer(serializers.Serializer):
|
|||||||
Serializer for password change endpoint.
|
Serializer for password change endpoint.
|
||||||
"""
|
"""
|
||||||
old_password = serializers.CharField(required=True)
|
old_password = serializers.CharField(required=True)
|
||||||
new_password = serializers.CharField(required=True)
|
new_password = serializers.CharField(required=True, min_length=8)
|
||||||
confirm_password = serializers.CharField(required=True)
|
confirm_password = serializers.CharField(required=True, min_length=8)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if data.get("old_password") == data.get("new_password"):
|
if data.get("old_password") == data.get("new_password"):
|
||||||
@ -187,9 +187,7 @@ class ChangePasswordSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
|
|
||||||
class ResetPasswordSerializer(serializers.Serializer):
|
class ResetPasswordSerializer(serializers.Serializer):
|
||||||
model = User
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Serializer for password change endpoint.
|
Serializer for password change endpoint.
|
||||||
"""
|
"""
|
||||||
new_password = serializers.CharField(required=True)
|
new_password = serializers.CharField(required=True, min_length=8)
|
||||||
|
@ -105,17 +105,21 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
|||||||
def post(self, request):
|
def post(self, request):
|
||||||
email = request.data.get("email")
|
email = request.data.get("email")
|
||||||
|
|
||||||
if User.objects.filter(email=email).exists():
|
try:
|
||||||
user = User.objects.get(email=email)
|
validate_email(email)
|
||||||
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
|
except ValidationError:
|
||||||
token = PasswordResetTokenGenerator().make_token(user)
|
return Response({"error": "Please enter a valid email"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Get the user
|
||||||
|
user = User.objects.filter(email=email).first()
|
||||||
|
if user:
|
||||||
|
# Get the reset token for user
|
||||||
|
uidb64, token = get_tokens_for_user(user=user)
|
||||||
current_site = request.META.get("HTTP_ORIGIN")
|
current_site = request.META.get("HTTP_ORIGIN")
|
||||||
|
# send the forgot password email
|
||||||
forgot_password.delay(
|
forgot_password.delay(
|
||||||
user.first_name, user.email, uidb64, token, current_site
|
user.first_name, user.email, uidb64, token, current_site
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Check your email to reset your password"},
|
{"message": "Check your email to reset your password"},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
@ -130,14 +134,18 @@ class ResetPasswordEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def post(self, request, uidb64, token):
|
def post(self, request, uidb64, token):
|
||||||
try:
|
try:
|
||||||
|
# Decode the id from the uidb64
|
||||||
id = smart_str(urlsafe_base64_decode(uidb64))
|
id = smart_str(urlsafe_base64_decode(uidb64))
|
||||||
user = User.objects.get(id=id)
|
user = User.objects.get(id=id)
|
||||||
|
|
||||||
|
# check if the token is valid for the user
|
||||||
if not PasswordResetTokenGenerator().check_token(user, token):
|
if not PasswordResetTokenGenerator().check_token(user, token):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Token is invalid"},
|
{"error": "Token is invalid"},
|
||||||
status=status.HTTP_401_UNAUTHORIZED,
|
status=status.HTTP_401_UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Reset the password
|
||||||
serializer = ResetPasswordSerializer(data=request.data)
|
serializer = ResetPasswordSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
# set_password also hashes the password that the user will get
|
# set_password also hashes the password that the user will get
|
||||||
@ -145,9 +153,9 @@ class ResetPasswordEndpoint(BaseAPIView):
|
|||||||
user.is_password_autoset = False
|
user.is_password_autoset = False
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
# Log the user in
|
||||||
# Generate access token for the user
|
# Generate access token for the user
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
@ -166,7 +174,6 @@ class ResetPasswordEndpoint(BaseAPIView):
|
|||||||
class ChangePasswordEndpoint(BaseAPIView):
|
class ChangePasswordEndpoint(BaseAPIView):
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = ChangePasswordSerializer(data=request.data)
|
serializer = ChangePasswordSerializer(data=request.data)
|
||||||
|
|
||||||
user = User.objects.get(pk=request.user.id)
|
user = User.objects.get(pk=request.user.id)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if not user.check_password(serializer.data.get("old_password")):
|
if not user.check_password(serializer.data.get("old_password")):
|
||||||
@ -218,16 +225,15 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
# get the email
|
|
||||||
|
|
||||||
# Check the instance registration
|
# Check the instance registration
|
||||||
instance = Instance.objects.first()
|
instance = Instance.objects.first()
|
||||||
if instance is None:
|
if instance is None or not instance.is_setup_done:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Instance is not configured"},
|
{"error": "Instance is not configured"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get the configurations
|
||||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||||
|
|
||||||
email = request.data.get("email", False)
|
email = request.data.get("email", False)
|
||||||
@ -267,7 +273,7 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create the user with default values
|
||||||
user = User.objects.create(
|
user = User.objects.create(
|
||||||
email=email,
|
email=email,
|
||||||
username=uuid.uuid4().hex,
|
username=uuid.uuid4().hex,
|
||||||
@ -325,7 +331,7 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
first_time=True,
|
first_time=True,
|
||||||
)
|
)
|
||||||
# Automatically send the email
|
# Automatically send the email
|
||||||
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
|
||||||
# Existing user
|
# Existing user
|
||||||
else:
|
else:
|
||||||
if type == "magic_code":
|
if type == "magic_code":
|
||||||
|
@ -10,14 +10,12 @@ from django.utils import timezone
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
from sentry_sdk import capture_message
|
from sentry_sdk import capture_message
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
@ -33,7 +31,6 @@ from plane.settings.redis import redis_instance
|
|||||||
from plane.license.models import InstanceConfiguration, Instance
|
from plane.license.models import InstanceConfiguration, Instance
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
from plane.bgtasks.event_tracking_task import auth_events
|
from plane.bgtasks.event_tracking_task import auth_events
|
||||||
from plane.bgtasks.magic_link_code_task import magic_link
|
|
||||||
from plane.bgtasks.user_count_task import update_user_instance_user_count
|
from plane.bgtasks.user_count_task import update_user_instance_user_count
|
||||||
|
|
||||||
|
|
||||||
@ -49,6 +46,14 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
# Check if the instance configuration is done
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
if instance is None or not instance.is_setup_done:
|
||||||
|
return Response(
|
||||||
|
{"error": "Instance is not configured"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||||
|
|
||||||
email = request.data.get("email", False)
|
email = request.data.get("email", False)
|
||||||
@ -71,6 +76,7 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If the sign up is not enabled and the user does not have invite disallow him from creating the account
|
||||||
if (
|
if (
|
||||||
get_configuration_value(
|
get_configuration_value(
|
||||||
instance_configuration,
|
instance_configuration,
|
||||||
@ -124,6 +130,14 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
# Check if the instance configuration is done
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
if instance is None or not instance.is_setup_done:
|
||||||
|
return Response(
|
||||||
|
{"error": "Instance is not configured"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
email = request.data.get("email", False)
|
email = request.data.get("email", False)
|
||||||
password = request.data.get("password", False)
|
password = request.data.get("password", False)
|
||||||
|
|
||||||
@ -144,14 +158,6 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if the instance setup is done or not
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
if instance is None or not instance.is_setup_done:
|
|
||||||
return Response(
|
|
||||||
{"error": "Instance is not configured"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the user
|
# Get the user
|
||||||
user = User.objects.filter(email=email).first()
|
user = User.objects.filter(email=email).first()
|
||||||
|
|
||||||
@ -288,6 +294,7 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
# Check if the instance configuration is done
|
||||||
instance = Instance.objects.first()
|
instance = Instance.objects.first()
|
||||||
if instance is None or not instance.is_setup_done:
|
if instance is None or not instance.is_setup_done:
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -303,14 +303,6 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
instance_configuration = InstanceConfiguration.objects.values(
|
instance_configuration = InstanceConfiguration.objects.values(
|
||||||
"key", "value"
|
"key", "value"
|
||||||
)
|
)
|
||||||
# Check if instance is registered or not
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
if instance is None and not instance.is_setup_done:
|
|
||||||
return Response(
|
|
||||||
{"error": "Instance is not configured"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
get_configuration_value(
|
get_configuration_value(
|
||||||
instance_configuration,
|
instance_configuration,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
import os
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||||
@ -17,8 +18,8 @@ from sentry_sdk import capture_exception
|
|||||||
from plane.db.models import Issue
|
from plane.db.models import Issue
|
||||||
from plane.utils.analytics_plot import build_graph_plot
|
from plane.utils.analytics_plot import build_graph_plot
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.license.models import InstanceConfiguration
|
from plane.license.models import InstanceConfiguration, Instance
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
from plane.license.utils.instance_value import get_email_configuration
|
||||||
|
|
||||||
row_mapping = {
|
row_mapping = {
|
||||||
"state__name": "State",
|
"state__name": "State",
|
||||||
@ -43,7 +44,7 @@ CYCLE_ID = "issue_cycle__cycle_id"
|
|||||||
MODULE_ID = "issue_module__module_id"
|
MODULE_ID = "issue_module__module_id"
|
||||||
|
|
||||||
|
|
||||||
def send_export_email(email, slug, csv_buffer):
|
def send_export_email(email, slug, csv_buffer, rows):
|
||||||
"""Helper function to send export email."""
|
"""Helper function to send export email."""
|
||||||
subject = "Your Export is ready"
|
subject = "Your Export is ready"
|
||||||
html_content = render_to_string("emails/exports/analytics.html", {})
|
html_content = render_to_string("emails/exports/analytics.html", {})
|
||||||
@ -55,47 +56,58 @@ def send_export_email(email, slug, csv_buffer):
|
|||||||
instance_configuration = InstanceConfiguration.objects.filter(
|
instance_configuration = InstanceConfiguration.objects.filter(
|
||||||
key__startswith="EMAIL_"
|
key__startswith="EMAIL_"
|
||||||
).values("key", "value")
|
).values("key", "value")
|
||||||
|
|
||||||
|
(
|
||||||
|
EMAIL_HOST,
|
||||||
|
EMAIL_HOST_USER,
|
||||||
|
EMAIL_HOST_PASSWORD,
|
||||||
|
EMAIL_PORT,
|
||||||
|
EMAIL_USE_TLS,
|
||||||
|
EMAIL_FROM,
|
||||||
|
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||||
|
|
||||||
|
# Send the email if the users don't have smtp configured
|
||||||
|
if EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD:
|
||||||
|
# Check the instance registration
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-instance-id": instance.instance_id,
|
||||||
|
"x-api-key": instance.api_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"email": email,
|
||||||
|
"slug": slug,
|
||||||
|
"rows": rows,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = requests.post(
|
||||||
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/analytics/",
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
connection = get_connection(
|
connection = get_connection(
|
||||||
host=get_configuration_value(
|
host=EMAIL_HOST,
|
||||||
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST")
|
port=int(EMAIL_PORT),
|
||||||
),
|
username=EMAIL_HOST_USER,
|
||||||
port=int(
|
password=EMAIL_HOST_PASSWORD,
|
||||||
get_configuration_value(
|
use_tls=bool(EMAIL_USE_TLS),
|
||||||
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
username=get_configuration_value(
|
|
||||||
instance_configuration,
|
|
||||||
"EMAIL_HOST_USER",
|
|
||||||
os.environ.get("EMAIL_HOST_USER"),
|
|
||||||
),
|
|
||||||
password=get_configuration_value(
|
|
||||||
instance_configuration,
|
|
||||||
"EMAIL_HOST_PASSWORD",
|
|
||||||
os.environ.get("EMAIL_HOST_PASSWORD"),
|
|
||||||
),
|
|
||||||
use_tls=bool(
|
|
||||||
get_configuration_value(
|
|
||||||
instance_configuration,
|
|
||||||
"EMAIL_USE_TLS",
|
|
||||||
os.environ.get("EMAIL_USE_TLS", "1"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(
|
msg = EmailMultiAlternatives(
|
||||||
subject=subject,
|
subject=subject,
|
||||||
body=text_content,
|
body=text_content,
|
||||||
from_email=get_configuration_value(
|
from_email=EMAIL_FROM,
|
||||||
instance_configuration,
|
|
||||||
"EMAIL_FROM",
|
|
||||||
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
|
||||||
),
|
|
||||||
to=[email],
|
to=[email],
|
||||||
connection=connection,
|
connection=connection,
|
||||||
)
|
)
|
||||||
msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue())
|
msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue())
|
||||||
msg.send(fail_silently=False)
|
msg.send(fail_silently=False)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def get_assignee_details(slug, filters):
|
def get_assignee_details(slug, filters):
|
||||||
@ -463,8 +475,11 @@ def analytic_export_task(email, data, slug):
|
|||||||
)
|
)
|
||||||
|
|
||||||
csv_buffer = generate_csv_from_rows(rows)
|
csv_buffer = generate_csv_from_rows(rows)
|
||||||
send_export_email(email, slug, csv_buffer)
|
send_export_email(email, slug, csv_buffer, rows)
|
||||||
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
print(e)
|
print(e)
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
|
return
|
||||||
|
@ -40,13 +40,10 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
|||||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||||
|
|
||||||
# Send the email if the users don't have smtp configured
|
# Send the email if the users don't have smtp configured
|
||||||
if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD:
|
if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
|
||||||
# Check the instance registration
|
# Check the instance registration
|
||||||
instance = Instance.objects.first()
|
instance = Instance.objects.first()
|
||||||
|
|
||||||
# send the emails through control center
|
|
||||||
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
|
|
||||||
|
|
||||||
# headers
|
# headers
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -61,7 +58,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
f"{license_engine_base_url}/api/instances/users/forgot-password/",
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/forgot-password/",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
)
|
)
|
||||||
|
@ -21,7 +21,6 @@ from plane.license.utils.instance_value import get_email_configuration
|
|||||||
@shared_task
|
@shared_task
|
||||||
def magic_link(email, key, token, current_site):
|
def magic_link(email, key, token, current_site):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
instance_configuration = InstanceConfiguration.objects.filter(
|
instance_configuration = InstanceConfiguration.objects.filter(
|
||||||
key__startswith="EMAIL_"
|
key__startswith="EMAIL_"
|
||||||
).values("key", "value")
|
).values("key", "value")
|
||||||
@ -36,13 +35,10 @@ def magic_link(email, key, token, current_site):
|
|||||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||||
|
|
||||||
# Send the email if the users don't have smtp configured
|
# Send the email if the users don't have smtp configured
|
||||||
if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD:
|
if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
|
||||||
# Check the instance registration
|
# Check the instance registration
|
||||||
instance = Instance.objects.first()
|
instance = Instance.objects.first()
|
||||||
|
|
||||||
# send the emails through control center
|
|
||||||
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
|
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-instance-id": instance.instance_id,
|
"x-instance-id": instance.instance_id,
|
||||||
@ -55,7 +51,7 @@ def magic_link(email, key, token, current_site):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
f"{license_engine_base_url}/api/instances/users/magic-code/",
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/magic-code/",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
)
|
)
|
||||||
|
@ -32,13 +32,9 @@ def update_user_instance_user_count():
|
|||||||
"x-api-key": instance.api_key,
|
"x-api-key": instance.api_key,
|
||||||
}
|
}
|
||||||
|
|
||||||
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL")
|
|
||||||
if not license_engine_base_url:
|
|
||||||
raise Exception("License Engine base url is required")
|
|
||||||
|
|
||||||
# Update the license engine
|
# Update the license engine
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
f"{license_engine_base_url}/api/instances/",
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
)
|
)
|
||||||
|
@ -51,15 +51,10 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
|||||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||||
|
|
||||||
# Send the email if the users don't have smtp configured
|
# Send the email if the users don't have smtp configured
|
||||||
if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD:
|
if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
|
||||||
# Check the instance registration
|
# Check the instance registration
|
||||||
instance = Instance.objects.first()
|
instance = Instance.objects.first()
|
||||||
|
|
||||||
# send the emails through control center
|
|
||||||
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
|
|
||||||
if not license_engine_base_url:
|
|
||||||
raise Exception("License engine base url is required")
|
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-instance-id": instance.instance_id,
|
"x-instance-id": instance.instance_id,
|
||||||
@ -73,7 +68,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
|||||||
"email": email,
|
"email": email,
|
||||||
}
|
}
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
f"{license_engine_base_url}/api/instances/users/workspace-invitation/",
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/workspace-invitation/",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
)
|
)
|
||||||
|
@ -11,6 +11,7 @@ from django.utils import timezone
|
|||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -34,7 +35,6 @@ from plane.db.models import User
|
|||||||
from plane.license.utils.encryption import encrypt_data
|
from plane.license.utils.encryption import encrypt_data
|
||||||
from plane.settings.redis import redis_instance
|
from plane.settings.redis import redis_instance
|
||||||
from plane.bgtasks.magic_link_code_task import magic_link
|
from plane.bgtasks.magic_link_code_task import magic_link
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
|
||||||
|
|
||||||
|
|
||||||
class InstanceEndpoint(BaseAPIView):
|
class InstanceEndpoint(BaseAPIView):
|
||||||
@ -57,25 +57,17 @@ class InstanceEndpoint(BaseAPIView):
|
|||||||
# Load JSON content from the file
|
# Load JSON content from the file
|
||||||
data = json.load(file)
|
data = json.load(file)
|
||||||
|
|
||||||
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL")
|
|
||||||
|
|
||||||
if not license_engine_base_url:
|
|
||||||
raise Response(
|
|
||||||
{"error": "LICENSE_ENGINE_BASE_URL is required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"instance_key": os.environ.get("INSTANCE_KEY"),
|
"instance_key":settings.INSTANCE_KEY,
|
||||||
"version": data.get("version", 0.1),
|
"version": data.get("version", 0.1),
|
||||||
"machine_signature": os.environ.get("MACHINE_SIGNATURE"),
|
"machine_signature": os.environ.get("MACHINE_SIGNATURE"),
|
||||||
"user_count": User.objects.filter(is_bot=False).count(),
|
"user_count": User.objects.filter(is_bot=False).count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{license_engine_base_url}/api/instances/",
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
)
|
)
|
||||||
@ -130,6 +122,24 @@ class InstanceEndpoint(BaseAPIView):
|
|||||||
serializer = InstanceSerializer(instance, data=request.data, partial=True)
|
serializer = InstanceSerializer(instance, data=request.data, partial=True)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
# Save the user in control center
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-instance-id": instance.instance_id,
|
||||||
|
"x-api-key": instance.api_key,
|
||||||
|
}
|
||||||
|
# Update instance settings in the license engine
|
||||||
|
_ = requests.patch(
|
||||||
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||||
|
headers=headers,
|
||||||
|
data=json.dumps(
|
||||||
|
{
|
||||||
|
"is_support_required": serializer.data["is_support_required"],
|
||||||
|
"is_telemetry_enabled": serializer.data["is_telemetry_enabled"],
|
||||||
|
"version": serializer.data["version"],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -251,7 +261,6 @@ class AdminMagicSignInGenerateEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if not email:
|
if not email:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Please provide a valid email address"},
|
{"error": "Please provide a valid email address"},
|
||||||
@ -409,13 +418,6 @@ class AdminSetUserPasswordEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
|
|
||||||
if not license_engine_base_url:
|
|
||||||
return Response(
|
|
||||||
{"error": "License engine base url is required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save the user in control center
|
# Save the user in control center
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -423,14 +425,14 @@ class AdminSetUserPasswordEndpoint(BaseAPIView):
|
|||||||
"x-api-key": instance.api_key,
|
"x-api-key": instance.api_key,
|
||||||
}
|
}
|
||||||
_ = requests.patch(
|
_ = requests.patch(
|
||||||
f"{license_engine_base_url}/api/instances/",
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=json.dumps({"is_setup_done": True}),
|
data=json.dumps({"is_setup_done": True}),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Also register the user as admin
|
# Also register the user as admin
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
f"{license_engine_base_url}/api/instances/users/register/",
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/register/",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
{
|
{
|
||||||
@ -472,23 +474,19 @@ class SignUpScreenVisitedEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
|
if not instance.is_signup_screen_visited:
|
||||||
|
instance.is_signup_screen_visited = True
|
||||||
if not license_engine_base_url:
|
instance.save()
|
||||||
return Response(
|
# set the headers
|
||||||
{"error": "License engine base url is required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-instance-id": instance.instance_id,
|
"x-instance-id": instance.instance_id,
|
||||||
"x-api-key": instance.api_key,
|
"x-api-key": instance.api_key,
|
||||||
}
|
}
|
||||||
|
# create the payload
|
||||||
payload = {"is_signup_screen_visited": True}
|
payload = {"is_signup_screen_visited": True}
|
||||||
response = requests.patch(
|
_ = requests.patch(
|
||||||
f"{license_engine_base_url}/api/instances/",
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
)
|
)
|
||||||
|
@ -6,6 +6,7 @@ import requests
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.license.models import Instance
|
from plane.license.models import Instance
|
||||||
@ -30,31 +31,22 @@ class Command(BaseCommand):
|
|||||||
data = json.load(file)
|
data = json.load(file)
|
||||||
|
|
||||||
machine_signature = options.get("machine_signature", False)
|
machine_signature = options.get("machine_signature", False)
|
||||||
instance_key = os.environ.get("INSTANCE_KEY", False)
|
|
||||||
|
|
||||||
# Raise an exception if the admin email is not provided
|
|
||||||
if not instance_key:
|
|
||||||
raise CommandError("INSTANCE_KEY is required")
|
|
||||||
|
|
||||||
if not machine_signature:
|
if not machine_signature:
|
||||||
raise CommandError("Machine signature is required")
|
raise CommandError("Machine signature is required")
|
||||||
|
|
||||||
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL")
|
|
||||||
|
|
||||||
if not license_engine_base_url:
|
|
||||||
raise CommandError("LICENSE_ENGINE_BASE_URL is required")
|
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"instance_key": instance_key,
|
"instance_key": settings.INSTANCE_KEY,
|
||||||
"version": data.get("version", 0.1),
|
"version": data.get("version", 0.1),
|
||||||
"machine_signature": machine_signature,
|
"machine_signature": machine_signature,
|
||||||
"user_count": User.objects.filter(is_bot=False).count(),
|
"user_count": User.objects.filter(is_bot=False).count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{license_engine_base_url}/api/instances/",
|
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
)
|
)
|
||||||
|
@ -325,3 +325,9 @@ USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
|
|||||||
# Posthog settings
|
# Posthog settings
|
||||||
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
|
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
|
||||||
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
|
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
|
||||||
|
|
||||||
|
# License engine base url
|
||||||
|
LICENSE_ENGINE_BASE_URL = os.environ.get("LICENSE_ENGINE_BASE_URL", "https://control-center.plane.so")
|
||||||
|
|
||||||
|
# instance key
|
||||||
|
INSTANCE_KEY = os.environ.get("INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3")
|
||||||
|
@ -149,20 +149,20 @@
|
|||||||
padding-top: 10px !important;
|
padding-top: 10px !important;
|
||||||
}
|
}
|
||||||
.r13-o {
|
.r13-o {
|
||||||
border-bottom-color: #efefef !important;
|
border-bottom-color: #d9e4ff !important;
|
||||||
border-bottom-width: 1px !important;
|
border-bottom-width: 1px !important;
|
||||||
border-left-color: #efefef !important;
|
border-left-color: #d9e4ff !important;
|
||||||
border-left-width: 1px !important;
|
border-left-width: 1px !important;
|
||||||
border-right-color: #efefef !important;
|
border-right-color: #d9e4ff !important;
|
||||||
border-right-width: 1px !important;
|
border-right-width: 1px !important;
|
||||||
border-style: solid !important;
|
border-style: solid !important;
|
||||||
border-top-color: #efefef !important;
|
border-top-color: #d9e4ff !important;
|
||||||
border-top-width: 1px !important;
|
border-top-width: 1px !important;
|
||||||
margin: 0 auto 0 0 !important;
|
margin: 0 auto 0 0 !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
.r14-i {
|
.r14-i {
|
||||||
background-color: #e3e6f1 !important;
|
background-color: #ecf1ff !important;
|
||||||
padding-bottom: 10px !important;
|
padding-bottom: 10px !important;
|
||||||
padding-left: 10px !important;
|
padding-left: 10px !important;
|
||||||
padding-right: 10px !important;
|
padding-right: 10px !important;
|
||||||
@ -225,16 +225,11 @@
|
|||||||
padding-top: 5px !important;
|
padding-top: 5px !important;
|
||||||
}
|
}
|
||||||
.r24-o {
|
.r24-o {
|
||||||
border-style: solid !important;
|
|
||||||
margin-right: 8px !important;
|
|
||||||
width: 32px !important;
|
|
||||||
}
|
|
||||||
.r25-o {
|
|
||||||
border-style: solid !important;
|
border-style: solid !important;
|
||||||
margin-right: 0px !important;
|
margin-right: 0px !important;
|
||||||
width: 32px !important;
|
width: 32px !important;
|
||||||
}
|
}
|
||||||
.r26-i {
|
.r25-i {
|
||||||
padding-bottom: 0px !important;
|
padding-bottom: 0px !important;
|
||||||
padding-top: 5px !important;
|
padding-top: 5px !important;
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
@ -664,17 +659,17 @@
|
|||||||
width="100%"
|
width="100%"
|
||||||
class="r13-o"
|
class="r13-o"
|
||||||
style="
|
style="
|
||||||
background-color: #e3e6f1;
|
background-color: #ecf1ff;
|
||||||
border-bottom-color: #efefef;
|
border-bottom-color: #d9e4ff;
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-left-color: #efefef;
|
border-left-color: #d9e4ff;
|
||||||
border-left-width: 1px;
|
border-left-width: 1px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border-right-color: #efefef;
|
border-right-color: #d9e4ff;
|
||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-top-color: #efefef;
|
border-top-color: #d9e4ff;
|
||||||
border-top-width: 1px;
|
border-top-width: 1px;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -690,7 +685,7 @@
|
|||||||
font-family: georgia, serif;
|
font-family: georgia, serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
background-color: #e3e6f1;
|
background-color: #ecf1ff;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
line-height: 3;
|
line-height: 3;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
@ -714,10 +709,10 @@
|
|||||||
<p style="margin: 0">
|
<p style="margin: 0">
|
||||||
<span
|
<span
|
||||||
style="
|
style="
|
||||||
color: #716c6c;
|
color: #5f5e5e;
|
||||||
font-family: Arial,
|
font-family: Arial,
|
||||||
helvetica, sans-serif;
|
helvetica, sans-serif;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
"
|
"
|
||||||
>Please copy and paste this
|
>Please copy and paste this
|
||||||
on the screen where you
|
on the screen where you
|
||||||
@ -1251,13 +1246,14 @@
|
|||||||
role="presentation"
|
role="presentation"
|
||||||
width="100%"
|
width="100%"
|
||||||
class="r22-o"
|
class="r22-o"
|
||||||
class="r24-o"
|
|
||||||
style="
|
style="
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
class="r23-i"
|
class="r23-i"
|
||||||
class="r17-i"
|
|
||||||
style="
|
style="
|
||||||
font-size: 0px;
|
font-size: 0px;
|
||||||
line-height: 0px;
|
line-height: 0px;
|
||||||
@ -1309,7 +1305,7 @@
|
|||||||
border="0"
|
border="0"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
width="100%"
|
width="100%"
|
||||||
class="r25-o"
|
class="r24-o"
|
||||||
style="
|
style="
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -1441,7 +1437,7 @@
|
|||||||
<td
|
<td
|
||||||
align="center"
|
align="center"
|
||||||
valign="top"
|
valign="top"
|
||||||
class="r26-i nl2go-default-textstyle"
|
class="r25-i nl2go-default-textstyle"
|
||||||
style="
|
style="
|
||||||
color: #3b3f44;
|
color: #3b3f44;
|
||||||
font-family: georgia, serif;
|
font-family: georgia, serif;
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
||||||
<html>
|
|
||||||
Dear {{username}},<br/>
|
|
||||||
Your requested Issue's data has been successfully exported from Plane. The export includes all relevant information about issues you requested from your selected projects.</br>
|
|
||||||
Please find the attachment and download the CSV file. If you have any questions or need further assistance, please don't hesitate to contact our support team at <a href = "mailto: engineering@plane.com">engineering@plane.so</a>. We're here to help!</br>
|
|
||||||
Thank you for using Plane. We hope this export will aid you in effectively managing your projects.</br>
|
|
||||||
Regards,
|
|
||||||
Team Plane
|
|
||||||
</html>
|
|
@ -71,7 +71,6 @@ services:
|
|||||||
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
|
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
|
||||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||||
- WEB_URL=$SERVICE_FQDN_PLANE_8082
|
- WEB_URL=$SERVICE_FQDN_PLANE_8082
|
||||||
- LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
@ -117,7 +116,6 @@ services:
|
|||||||
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||||
- LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
@ -164,7 +162,6 @@ services:
|
|||||||
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||||
- LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
|
@ -5,15 +5,16 @@ x-app-env : &app-env
|
|||||||
- NGINX_PORT=${NGINX_PORT:-80}
|
- NGINX_PORT=${NGINX_PORT:-80}
|
||||||
- WEB_URL=${WEB_URL:-http://localhost}
|
- WEB_URL=${WEB_URL:-http://localhost}
|
||||||
- DEBUG=${DEBUG:-0}
|
- DEBUG=${DEBUG:-0}
|
||||||
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
|
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.production} # deprecated
|
||||||
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces}
|
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0} # deprecated
|
||||||
|
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces} # deprecated
|
||||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||||
|
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
|
||||||
|
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-""}
|
||||||
|
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-""}
|
||||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
||||||
- DOCKERIZED=${DOCKERIZED:-1} # deprecated
|
- DOCKERIZED=${DOCKERIZED:-1} # deprecated
|
||||||
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-""}
|
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-""}
|
||||||
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
|
|
||||||
- ADMIN_EMAIL=${ADMIN_EMAIL:-""}
|
|
||||||
- LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-""}
|
|
||||||
# Gunicorn Workers
|
# Gunicorn Workers
|
||||||
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}
|
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}
|
||||||
#DB SETTINGS
|
#DB SETTINGS
|
||||||
@ -28,12 +29,12 @@ x-app-env : &app-env
|
|||||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||||
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
|
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
|
||||||
# EMAIL SETTINGS
|
# EMAIL SETTINGS - Deprecated can be configured through admin panel
|
||||||
- EMAIL_HOST=${EMAIL_HOST:-""}
|
- EMAIL_HOST=${EMAIL_HOST:-""}
|
||||||
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
|
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
|
||||||
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
|
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
|
||||||
- EMAIL_PORT=${EMAIL_PORT:-587}
|
- EMAIL_PORT=${EMAIL_PORT:-587}
|
||||||
- EMAIL_FROM=${EMAIL_FROM:-"Team Plane <team@mailer.plane.so>"}
|
- EMAIL_FROM=${EMAIL_FROM:-"Team Plane <team@mailer.plane.so>"}
|
||||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
|
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
|
||||||
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
|
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
|
||||||
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
|
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
|
||||||
@ -42,10 +43,11 @@ x-app-env : &app-env
|
|||||||
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-"sk-"}
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-"sk-"}
|
||||||
- GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"}
|
- GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"}
|
||||||
# LOGIN/SIGNUP SETTINGS
|
# LOGIN/SIGNUP SETTINGS - Deprecated can be configured through admin panel
|
||||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||||
- ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1}
|
- ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1}
|
||||||
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
|
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
|
||||||
|
# Application secret
|
||||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||||
# DATA STORE SETTINGS
|
# DATA STORE SETTINGS
|
||||||
- USE_MINIO=${USE_MINIO:-1}
|
- USE_MINIO=${USE_MINIO:-1}
|
||||||
|
@ -10,10 +10,12 @@ DEBUG=0
|
|||||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||||
NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
||||||
SENTRY_DSN=""
|
SENTRY_DSN=""
|
||||||
|
SENTRY_ENVIRONMENT="production"
|
||||||
|
GOOGLE_CLIENT_ID=""
|
||||||
|
GITHUB_CLIENT_ID=""
|
||||||
GITHUB_CLIENT_SECRET=""
|
GITHUB_CLIENT_SECRET=""
|
||||||
DOCKERIZED=1 # deprecated
|
DOCKERIZED=1 # deprecated
|
||||||
CORS_ALLOWED_ORIGINS="http://localhost"
|
CORS_ALLOWED_ORIGINS="http://localhost"
|
||||||
SENTRY_ENVIRONMENT="production"
|
|
||||||
|
|
||||||
#DB SETTINGS
|
#DB SETTINGS
|
||||||
PGHOST=plane-db
|
PGHOST=plane-db
|
||||||
@ -34,7 +36,7 @@ EMAIL_HOST=""
|
|||||||
EMAIL_HOST_USER=""
|
EMAIL_HOST_USER=""
|
||||||
EMAIL_HOST_PASSWORD=""
|
EMAIL_HOST_PASSWORD=""
|
||||||
EMAIL_PORT=587
|
EMAIL_PORT=587
|
||||||
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
||||||
EMAIL_USE_TLS=1
|
EMAIL_USE_TLS=1
|
||||||
EMAIL_USE_SSL=0
|
EMAIL_USE_SSL=0
|
||||||
|
|
||||||
@ -63,9 +65,3 @@ FILE_SIZE_LIMIT=5242880
|
|||||||
|
|
||||||
# Gunicorn Workers
|
# Gunicorn Workers
|
||||||
GUNICORN_WORKERS=2
|
GUNICORN_WORKERS=2
|
||||||
|
|
||||||
# Admin Email
|
|
||||||
ADMIN_EMAIL=""
|
|
||||||
|
|
||||||
# License Engine url
|
|
||||||
LICENSE_ENGINE_BASE_URL=""
|
|
@ -84,7 +84,7 @@ export const EmailForm: React.FC<Props> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
<h1 className="text-center text-2xl sm:text-2.5xl font-medium text-onboarding-text-100">
|
||||||
Get on your flight deck!
|
Get on your flight deck
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-center text-sm text-onboarding-text-200 mt-3">
|
<p className="text-center text-sm text-onboarding-text-200 mt-3">
|
||||||
Sign in with the email you used to sign up for Plane
|
Sign in with the email you used to sign up for Plane
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { XCircle } from "lucide-react";
|
import { XCircle } from "lucide-react";
|
||||||
@ -36,6 +36,8 @@ const authService = new AuthService();
|
|||||||
|
|
||||||
export const PasswordForm: React.FC<Props> = (props) => {
|
export const PasswordForm: React.FC<Props> = (props) => {
|
||||||
const { email, updateEmail, handleStepChange, handleSignInRedirection } = props;
|
const { email, updateEmail, handleStepChange, handleSignInRedirection } = props;
|
||||||
|
// states
|
||||||
|
const [isSendingResetPasswordLink, setIsSendingResetPasswordLink] = useState(false);
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// form info
|
// form info
|
||||||
@ -113,6 +115,8 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsSendingResetPasswordLink(true);
|
||||||
|
|
||||||
authService
|
authService
|
||||||
.sendResetPasswordLink({ email: emailFormValue })
|
.sendResetPasswordLink({ email: emailFormValue })
|
||||||
.then(() => handleStepChange(ESignInSteps.SET_PASSWORD_LINK))
|
.then(() => handleStepChange(ESignInSteps.SET_PASSWORD_LINK))
|
||||||
@ -122,7 +126,8 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
|
.finally(() => setIsSendingResetPasswordLink(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -189,9 +194,12 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleForgotPassword}
|
onClick={handleForgotPassword}
|
||||||
className="text-xs font-medium text-custom-primary-100"
|
className={`text-xs font-medium ${
|
||||||
|
isSendingResetPasswordLink ? "text-onboarding-text-300" : "text-custom-primary-100"
|
||||||
|
}`}
|
||||||
|
disabled={isSendingResetPasswordLink}
|
||||||
>
|
>
|
||||||
Forgot your password?
|
{isSendingResetPasswordLink ? "Sending link..." : "Forgot your password?"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
// hooks
|
||||||
|
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
EmailForm,
|
EmailForm,
|
||||||
@ -19,33 +21,27 @@ export enum ESignInSteps {
|
|||||||
CREATE_PASSWORD = "CREATE_PASSWORD",
|
CREATE_PASSWORD = "CREATE_PASSWORD",
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
|
||||||
handleSignInRedirection: () => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const OAUTH_HIDDEN_STEPS = [ESignInSteps.OPTIONAL_SET_PASSWORD, ESignInSteps.CREATE_PASSWORD];
|
const OAUTH_HIDDEN_STEPS = [ESignInSteps.OPTIONAL_SET_PASSWORD, ESignInSteps.CREATE_PASSWORD];
|
||||||
|
|
||||||
export const SignInRoot: React.FC<Props> = (props) => {
|
export const SignInRoot = () => {
|
||||||
const { handleSignInRedirection } = props;
|
|
||||||
// states
|
// states
|
||||||
const [signInStep, setSignInStep] = useState<ESignInSteps>(ESignInSteps.EMAIL);
|
const [signInStep, setSignInStep] = useState<ESignInSteps>(ESignInSteps.EMAIL);
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
|
// sign in redirection hook
|
||||||
|
const { handleRedirection } = useSignInRedirection();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mx-auto flex flex-col">
|
<div className="mx-auto flex flex-col">
|
||||||
{signInStep === ESignInSteps.EMAIL && (
|
{signInStep === ESignInSteps.EMAIL && (
|
||||||
<EmailForm
|
<EmailForm handleStepChange={(step) => setSignInStep(step)} updateEmail={(newEmail) => setEmail(newEmail)} />
|
||||||
handleStepChange={(step: ESignInSteps) => setSignInStep(step)}
|
|
||||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{signInStep === ESignInSteps.PASSWORD && (
|
{signInStep === ESignInSteps.PASSWORD && (
|
||||||
<PasswordForm
|
<PasswordForm
|
||||||
email={email}
|
email={email}
|
||||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||||
handleStepChange={(step: ESignInSteps) => setSignInStep(step)}
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
handleSignInRedirection={handleSignInRedirection}
|
handleSignInRedirection={handleRedirection}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{signInStep === ESignInSteps.SET_PASSWORD_LINK && (
|
{signInStep === ESignInSteps.SET_PASSWORD_LINK && (
|
||||||
@ -55,30 +51,30 @@ export const SignInRoot: React.FC<Props> = (props) => {
|
|||||||
<UniqueCodeForm
|
<UniqueCodeForm
|
||||||
email={email}
|
email={email}
|
||||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||||
handleStepChange={(step: ESignInSteps) => setSignInStep(step)}
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
handleSignInRedirection={handleSignInRedirection}
|
handleSignInRedirection={handleRedirection}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{signInStep === ESignInSteps.OPTIONAL_SET_PASSWORD && (
|
{signInStep === ESignInSteps.OPTIONAL_SET_PASSWORD && (
|
||||||
<OptionalSetPasswordForm
|
<OptionalSetPasswordForm
|
||||||
email={email}
|
email={email}
|
||||||
handleStepChange={(step: ESignInSteps) => setSignInStep(step)}
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
handleSignInRedirection={handleSignInRedirection}
|
handleSignInRedirection={handleRedirection}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{signInStep === ESignInSteps.CREATE_PASSWORD && (
|
{signInStep === ESignInSteps.CREATE_PASSWORD && (
|
||||||
<CreatePasswordForm
|
<CreatePasswordForm
|
||||||
email={email}
|
email={email}
|
||||||
handleStepChange={(step: ESignInSteps) => setSignInStep(step)}
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
handleSignInRedirection={handleSignInRedirection}
|
handleSignInRedirection={handleRedirection}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!OAUTH_HIDDEN_STEPS.includes(signInStep) && (
|
{!OAUTH_HIDDEN_STEPS.includes(signInStep) && (
|
||||||
<OAuthOptions
|
<OAuthOptions
|
||||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||||
handleStepChange={(step: ESignInSteps) => setSignInStep(step)}
|
handleStepChange={(step) => setSignInStep(step)}
|
||||||
handleSignInRedirection={handleSignInRedirection}
|
handleSignInRedirection={handleRedirection}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -30,7 +30,7 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
|||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
formState: { errors, isValid },
|
formState: { errors, isValid },
|
||||||
watch,
|
handleSubmit,
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email,
|
email,
|
||||||
@ -39,11 +39,13 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSendNewLink = async () => {
|
const handleSendNewLink = async (formData: { email: string }) => {
|
||||||
setIsSendingNewLink(true);
|
setIsSendingNewLink(true);
|
||||||
|
|
||||||
|
updateEmail(formData.email);
|
||||||
|
|
||||||
const payload: IEmailCheckData = {
|
const payload: IEmailCheckData = {
|
||||||
email: watch("email"),
|
email: formData.email,
|
||||||
type: "password",
|
type: "password",
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -76,7 +78,7 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
|||||||
password
|
password
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form className="mt-5 sm:w-96 mx-auto space-y-4">
|
<form onSubmit={handleSubmit(handleSendNewLink)} className="mt-5 sm:w-96 mx-auto space-y-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@ -92,10 +94,7 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
|||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={onChange}
|
||||||
updateEmail(e.target.value);
|
|
||||||
onChange(e.target.value);
|
|
||||||
}}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.email)}
|
hasError={Boolean(errors.email)}
|
||||||
placeholder="orville.wright@firstflight.com"
|
placeholder="orville.wright@firstflight.com"
|
||||||
@ -112,11 +111,10 @@ export const SetPasswordLink: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
size="xl"
|
size="xl"
|
||||||
onClick={handleSendNewLink}
|
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
loading={isSendingNewLink}
|
loading={isSendingNewLink}
|
||||||
>
|
>
|
||||||
|
@ -11,6 +11,7 @@ import { ArchiveRestore } from "lucide-react";
|
|||||||
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||||
@ -28,6 +29,8 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
|||||||
const projectDetails = projectStore.currentProjectDetails;
|
const projectDetails = projectStore.currentProjectDetails;
|
||||||
const userRole = userStore.currentProjectRole;
|
const userRole = userStore.currentProjectRole;
|
||||||
|
|
||||||
|
const isAdmin = userRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SelectMonthModal
|
<SelectMonthModal
|
||||||
@ -56,7 +59,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
|||||||
projectDetails?.archive_in === 0 ? handleChange({ archive_in: 1 }) : handleChange({ archive_in: 0 })
|
projectDetails?.archive_in === 0 ? handleChange({ archive_in: 1 }) : handleChange({ archive_in: 0 })
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={userRole !== 20}
|
disabled={!isAdmin}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -74,7 +77,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
|||||||
}}
|
}}
|
||||||
input
|
input
|
||||||
width="w-full"
|
width="w-full"
|
||||||
disabled={userRole !== 20}
|
disabled={!isAdmin}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||||
|
@ -11,6 +11,7 @@ import { ArchiveX } from "lucide-react";
|
|||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||||
@ -53,6 +54,8 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||||||
default_state: defaultState,
|
default_state: defaultState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isAdmin = userRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SelectMonthModal
|
<SelectMonthModal
|
||||||
@ -83,7 +86,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||||||
: handleChange({ close_in: 0, default_state: null })
|
: handleChange({ close_in: 0, default_state: null })
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={userRole !== 20}
|
disabled={!isAdmin}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -102,7 +105,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||||||
}}
|
}}
|
||||||
input
|
input
|
||||||
width="w-full"
|
width="w-full"
|
||||||
disabled={userRole !== 20}
|
disabled={!isAdmin}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||||
|
@ -2,10 +2,12 @@ import React, { useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// services
|
// services
|
||||||
import { CycleService } from "services/cycle.service";
|
import { CycleService } from "services/cycle.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
//icons
|
//icons
|
||||||
import { ContrastIcon, TransferIcon } from "@plane/ui";
|
import { ContrastIcon, TransferIcon } from "@plane/ui";
|
||||||
import { AlertCircle, Search, X } from "lucide-react";
|
import { AlertCircle, Search, X } from "lucide-react";
|
||||||
@ -23,17 +25,19 @@ type Props = {
|
|||||||
|
|
||||||
const cycleService = new CycleService();
|
const cycleService = new CycleService();
|
||||||
|
|
||||||
export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) => {
|
export const TransferIssuesModal: React.FC<Props> = observer(({ isOpen, handleClose }) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const { cycleIssues: cycleIssueStore } = useMobxStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const transferIssue = async (payload: any) => {
|
const transferIssue = async (payload: any) => {
|
||||||
await cycleService
|
await cycleIssueStore
|
||||||
.transferIssues(workspaceSlug as string, projectId as string, cycleId as string, payload)
|
.transferIssuesFromCycle(workspaceSlug as string, projectId as string, cycleId as string, payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -159,4 +163,4 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -22,6 +22,7 @@ import { Button } from "@plane/ui";
|
|||||||
import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react";
|
import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import type { TInboxStatus } from "types";
|
import type { TInboxStatus } from "types";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
export const InboxActionsHeader = observer(() => {
|
export const InboxActionsHeader = observer(() => {
|
||||||
const [date, setDate] = useState(new Date());
|
const [date, setDate] = useState(new Date());
|
||||||
@ -71,7 +72,7 @@ export const InboxActionsHeader = observer(() => {
|
|||||||
}, [issue]);
|
}, [issue]);
|
||||||
|
|
||||||
const issueStatus = issue?.issue_inbox[0].status;
|
const issueStatus = issue?.issue_inbox[0].status;
|
||||||
const isAllowed = userRole === 15 || userRole === 20;
|
const isAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const tomorrow = new Date(today);
|
const tomorrow = new Date(today);
|
||||||
|
@ -16,6 +16,7 @@ import { Loader } from "@plane/ui";
|
|||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { IInboxIssue, IIssue } from "types";
|
import { IInboxIssue, IIssue } from "types";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
const defaultValues: Partial<IInboxIssue> = {
|
const defaultValues: Partial<IInboxIssue> = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -144,6 +145,8 @@ export const InboxMainContent: React.FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{issueDetails ? (
|
{issueDetails ? (
|
||||||
@ -222,7 +225,7 @@ export const InboxMainContent: React.FC = observer(() => {
|
|||||||
description_html: issueDetails.description_html,
|
description_html: issueDetails.description_html,
|
||||||
}}
|
}}
|
||||||
handleFormSubmit={submitChanges}
|
handleFormSubmit={submitChanges}
|
||||||
isAllowed={userRole === 15 || userRole === 20 || user?.id === issueDetails.created_by}
|
isAllowed={isAllowed || user?.id === issueDetails.created_by}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
IViewIssuesStore,
|
IViewIssuesStore,
|
||||||
} from "store/issues";
|
} from "store/issues";
|
||||||
import { TUnGroupedIssues } from "store/issues/types";
|
import { TUnGroupedIssues } from "store/issues/types";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
interface IBaseGanttRoot {
|
interface IBaseGanttRoot {
|
||||||
issueFiltersStore:
|
issueFiltersStore:
|
||||||
@ -69,7 +70,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAllowed = currentProjectRole && currentProjectRole >= 15;
|
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -31,6 +31,7 @@ import { KanBan } from "./default";
|
|||||||
import { KanBanSwimLanes } from "./swimlanes";
|
import { KanBanSwimLanes } from "./swimlanes";
|
||||||
import { EProjectStore } from "store/command-palette.store";
|
import { EProjectStore } from "store/command-palette.store";
|
||||||
import { IssuePeekOverview } from "components/issues";
|
import { IssuePeekOverview } from "components/issues";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
export interface IBaseKanBanLayout {
|
export interface IBaseKanBanLayout {
|
||||||
issueStore:
|
issueStore:
|
||||||
@ -93,7 +94,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const { currentProjectRole } = userStore;
|
const { currentProjectRole } = userStore;
|
||||||
const isEditingAllowed = [15, 20].includes(currentProjectRole || 0);
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const issues = issueStore?.getIssues || {};
|
const issues = issueStore?.getIssues || {};
|
||||||
const issueIds = issueStore?.getIssuesIds || [];
|
const issueIds = issueStore?.getIssuesIds || [];
|
||||||
|
@ -25,6 +25,7 @@ import { IIssueResponse } from "store/issues/types";
|
|||||||
import { EProjectStore } from "store/command-palette.store";
|
import { EProjectStore } from "store/command-palette.store";
|
||||||
import { IssuePeekOverview } from "components/issues";
|
import { IssuePeekOverview } from "components/issues";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
enum EIssueActions {
|
enum EIssueActions {
|
||||||
UPDATE = "update",
|
UPDATE = "update",
|
||||||
@ -83,7 +84,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const { currentProjectRole } = userStore;
|
const { currentProjectRole } = userStore;
|
||||||
const isEditingAllowed = [15, 20].includes(currentProjectRole || 0);
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const issueIds = issueStore?.getIssuesIds || [];
|
const issueIds = issueStore?.getIssuesIds || [];
|
||||||
const issues = issueStore?.getIssues;
|
const issues = issueStore?.getIssues;
|
||||||
|
@ -18,6 +18,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { EFilterType, TUnGroupedIssues } from "store/issues/types";
|
import { EFilterType, TUnGroupedIssues } from "store/issues/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
import { IQuickActionProps } from "../list/list-view-types";
|
import { IQuickActionProps } from "../list/list-view-types";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
interface IBaseSpreadsheetRoot {
|
interface IBaseSpreadsheetRoot {
|
||||||
issueFiltersStore:
|
issueFiltersStore:
|
||||||
@ -49,7 +50,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
|||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const { currentProjectRole } = userStore;
|
const { currentProjectRole } = userStore;
|
||||||
const isEditingAllowed = [15, 20].includes(currentProjectRole || 0);
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const issuesResponse = issueStore.getIssues;
|
const issuesResponse = issueStore.getIssues;
|
||||||
const issueIds = (issueStore.getIssuesIds ?? []) as TUnGroupedIssues;
|
const issueIds = (issueStore.getIssuesIds ?? []) as TUnGroupedIssues;
|
||||||
|
@ -14,6 +14,7 @@ import { IIssue } from "types";
|
|||||||
// services
|
// services
|
||||||
import { FileService } from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
// store
|
// store
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
const { currentProjectRole } = userStore;
|
const { currentProjectRole } = userStore;
|
||||||
const isAllowed = [15, 20].includes(currentProjectRole || 0);
|
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
const [characterLimit, setCharacterLimit] = useState(false);
|
const [characterLimit, setCharacterLimit] = useState(false);
|
||||||
|
@ -12,6 +12,7 @@ import { IssueView } from "./view";
|
|||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
interface IIssuePeekOverview {
|
interface IIssuePeekOverview {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -118,7 +119,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRole = userStore.currentProjectRole ?? 5;
|
const userRole = userStore.currentProjectRole ?? EUserWorkspaceRoles.GUEST;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -26,6 +26,7 @@ import { MinusCircle } from "lucide-react";
|
|||||||
import { IIssue, IIssueComment } from "types";
|
import { IIssue, IIssueComment } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUES_ACTIVITY, SUB_ISSUES } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_ACTIVITY, SUB_ISSUES } from "constants/fetch-keys";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueDetails: IIssue;
|
issueDetails: IIssue;
|
||||||
@ -100,6 +101,8 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg">
|
<div className="rounded-lg">
|
||||||
@ -166,7 +169,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug as string}
|
workspaceSlug={workspaceSlug as string}
|
||||||
issue={issueDetails}
|
issue={issueDetails}
|
||||||
handleFormSubmit={submitChanges}
|
handleFormSubmit={submitChanges}
|
||||||
isAllowed={userRole === 20 || userRole === 15 || !uneditable}
|
isAllowed={isAllowed || !uneditable}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IssueReaction workspaceSlug={workspaceSlug} issueId={issueId} projectId={projectId} />
|
<IssueReaction workspaceSlug={workspaceSlug} issueId={issueId} projectId={projectId} />
|
||||||
|
@ -40,6 +40,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
|
|||||||
import type { IIssue, IIssueLink, linkDetails } from "types";
|
import type { IIssue, IIssueLink, linkDetails } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: any;
|
control: any;
|
||||||
@ -245,7 +246,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
setLinkModal(true);
|
setLinkModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isNotAllowed = userRole === 5 || userRole === 10;
|
const isAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -295,7 +296,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<LinkIcon className="h-3.5 w-3.5" />
|
<LinkIcon className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!isNotAllowed && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && (
|
{isAllowed && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-500/20 focus:outline-none"
|
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-500/20 focus:outline-none"
|
||||||
@ -325,7 +326,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<SidebarStateSelect
|
<SidebarStateSelect
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(val: string) => submitChanges({ state: val })}
|
onChange={(val: string) => submitChanges({ state: val })}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -346,7 +347,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<SidebarAssigneeSelect
|
<SidebarAssigneeSelect
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(val: string[]) => submitChanges({ assignees: val })}
|
onChange={(val: string[]) => submitChanges({ assignees: val })}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -367,7 +368,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<SidebarPrioritySelect
|
<SidebarPrioritySelect
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(val) => submitChanges({ priority: val })}
|
onChange={(val) => submitChanges({ priority: val })}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -388,7 +389,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<SidebarEstimateSelect
|
<SidebarEstimateSelect
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(val: number | null) => submitChanges({ estimate_point: val })}
|
onChange={(val: number | null) => submitChanges({ estimate_point: val })}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -416,7 +417,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
onChange(val);
|
onChange(val);
|
||||||
}}
|
}}
|
||||||
issueDetails={issueDetail}
|
issueDetails={issueDetail}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -441,7 +442,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||||
}}
|
}}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
||||||
@ -462,7 +463,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||||
}}
|
}}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("duplicate")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("duplicate")) && (
|
||||||
@ -480,7 +481,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||||
}}
|
}}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("relates_to")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("relates_to")) && (
|
||||||
@ -498,7 +499,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||||
}}
|
}}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
|
||||||
@ -522,7 +523,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
className="bg-custom-background-80 border-none"
|
className="bg-custom-background-80 border-none"
|
||||||
maxDate={maxDate ?? undefined}
|
maxDate={maxDate ?? undefined}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -550,7 +551,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
className="bg-custom-background-80 border-none"
|
className="bg-custom-background-80 border-none"
|
||||||
minDate={minDate ?? undefined}
|
minDate={minDate ?? undefined}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -571,7 +572,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<SidebarCycleSelect
|
<SidebarCycleSelect
|
||||||
issueDetail={issueDetail}
|
issueDetail={issueDetail}
|
||||||
handleCycleChange={handleCycleChange}
|
handleCycleChange={handleCycleChange}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -586,7 +587,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<SidebarModuleSelect
|
<SidebarModuleSelect
|
||||||
issueDetail={issueDetail}
|
issueDetail={issueDetail}
|
||||||
handleModuleChange={handleModuleChange}
|
handleModuleChange={handleModuleChange}
|
||||||
disabled={isNotAllowed || uneditable}
|
disabled={!isAllowed || uneditable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -605,7 +606,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
issueDetails={issueDetail}
|
issueDetails={issueDetail}
|
||||||
labelList={issueDetail?.labels ?? []}
|
labelList={issueDetail?.labels ?? []}
|
||||||
submitChanges={submitChanges}
|
submitChanges={submitChanges}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={!isAllowed}
|
||||||
uneditable={uneditable ?? false}
|
uneditable={uneditable ?? false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -615,7 +616,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<div className={`min-h-[116px] py-1 text-xs ${uneditable ? "opacity-60" : ""}`}>
|
<div className={`min-h-[116px] py-1 text-xs ${uneditable ? "opacity-60" : ""}`}>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h4>Links</h4>
|
<h4>Links</h4>
|
||||||
{!isNotAllowed && (
|
{isAllowed && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
|
||||||
|
@ -22,6 +22,7 @@ import { IUser, IIssue, ISearchIssueResponse } from "types";
|
|||||||
import { IssueService } from "services/issue";
|
import { IssueService } from "services/issue";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { SUB_ISSUES } from "constants/fetch-keys";
|
import { SUB_ISSUES } from "constants/fetch-keys";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
export interface ISubIssuesRoot {
|
export interface ISubIssuesRoot {
|
||||||
parentIssue: IIssue;
|
parentIssue: IIssue;
|
||||||
@ -176,7 +177,7 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
[updateIssueStructure, projectId, updateIssue, user, workspaceSlug]
|
[updateIssueStructure, projectId, updateIssue, user, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEditable = userRole === 5 || userRole === 10 ? false : true;
|
const isEditable = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const mutateSubIssues = (parentIssueId: string | null) => {
|
const mutateSubIssues = (parentIssueId: string | null) => {
|
||||||
if (parentIssueId) mutate(SUB_ISSUES(parentIssueId));
|
if (parentIssueId) mutate(SUB_ISSUES(parentIssueId));
|
||||||
|
@ -33,6 +33,7 @@ import { linkDetails, IModule, ModuleLink } from "types";
|
|||||||
import { MODULE_DETAILS } from "constants/fetch-keys";
|
import { MODULE_DETAILS } from "constants/fetch-keys";
|
||||||
// constant
|
// constant
|
||||||
import { MODULE_STATUS } from "constants/module";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
const defaultValues: Partial<IModule> = {
|
const defaultValues: Partial<IModule> = {
|
||||||
lead: "",
|
lead: "",
|
||||||
@ -588,10 +589,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
handleEditLink={handleEditLink}
|
handleEditLink={handleEditLink}
|
||||||
handleDeleteLink={handleDeleteLink}
|
handleDeleteLink={handleDeleteLink}
|
||||||
userAuth={{
|
userAuth={{
|
||||||
isGuest: userRole === 5,
|
isGuest: userRole === EUserWorkspaceRoles.GUEST,
|
||||||
isViewer: userRole === 10,
|
isViewer: userRole === EUserWorkspaceRoles.VIEWER,
|
||||||
isMember: userRole === 15,
|
isMember: userRole === EUserWorkspaceRoles.MEMBER,
|
||||||
isOwner: userRole === 20,
|
isOwner: userRole === EUserWorkspaceRoles.ADMIN,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { Lightbulb } from "lucide-react";
|
import { Lightbulb } from "lucide-react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// hooks
|
||||||
|
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||||
// components
|
// components
|
||||||
import { SignInRoot } from "components/account";
|
import { SignInRoot } from "components/account";
|
||||||
// ui
|
// ui
|
||||||
@ -14,69 +15,33 @@ import { Loader, Spinner } from "@plane/ui";
|
|||||||
// images
|
// images
|
||||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
import latestFeatures from "public/onboarding/onboarding-pages.svg";
|
import latestFeatures from "public/onboarding/onboarding-pages.svg";
|
||||||
// types
|
|
||||||
import { IUser, IUserSettings } from "types";
|
|
||||||
|
|
||||||
export type AuthType = "sign-in" | "sign-up";
|
export type AuthType = "sign-in" | "sign-up";
|
||||||
|
|
||||||
export const SignInView = observer(() => {
|
export const SignInView = observer(() => {
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
user: { fetchCurrentUser, fetchCurrentUserSettings },
|
user: { currentUser },
|
||||||
appConfig: { envConfig },
|
appConfig: { envConfig },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { next: next_url } = router.query;
|
|
||||||
// states
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
|
||||||
// next-themes
|
// next-themes
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
// sign in redirection hook
|
||||||
const handleSignInRedirection = useCallback(
|
const { isRedirecting, handleRedirection } = useSignInRedirection();
|
||||||
async (user: IUser) => {
|
|
||||||
// if the user is not onboarded, redirect them to the onboarding page
|
|
||||||
if (!user.is_onboarded) {
|
|
||||||
router.push("/onboarding");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// if next_url is provided, redirect the user to that url
|
|
||||||
if (next_url) {
|
|
||||||
router.push(next_url.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the user is onboarded, fetch their last workspace details
|
|
||||||
await fetchCurrentUserSettings()
|
|
||||||
.then((userSettings: IUserSettings) => {
|
|
||||||
const workspaceSlug =
|
|
||||||
userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug;
|
|
||||||
if (workspaceSlug) router.push(`/${workspaceSlug}`);
|
|
||||||
else router.push("/profile");
|
|
||||||
})
|
|
||||||
.catch(() => setLoading(false));
|
|
||||||
},
|
|
||||||
[fetchCurrentUserSettings, router, next_url]
|
|
||||||
);
|
|
||||||
|
|
||||||
const mutateUserInfo = useCallback(async () => {
|
|
||||||
await fetchCurrentUser().then(async (user) => {
|
|
||||||
await handleSignInRedirection(user);
|
|
||||||
});
|
|
||||||
}, [fetchCurrentUser, handleSignInRedirection]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateUserInfo();
|
handleRedirection();
|
||||||
}, [mutateUserInfo]);
|
}, [handleRedirection]);
|
||||||
|
|
||||||
|
if (isRedirecting || currentUser)
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="grid place-items-center h-screen">
|
<div className="grid place-items-center h-screen">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
);
|
||||||
<div className={`bg-onboarding-gradient-100 h-full w-full`}>
|
|
||||||
|
return (
|
||||||
|
<div className="bg-onboarding-gradient-100 h-full w-full">
|
||||||
<div className="flex items-center justify-between sm:py-5 px-8 pb-4 sm:px-16 lg:px-28 ">
|
<div className="flex items-center justify-between sm:py-5 px-8 pb-4 sm:px-16 lg:px-28 ">
|
||||||
<div className="flex gap-x-2 py-10 items-center">
|
<div className="flex gap-x-2 py-10 items-center">
|
||||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||||
@ -102,7 +67,7 @@ export const SignInView = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SignInRoot handleSignInRedirection={mutateUserInfo} />
|
<SignInRoot />
|
||||||
|
|
||||||
<div className="flex py-2 bg-onboarding-background-100 border border-onboarding-border-200 mx-auto rounded-[3.5px] sm:w-96 mt-16">
|
<div className="flex py-2 bg-onboarding-background-100 border border-onboarding-border-200 mx-auto rounded-[3.5px] sm:w-96 mt-16">
|
||||||
<Lightbulb className="h-7 w-7 mr-2 mx-3" />
|
<Lightbulb className="h-7 w-7 mr-2 mx-3" />
|
||||||
@ -129,7 +94,5 @@ export const SignInView = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -26,6 +26,7 @@ import { CustomMenu, Tooltip } from "@plane/ui";
|
|||||||
import { CreateUpdatePageModal, DeletePageModal } from "components/pages";
|
import { CreateUpdatePageModal, DeletePageModal } from "components/pages";
|
||||||
// types
|
// types
|
||||||
import { IPage } from "types";
|
import { IPage } from "types";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
export interface IPagesListItem {
|
export interface IPagesListItem {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -144,7 +145,7 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
setCreateUpdatePageModal(true);
|
setCreateUpdatePageModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const userCanEdit = currentProjectRole === 15 || currentProjectRole === 20;
|
const userCanEdit = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -13,7 +13,7 @@ import { CustomSelect, Tooltip } from "@plane/ui";
|
|||||||
// icons
|
// icons
|
||||||
import { ChevronDown, Dot, XCircle } from "lucide-react";
|
import { ChevronDown, Dot, XCircle } from "lucide-react";
|
||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
|
||||||
// types
|
// types
|
||||||
import { IProjectMember, TUserProjectRole } from "types";
|
import { IProjectMember, TUserProjectRole } from "types";
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const isAdmin = currentProjectRole === 20;
|
const isAdmin = currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
||||||
const memberDetails = member.member;
|
const memberDetails = member.member;
|
||||||
|
|
||||||
const handleRemove = async () => {
|
const handleRemove = async () => {
|
||||||
@ -148,12 +148,13 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
disabled={
|
disabled={
|
||||||
memberDetails.id === currentUser?.id ||
|
memberDetails.id === currentUser?.id ||
|
||||||
!member.member ||
|
!member.member ||
|
||||||
(currentProjectRole && currentProjectRole !== 20 && currentProjectRole < member.role)
|
!currentProjectRole ||
|
||||||
|
currentProjectRole < member.role
|
||||||
}
|
}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
>
|
>
|
||||||
{Object.keys(ROLE).map((key) => {
|
{Object.keys(ROLE).map((key) => {
|
||||||
if (currentProjectRole && currentProjectRole !== 20 && currentProjectRole < parseInt(key)) return null;
|
if (currentProjectRole && !isAdmin && currentProjectRole < parseInt(key)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSelect.Option key={key} value={parseInt(key, 10)}>
|
<CustomSelect.Option key={key} value={parseInt(key, 10)}>
|
||||||
|
@ -15,6 +15,7 @@ import { Loader } from "@plane/ui";
|
|||||||
import { IProject, IUserLite, IWorkspace } from "types";
|
import { IProject, IUserLite, IWorkspace } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
const defaultValues: Partial<IProject> = {
|
const defaultValues: Partial<IProject> = {
|
||||||
project_lead: null,
|
project_lead: null,
|
||||||
@ -29,7 +30,7 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
|||||||
const { user: userStore, project: projectStore } = useMobxStore();
|
const { user: userStore, project: projectStore } = useMobxStore();
|
||||||
const { currentProjectDetails } = projectStore;
|
const { currentProjectDetails } = projectStore;
|
||||||
const { currentProjectRole } = userStore;
|
const { currentProjectRole } = userStore;
|
||||||
const isAdmin = currentProjectRole === 20;
|
const isAdmin = currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
||||||
// hooks
|
// hooks
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// form info
|
// form info
|
||||||
|
@ -15,7 +15,7 @@ import useToast from "hooks/use-toast";
|
|||||||
// types
|
// types
|
||||||
import { IProjectMember, TUserProjectRole } from "types";
|
import { IProjectMember, TUserProjectRole } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -246,7 +246,8 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
width="w-full"
|
width="w-full"
|
||||||
>
|
>
|
||||||
{Object.entries(ROLE).map(([key, label]) => {
|
{Object.entries(ROLE).map(([key, label]) => {
|
||||||
if (parseInt(key) > (currentProjectRole ?? 5)) return null;
|
if (parseInt(key) > (currentProjectRole ?? EUserWorkspaceRoles.GUEST))
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSelect.Option key={key} value={key}>
|
<CustomSelect.Option key={key} value={key}>
|
||||||
|
@ -9,6 +9,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {};
|
type Props = {};
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
|
|||||||
user: { currentUser, currentProjectRole },
|
user: { currentUser, currentProjectRole },
|
||||||
trackEvent: { setTrackElement, postHogEventTracker },
|
trackEvent: { setTrackElement, postHogEventTracker },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
const isAdmin = currentProjectRole === 20;
|
const isAdmin = currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
||||||
// hooks
|
// hooks
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -97,7 +98,7 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
|
|||||||
project_id: currentProjectDetails?.id,
|
project_id: currentProjectDetails?.id,
|
||||||
project_name: currentProjectDetails?.name,
|
project_name: currentProjectDetails?.name,
|
||||||
project_identifier: currentProjectDetails?.identifier,
|
project_identifier: currentProjectDetails?.identifier,
|
||||||
enabled: !currentProjectDetails?.[feature.property as keyof IProject]
|
enabled: !currentProjectDetails?.[feature.property as keyof IProject],
|
||||||
});
|
});
|
||||||
handleSubmit({
|
handleSubmit({
|
||||||
[feature.property]: !currentProjectDetails?.[feature.property as keyof IProject],
|
[feature.property]: !currentProjectDetails?.[feature.property as keyof IProject],
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
export const getUserRole = (role: number) => {
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
|
export const getUserRole = (role: EUserWorkspaceRoles) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case 5:
|
case EUserWorkspaceRoles.GUEST:
|
||||||
return "GUEST";
|
return "GUEST";
|
||||||
case 10:
|
case EUserWorkspaceRoles.VIEWER:
|
||||||
return "VIEWER";
|
return "VIEWER";
|
||||||
case 15:
|
case EUserWorkspaceRoles.MEMBER:
|
||||||
return "MEMBER";
|
return "MEMBER";
|
||||||
case 20:
|
case EUserWorkspaceRoles.ADMIN:
|
||||||
return "ADMIN";
|
return "ADMIN";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
74
web/hooks/use-sign-in-redirection.ts
Normal file
74
web/hooks/use-sign-in-redirection.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// types
|
||||||
|
import { IUser, IUserSettings } from "types";
|
||||||
|
|
||||||
|
type UseSignInRedirectionProps = {
|
||||||
|
error: any | null;
|
||||||
|
isRedirecting: boolean;
|
||||||
|
handleRedirection: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSignInRedirection = (): UseSignInRedirectionProps => {
|
||||||
|
// states
|
||||||
|
const [isRedirecting, setIsRedirecting] = useState(true);
|
||||||
|
const [error, setError] = useState<any | null>(null);
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { next_url } = router.query;
|
||||||
|
// mobx store
|
||||||
|
const {
|
||||||
|
user: { fetchCurrentUser, fetchCurrentUserSettings },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const handleSignInRedirection = useCallback(
|
||||||
|
async (user: IUser) => {
|
||||||
|
// if the user is not onboarded, redirect them to the onboarding page
|
||||||
|
if (!user.is_onboarded) {
|
||||||
|
router.push("/onboarding");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// if next_url is provided, redirect the user to that url
|
||||||
|
if (next_url) {
|
||||||
|
router.push(next_url.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the user is onboarded, fetch their last workspace details
|
||||||
|
await fetchCurrentUserSettings()
|
||||||
|
.then((userSettings: IUserSettings) => {
|
||||||
|
const workspaceSlug =
|
||||||
|
userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug;
|
||||||
|
if (workspaceSlug) router.push(`/${workspaceSlug}`);
|
||||||
|
else router.push("/profile");
|
||||||
|
})
|
||||||
|
.catch((err) => setError(err));
|
||||||
|
},
|
||||||
|
[fetchCurrentUserSettings, router, next_url]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateUserInfo = useCallback(async () => {
|
||||||
|
setIsRedirecting(true);
|
||||||
|
|
||||||
|
await fetchCurrentUser()
|
||||||
|
.then(async (user) => {
|
||||||
|
await handleSignInRedirection(user)
|
||||||
|
.catch((err) => setError(err))
|
||||||
|
.finally(() => setIsRedirecting(false));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setError(err);
|
||||||
|
setIsRedirecting(false);
|
||||||
|
});
|
||||||
|
}, [fetchCurrentUser, handleSignInRedirection]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
error,
|
||||||
|
isRedirecting,
|
||||||
|
handleRedirection: updateUserInfo,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSignInRedirection;
|
@ -1,15 +1,27 @@
|
|||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
// components
|
// components
|
||||||
import { ProjectSettingsSidebar } from "./sidebar";
|
import { ProjectSettingsSidebar } from "./sidebar";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { NotAuthorizedView } from "components/auth-screens";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
export interface IProjectSettingLayout {
|
export interface IProjectSettingLayout {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectSettingLayout: FC<IProjectSettingLayout> = (props) => {
|
export const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
return (
|
const {
|
||||||
|
user: { currentProjectRole },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const restrictViewSettings = currentProjectRole && currentProjectRole <= EUserWorkspaceRoles.VIEWER;
|
||||||
|
|
||||||
|
return restrictViewSettings ? (
|
||||||
|
<NotAuthorizedView type="project" />
|
||||||
|
) : (
|
||||||
<div className="flex gap-2 h-full w-full overflow-x-hidden overflow-y-scroll">
|
<div className="flex gap-2 h-full w-full overflow-x-hidden overflow-y-scroll">
|
||||||
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
|
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
|
||||||
<ProjectSettingsSidebar />
|
<ProjectSettingsSidebar />
|
||||||
@ -17,4 +29,4 @@ export const ProjectSettingLayout: FC<IProjectSettingLayout> = (props) => {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -14,6 +14,7 @@ import { ProjectSettingHeader } from "components/headers";
|
|||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
const AutomationSettingsPage: NextPageWithLayout = observer(() => {
|
const AutomationSettingsPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -39,7 +40,7 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAdmin = currentProjectRole === 20;
|
const isAdmin = currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
|
<section className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
|
||||||
|
@ -7,12 +7,23 @@ import { ProjectSettingHeader } from "components/headers";
|
|||||||
import { EstimatesList } from "components/estimates";
|
import { EstimatesList } from "components/estimates";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
const EstimatesSettingsPage: NextPageWithLayout = () => (
|
const EstimatesSettingsPage: NextPageWithLayout = observer(() => {
|
||||||
<div className="pr-9 py-8 w-full overflow-y-auto">
|
const {
|
||||||
|
user: { currentProjectRole },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const isAdmin = currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60 pointer-events-none"}`}>
|
||||||
<EstimatesList />
|
<EstimatesList />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
EstimatesSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
EstimatesSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ReactElement, useCallback } from "react";
|
import { ReactElement } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -9,6 +9,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import { AuthService } from "services/auth.service";
|
import { AuthService } from "services/auth.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import DefaultLayout from "layouts/default-layout";
|
||||||
// ui
|
// ui
|
||||||
@ -20,9 +21,6 @@ import latestFeatures from "public/onboarding/onboarding-pages.svg";
|
|||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// type
|
// type
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// types
|
|
||||||
import { IUser, IUserSettings } from "types";
|
|
||||||
|
|
||||||
type TResetPasswordFormValues = {
|
type TResetPasswordFormValues = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -45,10 +43,8 @@ const HomePage: NextPageWithLayout = () => {
|
|||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
// toast
|
// toast
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// mobx store
|
// sign in redirection hook
|
||||||
const {
|
const { handleRedirection } = useSignInRedirection();
|
||||||
user: { fetchCurrentUser, fetchCurrentUserSettings },
|
|
||||||
} = useMobxStore();
|
|
||||||
// form info
|
// form info
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -61,31 +57,6 @@ const HomePage: NextPageWithLayout = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSignInRedirection = useCallback(
|
|
||||||
async (user: IUser) => {
|
|
||||||
// if the user is not onboarded, redirect them to the onboarding page
|
|
||||||
if (!user.is_onboarded) {
|
|
||||||
router.push("/onboarding");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the user is onboarded, fetch their last workspace details
|
|
||||||
await fetchCurrentUserSettings().then((userSettings: IUserSettings) => {
|
|
||||||
const workspaceSlug =
|
|
||||||
userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug;
|
|
||||||
if (workspaceSlug) router.push(`/${workspaceSlug}`);
|
|
||||||
else router.push("/profile");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[fetchCurrentUserSettings, router]
|
|
||||||
);
|
|
||||||
|
|
||||||
const mutateUserInfo = useCallback(async () => {
|
|
||||||
await fetchCurrentUser().then(async (user) => {
|
|
||||||
await handleSignInRedirection(user);
|
|
||||||
});
|
|
||||||
}, [fetchCurrentUser, handleSignInRedirection]);
|
|
||||||
|
|
||||||
const handleResetPassword = async (formData: TResetPasswordFormValues) => {
|
const handleResetPassword = async (formData: TResetPasswordFormValues) => {
|
||||||
if (!uidb64 || !token || !email) return;
|
if (!uidb64 || !token || !email) return;
|
||||||
|
|
||||||
@ -95,7 +66,7 @@ const HomePage: NextPageWithLayout = () => {
|
|||||||
|
|
||||||
await authService
|
await authService
|
||||||
.resetPassword(uidb64.toString(), token.toString(), payload)
|
.resetPassword(uidb64.toString(), token.toString(), payload)
|
||||||
.then(() => mutateUserInfo())
|
.then(() => handleRedirection())
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 527 KiB After Width: | Height: | Size: 528 KiB |
@ -5,6 +5,7 @@ import { RootStore } from "../root";
|
|||||||
import { InboxService } from "services/inbox.service";
|
import { InboxService } from "services/inbox.service";
|
||||||
// types
|
// types
|
||||||
import { IInbox, IInboxFilterOptions, IInboxQueryParams } from "types";
|
import { IInbox, IInboxFilterOptions, IInboxQueryParams } from "types";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
export interface IInboxFiltersStore {
|
export interface IInboxFiltersStore {
|
||||||
// states
|
// states
|
||||||
@ -132,8 +133,8 @@ export class InboxFiltersStore implements IInboxFiltersStore {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const userRole = this.rootStore.user?.projectMemberInfo?.[projectId]?.role || 0;
|
const userRole = this.rootStore.user?.currentProjectRole || EUserWorkspaceRoles.GUEST;
|
||||||
if (userRole > 10) {
|
if (userRole > EUserWorkspaceRoles.VIEWER) {
|
||||||
await this.inboxService.patchInbox(workspaceSlug, projectId, inboxId, { view_props: newViewProps });
|
await this.inboxService.patchInbox(workspaceSlug, projectId, inboxId, { view_props: newViewProps });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -62,7 +62,14 @@ export interface ICycleIssuesStore {
|
|||||||
issueId: string,
|
issueId: string,
|
||||||
issueBridgeId: string
|
issueBridgeId: string
|
||||||
) => Promise<IIssue>;
|
) => Promise<IIssue>;
|
||||||
|
transferIssuesFromCycle: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
payload: {
|
||||||
|
new_cycle_id: string;
|
||||||
|
}
|
||||||
|
) => Promise<IIssue>;
|
||||||
viewFlags: ViewFlags;
|
viewFlags: ViewFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +110,7 @@ export class CycleIssuesStore extends IssueBaseStore implements ICycleIssuesStor
|
|||||||
quickAddIssue: action,
|
quickAddIssue: action,
|
||||||
addIssueToCycle: action,
|
addIssueToCycle: action,
|
||||||
removeIssueFromCycle: action,
|
removeIssueFromCycle: action,
|
||||||
|
transferIssuesFromCycle: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
@ -348,4 +356,28 @@ export class CycleIssuesStore extends IssueBaseStore implements ICycleIssuesStor
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
transferIssuesFromCycle = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
payload: {
|
||||||
|
new_cycle_id: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await this.cycleService.transferIssues(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
cycleId as string,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
await this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user