Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24f993dbee | |||
| 1ff0538077 | |||
| 0e055666e7 | |||
| 1b98b65a80 | |||
| 03c8aeed57 |
+13
-2
@@ -2,5 +2,16 @@
|
||||
*.pyc
|
||||
.env
|
||||
venv
|
||||
node_modules
|
||||
npm-debug.log
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
npm-debug.log
|
||||
.next/
|
||||
**/.next/
|
||||
.turbo/
|
||||
**/.turbo/
|
||||
build/
|
||||
**/build/
|
||||
out/
|
||||
**/out/
|
||||
dist/
|
||||
**/dist/
|
||||
@@ -27,4 +27,4 @@ python manage.py configure_instance
|
||||
# Create the default bucket
|
||||
python manage.py create_bucket
|
||||
|
||||
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
||||
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:${PORT:-8000} --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
||||
|
||||
@@ -95,6 +95,16 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = WorkspaceMemberInvite
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"email",
|
||||
"token",
|
||||
"workspace",
|
||||
"message",
|
||||
"responded_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class TeamSerializer(BaseSerializer):
|
||||
|
||||
@@ -65,6 +65,7 @@ urlpatterns = [
|
||||
{
|
||||
"delete": "destroy",
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
}
|
||||
),
|
||||
name="workspace-invitations",
|
||||
|
||||
@@ -34,7 +34,7 @@ from plane.app.serializers import (
|
||||
from plane.db.models import User, WorkspaceMemberInvite
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.forgot_password_task import forgot_password
|
||||
from plane.license.models import Instance, InstanceConfiguration
|
||||
from plane.license.models import Instance
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.bgtasks.magic_link_code_task import magic_link
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
@@ -321,8 +321,19 @@ class EmailCheckEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the configurations
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
# Get configuration values
|
||||
ENABLE_SIGNUP, ENABLE_MAGIC_LINK_LOGIN = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
"default": os.environ.get("ENABLE_SIGNUP"),
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
@@ -347,12 +358,7 @@ class EmailCheckEndpoint(BaseAPIView):
|
||||
if user is None:
|
||||
# Create the user
|
||||
if (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
== "0"
|
||||
ENABLE_SIGNUP == "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=email,
|
||||
).exists()
|
||||
@@ -372,13 +378,8 @@ class EmailCheckEndpoint(BaseAPIView):
|
||||
is_password_autoset=True,
|
||||
)
|
||||
|
||||
|
||||
if not bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_MAGIC_LINK_LOGIN",
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
|
||||
),
|
||||
ENABLE_MAGIC_LINK_LOGIN,
|
||||
):
|
||||
return Response(
|
||||
{"error": "Magic link sign in is disabled."},
|
||||
@@ -386,16 +387,15 @@ class EmailCheckEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=True,
|
||||
)
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=True,
|
||||
)
|
||||
key, token, current_attempt = generate_magic_token(email=email)
|
||||
if not current_attempt:
|
||||
return Response(
|
||||
@@ -413,28 +413,21 @@ class EmailCheckEndpoint(BaseAPIView):
|
||||
else:
|
||||
if user.is_password_autoset:
|
||||
## Generate a random token
|
||||
if not bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_MAGIC_LINK_LOGIN",
|
||||
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
|
||||
),
|
||||
):
|
||||
if not bool(ENABLE_MAGIC_LINK_LOGIN):
|
||||
return Response(
|
||||
{"error": "Magic link sign in is disabled."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=False,
|
||||
)
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
# Generate magic token
|
||||
key, token, current_attempt = generate_magic_token(email=email)
|
||||
@@ -454,16 +447,15 @@ class EmailCheckEndpoint(BaseAPIView):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
else:
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=False,
|
||||
)
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
# User should enter password to login
|
||||
return Response(
|
||||
|
||||
@@ -27,7 +27,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.license.models import InstanceConfiguration, Instance
|
||||
from plane.license.models import Instance
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
|
||||
@@ -52,8 +52,6 @@ class SignUpEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
|
||||
email = request.data.get("email", False)
|
||||
password = request.data.get("password", False)
|
||||
## Raise exception if any of the above are missing
|
||||
@@ -73,14 +71,20 @@ class SignUpEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# get configuration values
|
||||
# Get configuration values
|
||||
ENABLE_SIGNUP, = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
"default": os.environ.get("ENABLE_SIGNUP"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# If the sign up is not enabled and the user does not have invite disallow him from creating the account
|
||||
if (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
== "0"
|
||||
ENABLE_SIGNUP == "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=email,
|
||||
).exists()
|
||||
@@ -169,16 +173,17 @@ class SignInEndpoint(BaseAPIView):
|
||||
|
||||
# Create the user
|
||||
else:
|
||||
# Get the configurations
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
ENABLE_SIGNUP, = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
"default": os.environ.get("ENABLE_SIGNUP"),
|
||||
},
|
||||
]
|
||||
)
|
||||
# Create the user
|
||||
if (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
== "0"
|
||||
ENABLE_SIGNUP == "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=email,
|
||||
).exists()
|
||||
@@ -264,16 +269,15 @@ class SignInEndpoint(BaseAPIView):
|
||||
workspace_member_invites.delete()
|
||||
project_member_invites.delete()
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=False,
|
||||
)
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="EMAIL",
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
data = {
|
||||
@@ -347,16 +351,15 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=False,
|
||||
)
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium="MAGIC_LINK",
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
user.is_active = True
|
||||
user.is_email_verified = True
|
||||
|
||||
@@ -11,7 +11,6 @@ from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
@@ -21,89 +20,101 @@ class ConfigurationEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
|
||||
# Get all the configuration
|
||||
(
|
||||
GOOGLE_CLIENT_ID,
|
||||
GITHUB_CLIENT_ID,
|
||||
GITHUB_APP_NAME,
|
||||
EMAIL_HOST_USER,
|
||||
EMAIL_HOST_PASSWORD,
|
||||
ENABLE_MAGIC_LINK_LOGIN,
|
||||
ENABLE_EMAIL_PASSWORD,
|
||||
SLACK_CLIENT_ID,
|
||||
POSTHOG_API_KEY,
|
||||
POSTHOG_HOST,
|
||||
UNSPLASH_ACCESS_KEY,
|
||||
OPENAI_API_KEY,
|
||||
) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "GOOGLE_CLIENT_ID",
|
||||
"default": os.environ.get("GOOGLE_CLIENT_ID", None),
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_CLIENT_ID",
|
||||
"default": os.environ.get("GITHUB_CLIENT_ID", None),
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_APP_NAME",
|
||||
"default": os.environ.get("GITHUB_APP_NAME", None),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_USER",
|
||||
"default": os.environ.get("EMAIL_HOST_USER", None),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_PASSWORD",
|
||||
"default": os.environ.get("EMAIL_HOST_PASSWORD", None),
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_EMAIL_PASSWORD",
|
||||
"default": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
|
||||
},
|
||||
{
|
||||
"key": "SLACK_CLIENT_ID",
|
||||
"default": os.environ.get("SLACK_CLIENT_ID", "1"),
|
||||
},
|
||||
{
|
||||
"key": "POSTHOG_API_KEY",
|
||||
"default": os.environ.get("POSTHOG_API_KEY", "1"),
|
||||
},
|
||||
{
|
||||
"key": "POSTHOG_HOST",
|
||||
"default": os.environ.get("POSTHOG_HOST", "1"),
|
||||
},
|
||||
{
|
||||
"key": "UNSPLASH_ACCESS_KEY",
|
||||
"default": os.environ.get("UNSPLASH_ACCESS_KEY", "1"),
|
||||
},
|
||||
{
|
||||
"key": "OPENAI_API_KEY",
|
||||
"default": os.environ.get("OPENAI_API_KEY", "1"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
data = {}
|
||||
# Authentication
|
||||
data["google_client_id"] = get_configuration_value(
|
||||
instance_configuration,
|
||||
"GOOGLE_CLIENT_ID",
|
||||
os.environ.get("GOOGLE_CLIENT_ID", None),
|
||||
)
|
||||
data["github_client_id"] = get_configuration_value(
|
||||
instance_configuration,
|
||||
"GITHUB_CLIENT_ID",
|
||||
os.environ.get("GITHUB_CLIENT_ID", None),
|
||||
)
|
||||
data["github_app_name"] = get_configuration_value(
|
||||
instance_configuration,
|
||||
"GITHUB_APP_NAME",
|
||||
os.environ.get("GITHUB_APP_NAME", None),
|
||||
)
|
||||
data["google_client_id"] = GOOGLE_CLIENT_ID
|
||||
data["github_client_id"] = GITHUB_CLIENT_ID
|
||||
data["github_app_name"] = GITHUB_APP_NAME
|
||||
data["magic_login"] = (
|
||||
bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_USER",
|
||||
os.environ.get("EMAIL_HOST_USER", None),
|
||||
),
|
||||
)
|
||||
and bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
os.environ.get("EMAIL_HOST_PASSWORD", None),
|
||||
)
|
||||
)
|
||||
) and get_configuration_value(
|
||||
instance_configuration, "ENABLE_MAGIC_LINK_LOGIN", "1"
|
||||
) == "1"
|
||||
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
||||
) and ENABLE_MAGIC_LINK_LOGIN == "1"
|
||||
|
||||
data["email_password_login"] = (
|
||||
get_configuration_value(
|
||||
instance_configuration, "ENABLE_EMAIL_PASSWORD", "1"
|
||||
)
|
||||
== "1"
|
||||
)
|
||||
data["email_password_login"] = ENABLE_EMAIL_PASSWORD == "1"
|
||||
# Slack client
|
||||
data["slack_client_id"] = get_configuration_value(
|
||||
instance_configuration,
|
||||
"SLACK_CLIENT_ID",
|
||||
os.environ.get("SLACK_CLIENT_ID", None),
|
||||
)
|
||||
data["slack_client_id"] = SLACK_CLIENT_ID
|
||||
|
||||
# Posthog
|
||||
data["posthog_api_key"] = get_configuration_value(
|
||||
instance_configuration,
|
||||
"POSTHOG_API_KEY",
|
||||
os.environ.get("POSTHOG_API_KEY", None),
|
||||
)
|
||||
data["posthog_host"] = get_configuration_value(
|
||||
instance_configuration,
|
||||
"POSTHOG_HOST",
|
||||
os.environ.get("POSTHOG_HOST", None),
|
||||
)
|
||||
data["posthog_api_key"] = POSTHOG_API_KEY
|
||||
data["posthog_host"] = POSTHOG_HOST
|
||||
|
||||
# Unsplash
|
||||
data["has_unsplash_configured"] = bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"UNSPLASH_ACCESS_KEY",
|
||||
os.environ.get("UNSPLASH_ACCESS_KEY", None),
|
||||
)
|
||||
)
|
||||
data["has_unsplash_configured"] = UNSPLASH_ACCESS_KEY
|
||||
|
||||
# Open AI settings
|
||||
data["has_openai_configured"] = bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"OPENAI_API_KEY",
|
||||
os.environ.get("OPENAI_API_KEY", None),
|
||||
)
|
||||
)
|
||||
data["has_openai_configured"] = bool(OPENAI_API_KEY)
|
||||
|
||||
# File size settings
|
||||
data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||
|
||||
# is self managed
|
||||
data["is_self_managed"] = bool(int(os.environ.get("IS_SELF_MANAGED", "1")))
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Python imports
|
||||
import requests
|
||||
import os
|
||||
|
||||
# Third party imports
|
||||
from openai import OpenAI
|
||||
from rest_framework.response import Response
|
||||
@@ -15,23 +16,31 @@ from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Workspace, Project
|
||||
from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
|
||||
from plane.utils.integrations.github import get_release_notes
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
class GPTIntegrationEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "OPENAI_API_KEY",
|
||||
"default": os.environ.get("OPENAI_API_KEY", None),
|
||||
},
|
||||
{
|
||||
"key": "GPT_ENGINE",
|
||||
"default": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Get the configuration value
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
api_key = get_configuration_value(instance_configuration, "OPENAI_API_KEY", os.environ.get("OPENAI_API_KEY"))
|
||||
gpt_engine = get_configuration_value(instance_configuration, "GPT_ENGINE", os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"))
|
||||
|
||||
# Check the keys
|
||||
if not api_key or not gpt_engine:
|
||||
if not OPENAI_API_KEY or not GPT_ENGINE:
|
||||
return Response(
|
||||
{"error": "OpenAI API key and engine is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -48,11 +57,11 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
final_text = task + "\n" + prompt
|
||||
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
api_key=OPENAI_API_KEY,
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=gpt_engine,
|
||||
model=GPT_ENGINE,
|
||||
messages=[{"role": "user", "content": final_text}],
|
||||
)
|
||||
|
||||
@@ -79,13 +88,17 @@ class ReleaseNotesEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class UnsplashEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
unsplash_access_key = get_configuration_value(instance_configuration, "UNSPLASH_ACCESS_KEY", os.environ.get("UNSPLASH_ACCESS_KEY"))
|
||||
|
||||
UNSPLASH_ACCESS_KEY, = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "UNSPLASH_ACCESS_KEY",
|
||||
"default": os.environ.get("UNSPLASH_ACCESS_KEY"),
|
||||
}
|
||||
]
|
||||
)
|
||||
# Check unsplash access key
|
||||
if not unsplash_access_key:
|
||||
if not UNSPLASH_ACCESS_KEY:
|
||||
return Response([], status=status.HTTP_200_OK)
|
||||
|
||||
# Query parameters
|
||||
@@ -94,9 +107,9 @@ class UnsplashEndpoint(BaseAPIView):
|
||||
per_page = request.GET.get("per_page", 20)
|
||||
|
||||
url = (
|
||||
f"https://api.unsplash.com/search/photos/?client_id={unsplash_access_key}&query={query}&page=${page}&per_page={per_page}"
|
||||
f"https://api.unsplash.com/search/photos/?client_id={UNSPLASH_ACCESS_KEY}&query={query}&page=${page}&per_page={per_page}"
|
||||
if query
|
||||
else f"https://api.unsplash.com/photos/?client_id={unsplash_access_key}&page={page}&per_page={per_page}"
|
||||
else f"https://api.unsplash.com/photos/?client_id={UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}"
|
||||
)
|
||||
|
||||
headers = {
|
||||
|
||||
@@ -30,7 +30,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
from .base import BaseAPIView
|
||||
from plane.license.models import InstanceConfiguration, Instance
|
||||
from plane.license.models import Instance
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
@@ -147,18 +147,20 @@ class OauthEndpoint(BaseAPIView):
|
||||
id_token = request.data.get("credential", False)
|
||||
client_id = request.data.get("clientId", False)
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.values(
|
||||
"key", "value"
|
||||
GOOGLE_CLIENT_ID, GITHUB_CLIENT_ID = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "GOOGLE_CLIENT_ID",
|
||||
"default": os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_CLIENT_ID",
|
||||
"default": os.environ.get("GITHUB_CLIENT_ID"),
|
||||
},
|
||||
]
|
||||
)
|
||||
if not get_configuration_value(
|
||||
instance_configuration,
|
||||
"GOOGLE_CLIENT_ID",
|
||||
os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
) or not get_configuration_value(
|
||||
instance_configuration,
|
||||
"GITHUB_CLIENT_ID",
|
||||
os.environ.get("GITHUB_CLIENT_ID"),
|
||||
):
|
||||
|
||||
if not GOOGLE_CLIENT_ID or not GITHUB_CLIENT_ID:
|
||||
return Response(
|
||||
{"error": "Github or Google login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -278,16 +280,15 @@ class OauthEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium=medium.upper(),
|
||||
first_time=False,
|
||||
)
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium=medium.upper(),
|
||||
first_time=False,
|
||||
)
|
||||
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
|
||||
@@ -298,17 +299,16 @@ class OauthEndpoint(BaseAPIView):
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
except User.DoesNotExist:
|
||||
## Signup Case
|
||||
instance_configuration = InstanceConfiguration.objects.values(
|
||||
"key", "value"
|
||||
ENABLE_SIGNUP, = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
"default": os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
}
|
||||
]
|
||||
)
|
||||
if (
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"ENABLE_SIGNUP",
|
||||
os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
)
|
||||
== "0"
|
||||
ENABLE_SIGNUP == "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=email,
|
||||
).exists()
|
||||
@@ -411,16 +411,15 @@ class OauthEndpoint(BaseAPIView):
|
||||
project_member_invites.delete()
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium=medium.upper(),
|
||||
first_time=True,
|
||||
)
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="SIGN_IN",
|
||||
medium=medium.upper(),
|
||||
first_time=True,
|
||||
)
|
||||
|
||||
SocialLoginConnection.objects.update_or_create(
|
||||
medium=medium,
|
||||
|
||||
@@ -408,15 +408,14 @@ class WorkspaceJoinEndpoint(BaseAPIView):
|
||||
workspace_invite.delete()
|
||||
|
||||
# Send event
|
||||
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
|
||||
workspace_invite_event.delay(
|
||||
user=user.id if user is not None else None,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="MEMBER_ACCEPTED",
|
||||
accepted_from="EMAIL",
|
||||
)
|
||||
workspace_invite_event.delay(
|
||||
user=user.id if user is not None else None,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="MEMBER_ACCEPTED",
|
||||
accepted_from="EMAIL",
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Workspace Invitation Accepted"},
|
||||
|
||||
@@ -18,7 +18,6 @@ from sentry_sdk import capture_exception
|
||||
from plane.db.models import Issue
|
||||
from plane.utils.analytics_plot import build_graph_plot
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.license.models import InstanceConfiguration, Instance
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
row_mapping = {
|
||||
@@ -52,11 +51,6 @@ def send_export_email(email, slug, csv_buffer, rows):
|
||||
|
||||
csv_buffer.seek(0)
|
||||
|
||||
# Configure email connection from the database
|
||||
instance_configuration = InstanceConfiguration.objects.filter(
|
||||
key__startswith="EMAIL_"
|
||||
).values("key", "value")
|
||||
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
@@ -64,7 +58,7 @@ def send_export_email(email, slug, csv_buffer, rows):
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||
) = get_email_configuration()
|
||||
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
|
||||
@@ -1,50 +1,78 @@
|
||||
import uuid
|
||||
import os
|
||||
|
||||
from posthog import Posthog
|
||||
from django.conf import settings
|
||||
|
||||
#third party imports
|
||||
# third party imports
|
||||
from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
from posthog import Posthog
|
||||
|
||||
# module imports
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
def posthogConfiguration():
|
||||
POSTHOG_API_KEY, POSTHOG_HOST = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "POSTHOG_API_KEY",
|
||||
"default": os.environ.get("POSTHOG_API_KEY", None),
|
||||
},
|
||||
{
|
||||
"key": "POSTHOG_HOST",
|
||||
"default": os.environ.get("POSTHOG_HOST", None),
|
||||
},
|
||||
]
|
||||
)
|
||||
if POSTHOG_API_KEY and POSTHOG_HOST:
|
||||
return POSTHOG_API_KEY, POSTHOG_HOST
|
||||
else:
|
||||
return None, None
|
||||
|
||||
|
||||
@shared_task
|
||||
def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
|
||||
try:
|
||||
posthog = Posthog(settings.POSTHOG_API_KEY, host=settings.POSTHOG_HOST)
|
||||
posthog.capture(
|
||||
email,
|
||||
event=event_name,
|
||||
properties={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"user": {"email": email, "id": str(user)},
|
||||
"device_ctx": {
|
||||
"ip": ip,
|
||||
"user_agent": user_agent,
|
||||
},
|
||||
"medium": medium,
|
||||
"first_time": first_time
|
||||
}
|
||||
)
|
||||
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
|
||||
|
||||
if POSTHOG_API_KEY and POSTHOG_HOST:
|
||||
posthog = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST)
|
||||
posthog.capture(
|
||||
email,
|
||||
event=event_name,
|
||||
properties={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"user": {"email": email, "id": str(user)},
|
||||
"device_ctx": {
|
||||
"ip": ip,
|
||||
"user_agent": user_agent,
|
||||
},
|
||||
"medium": medium,
|
||||
"first_time": first_time
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
|
||||
|
||||
@shared_task
|
||||
def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_from):
|
||||
try:
|
||||
posthog = Posthog(settings.POSTHOG_API_KEY, host=settings.POSTHOG_HOST)
|
||||
posthog.capture(
|
||||
email,
|
||||
event=event_name,
|
||||
properties={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"user": {"email": email, "id": str(user)},
|
||||
"device_ctx": {
|
||||
"ip": ip,
|
||||
"user_agent": user_agent,
|
||||
},
|
||||
"accepted_from": accepted_from
|
||||
}
|
||||
)
|
||||
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
|
||||
|
||||
if POSTHOG_API_KEY and POSTHOG_HOST:
|
||||
posthog = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST)
|
||||
posthog.capture(
|
||||
email,
|
||||
event=event_name,
|
||||
properties={
|
||||
"event_id": uuid.uuid4().hex,
|
||||
"user": {"email": email, "id": str(user)},
|
||||
"device_ctx": {
|
||||
"ip": ip,
|
||||
"user_agent": user_agent,
|
||||
},
|
||||
"accepted_from": accepted_from
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
@@ -14,7 +14,6 @@ from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.license.models import InstanceConfiguration, Instance
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
|
||||
@@ -26,10 +25,6 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
)
|
||||
abs_url = str(current_site) + relative_link
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.filter(
|
||||
key__startswith="EMAIL_"
|
||||
).values("key", "value")
|
||||
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
@@ -37,7 +32,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||
) = get_email_configuration()
|
||||
|
||||
subject = "A new password to your Plane account has been requested"
|
||||
|
||||
@@ -51,9 +46,6 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.filter(
|
||||
key__startswith="EMAIL_"
|
||||
).values("key", "value")
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
|
||||
@@ -14,17 +14,12 @@ from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.license.models import InstanceConfiguration, Instance
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
|
||||
@shared_task
|
||||
def magic_link(email, key, token, current_site):
|
||||
try:
|
||||
instance_configuration = InstanceConfiguration.objects.filter(
|
||||
key__startswith="EMAIL_"
|
||||
).values("key", "value")
|
||||
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
@@ -32,7 +27,7 @@ def magic_link(email, key, token, current_site):
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||
) = get_email_configuration()
|
||||
|
||||
# Send the mail
|
||||
subject = f"Your unique Plane login code is {token}"
|
||||
|
||||
@@ -13,7 +13,6 @@ from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Project, User, ProjectMemberInvite
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
@shared_task
|
||||
@@ -47,7 +46,6 @@ def project_invitation(email, project_id, token, current_site, invitor):
|
||||
project_member_invite.save()
|
||||
|
||||
# Configure email connection from the database
|
||||
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
@@ -55,7 +53,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||
) = get_email_configuration()
|
||||
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
|
||||
@@ -17,7 +17,6 @@ from slack_sdk.errors import SlackApiError
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Workspace, WorkspaceMemberInvite, User
|
||||
from plane.license.models import InstanceConfiguration, Instance
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
|
||||
@@ -37,9 +36,6 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
# The complete url including the domain
|
||||
abs_url = str(current_site) + relative_link
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.filter(
|
||||
key__startswith="EMAIL_"
|
||||
).values("key", "value")
|
||||
|
||||
(
|
||||
EMAIL_HOST,
|
||||
@@ -48,7 +44,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration(instance_configuration=instance_configuration)
|
||||
) = get_email_configuration()
|
||||
|
||||
# Subject of the email
|
||||
subject = f"{user.first_name or user.display_name or user.email} has invited you to join them in {workspace.name} on Plane"
|
||||
@@ -69,9 +65,6 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
workspace_member_invite.message = text_content
|
||||
workspace_member_invite.save()
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.filter(
|
||||
key__startswith="EMAIL_"
|
||||
).values("key", "value")
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
|
||||
@@ -28,10 +28,6 @@ app.conf.beat_schedule = {
|
||||
"task": "plane.bgtasks.file_asset_task.delete_file_asset",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"check-instance-verification": {
|
||||
"task": "plane.license.bgtasks.instance_verification_task.instance_verification_task",
|
||||
"schedule": crontab(minute=0, hour='*/4'),
|
||||
},
|
||||
}
|
||||
|
||||
# Load task modules from all registered Django app configs.
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
# Python imports
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import User
|
||||
from plane.license.models import Instance, InstanceAdmin
|
||||
|
||||
|
||||
def instance_verification(instance):
|
||||
with open("package.json", "r") as file:
|
||||
# Load JSON content from the file
|
||||
data = json.load(file)
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
payload = {
|
||||
"instance_key": settings.INSTANCE_KEY,
|
||||
"version": data.get("version", 0.1),
|
||||
"machine_signature": os.environ.get("MACHINE_SIGNATURE", "machine-signature"),
|
||||
"user_count": User.objects.filter(is_bot=False).count(),
|
||||
}
|
||||
# Register the instance
|
||||
response = requests.post(
|
||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||
headers=headers,
|
||||
data=json.dumps(payload),
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
# check response status
|
||||
if response.status_code == 201:
|
||||
data = response.json()
|
||||
# Update instance
|
||||
instance.instance_id = data.get("id")
|
||||
instance.license_key = data.get("license_key")
|
||||
instance.api_key = data.get("api_key")
|
||||
instance.version = data.get("version")
|
||||
instance.user_count = data.get("user_count", 0)
|
||||
instance.is_verified = True
|
||||
instance.save()
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
def admin_verification(instance):
|
||||
# Save the user in control center
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-instance-id": instance.instance_id,
|
||||
"x-api-key": instance.api_key,
|
||||
}
|
||||
|
||||
# Get all the unverified instance admins
|
||||
instance_admins = InstanceAdmin.objects.filter(is_verified=False).select_related(
|
||||
"user"
|
||||
)
|
||||
updated_instance_admin = []
|
||||
|
||||
# Verify the instance admin
|
||||
for instance_admin in instance_admins:
|
||||
instance_admin.is_verified = True
|
||||
# Create the admin
|
||||
response = requests.post(
|
||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/register/",
|
||||
headers=headers,
|
||||
data=json.dumps(
|
||||
{
|
||||
"email": str(instance_admin.user.email),
|
||||
"signup_mode": "EMAIL",
|
||||
"is_admin": True,
|
||||
}
|
||||
),
|
||||
timeout=30,
|
||||
)
|
||||
updated_instance_admin.append(instance_admin)
|
||||
|
||||
# update all the instance admins
|
||||
InstanceAdmin.objects.bulk_update(
|
||||
updated_instance_admin, ["is_verified"], batch_size=10
|
||||
)
|
||||
return
|
||||
|
||||
def instance_user_count(instance):
|
||||
try:
|
||||
instance_users = User.objects.filter(is_bot=False).count()
|
||||
|
||||
# Update the count in the license engine
|
||||
payload = {
|
||||
"user_count": instance_users,
|
||||
}
|
||||
|
||||
# Save the user in control center
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-instance-id": instance.instance_id,
|
||||
"x-api-key": instance.api_key,
|
||||
}
|
||||
|
||||
# Update the license engine
|
||||
_ = requests.post(
|
||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||
headers=headers,
|
||||
data=json.dumps(payload),
|
||||
timeout=30,
|
||||
)
|
||||
return
|
||||
except requests.RequestException:
|
||||
return
|
||||
|
||||
|
||||
@shared_task
|
||||
def instance_verification_task():
|
||||
try:
|
||||
# Get the first instance
|
||||
instance = Instance.objects.first()
|
||||
|
||||
# Only register instance if it is not verified
|
||||
if not instance.is_verified:
|
||||
instance_verification(instance=instance)
|
||||
|
||||
# Admin verifications
|
||||
admin_verification(instance=instance)
|
||||
|
||||
# Update user count
|
||||
instance_user_count(instance=instance)
|
||||
|
||||
return
|
||||
except requests.RequestException:
|
||||
return
|
||||
@@ -30,13 +30,11 @@ class Command(BaseCommand):
|
||||
# Load JSON content from the file
|
||||
data = json.load(file)
|
||||
|
||||
machine_signature = options.get("machine_signature", False)
|
||||
machine_signature = options.get("machine_signature", "machine-signature")
|
||||
|
||||
if not machine_signature:
|
||||
raise CommandError("Machine signature is required")
|
||||
|
||||
# Check if machine is online
|
||||
headers = {"Content-Type": "application/json"}
|
||||
payload = {
|
||||
"instance_key": settings.INSTANCE_KEY,
|
||||
"version": data.get("version", 0.1),
|
||||
@@ -44,51 +42,21 @@ class Command(BaseCommand):
|
||||
"user_count": User.objects.filter(is_bot=False).count(),
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
|
||||
headers=headers,
|
||||
data=json.dumps(payload),
|
||||
timeout=30
|
||||
)
|
||||
instance = Instance.objects.create(
|
||||
instance_name="Plane Free",
|
||||
instance_id=secrets.token_hex(12),
|
||||
license_key=None,
|
||||
api_key=secrets.token_hex(8),
|
||||
version=payload.get("version"),
|
||||
last_checked_at=timezone.now(),
|
||||
user_count=payload.get("user_count", 0),
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
data = response.json()
|
||||
# Create instance
|
||||
instance = Instance.objects.create(
|
||||
instance_name="Plane Free",
|
||||
instance_id=data.get("id"),
|
||||
license_key=data.get("license_key"),
|
||||
api_key=data.get("api_key"),
|
||||
version=data.get("version"),
|
||||
last_checked_at=timezone.now(),
|
||||
user_count=data.get("user_count", 0),
|
||||
is_verified=True,
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Instance successfully registered and verified"
|
||||
)
|
||||
)
|
||||
return
|
||||
except requests.RequestException as _e:
|
||||
instance = Instance.objects.create(
|
||||
instance_name="Plane Free",
|
||||
instance_id=secrets.token_hex(12),
|
||||
license_key=None,
|
||||
api_key=secrets.token_hex(8),
|
||||
version=payload.get("version"),
|
||||
last_checked_at=timezone.now(),
|
||||
user_count=payload.get("user_count", 0),
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Instance registered"
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Instance successfully registered"
|
||||
)
|
||||
)
|
||||
return
|
||||
raise CommandError("Instance could not be registered")
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
|
||||
@@ -1,63 +1,71 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.utils.encryption import decrypt_data
|
||||
|
||||
|
||||
# Helper function to return value from the passed key
|
||||
def get_configuration_value(query, key, default=None):
|
||||
for item in query:
|
||||
if item["key"] == key:
|
||||
return item.get("value", default)
|
||||
return default
|
||||
def get_configuration_value(keys):
|
||||
environment_list = []
|
||||
if settings.SKIP_ENV_VAR:
|
||||
# Get the configurations
|
||||
instance_configuration = InstanceConfiguration.objects.values(
|
||||
"key", "value", "is_encrypted"
|
||||
)
|
||||
|
||||
for key in keys:
|
||||
for item in instance_configuration:
|
||||
if key.get("key") == item.get("key"):
|
||||
if item.get("is_encrypted", False):
|
||||
environment_list.append(decrypt_data(item.get("value")))
|
||||
else:
|
||||
environment_list.append(item.get("value"))
|
||||
|
||||
break
|
||||
else:
|
||||
environment_list.append(key.get("default"))
|
||||
else:
|
||||
# Get the configuration from os
|
||||
for key in keys:
|
||||
environment_list.append(os.environ.get(key.get("key"), key.get("default")))
|
||||
|
||||
return tuple(environment_list)
|
||||
|
||||
|
||||
def get_email_configuration(instance_configuration):
|
||||
# Get the configuration variables
|
||||
EMAIL_HOST_USER = get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_USER",
|
||||
os.environ.get("EMAIL_HOST_USER", None),
|
||||
)
|
||||
|
||||
EMAIL_HOST_PASSWORD = get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
os.environ.get("EMAIL_HOST_PASSWORD", None),
|
||||
)
|
||||
|
||||
EMAIL_HOST = get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST",
|
||||
os.environ.get("EMAIL_HOST", None),
|
||||
)
|
||||
|
||||
EMAIL_FROM = get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_FROM",
|
||||
os.environ.get("EMAIL_FROM", None),
|
||||
)
|
||||
|
||||
EMAIL_USE_TLS = get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_USE_TLS",
|
||||
os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
)
|
||||
|
||||
EMAIL_PORT = get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_PORT",
|
||||
587,
|
||||
)
|
||||
|
||||
EMAIL_FROM = get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_FROM",
|
||||
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
||||
)
|
||||
|
||||
def get_email_configuration():
|
||||
return (
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
EMAIL_HOST_PASSWORD,
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_FROM,
|
||||
get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_USER",
|
||||
"default": os.environ.get("EMAIL_HOST_USER"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_PASSWORD",
|
||||
"default": os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_PORT",
|
||||
"default": os.environ.get("EMAIL_PORT", 587),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_USE_TLS",
|
||||
"default": os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_FROM",
|
||||
"default": os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -287,7 +287,6 @@ CELERY_IMPORTS = (
|
||||
"plane.bgtasks.issue_automation_task",
|
||||
"plane.bgtasks.exporter_expired_task",
|
||||
"plane.bgtasks.file_asset_task",
|
||||
"plane.license.bgtasks.instance_verification_task",
|
||||
)
|
||||
|
||||
# Sentry Settings
|
||||
@@ -328,12 +327,10 @@ USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
|
||||
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", 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"
|
||||
)
|
||||
|
||||
# Skip environment variable configuration
|
||||
SKIP_ENV_VAR = os.environ.get("SKIP_ENV_VAR", "1") == "1"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build": "tsup --minify",
|
||||
"dev": "tsup --watch",
|
||||
"check-types": "tsc --noEmit",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
@@ -80,4 +80,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build": "tsup --minify",
|
||||
"dev": "tsup --watch",
|
||||
"check-types": "tsc --noEmit",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
@@ -63,4 +63,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build": "tsup --minify",
|
||||
"dev": "tsup --watch",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
@@ -57,4 +57,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build": "tsup --minify",
|
||||
"dev": "tsup --watch",
|
||||
"check-types": "tsc --noEmit",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
@@ -52,4 +52,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build": "tsup --minify",
|
||||
"dev": "tsup --watch",
|
||||
"check-types": "tsc --noEmit",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
@@ -55,4 +55,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build": "tsup --minify",
|
||||
"dev": "tsup --watch",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
@@ -48,4 +48,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts --external react",
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts --external react --minify",
|
||||
"dev": "tsup src/index.ts --format esm,cjs --watch --dts --external react",
|
||||
"lint": "eslint src/",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||
@@ -38,4 +38,4 @@
|
||||
"react-color": "^2.19.3",
|
||||
"react-popper": "^2.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,4 @@ cp ./space/.env.example ./space/.env
|
||||
cp ./apiserver/.env.example ./apiserver/.env
|
||||
|
||||
# Generate the SECRET_KEY that will be used by django
|
||||
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||
echo "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||
@@ -1,8 +1,9 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const path = require("path");
|
||||
const withImages = require("next-images");
|
||||
require("dotenv").config({ path: ".env" });
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
const nextConfig = {
|
||||
basePath: process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? "/spaces" : "",
|
||||
reactStrictMode: false,
|
||||
swcMinify: true,
|
||||
images: {
|
||||
@@ -11,12 +12,8 @@ const nextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
if (parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) {
|
||||
const nextConfigWithNginx = withImages({
|
||||
basePath: "/spaces",
|
||||
...nextConfig,
|
||||
});
|
||||
module.exports = nextConfigWithNginx;
|
||||
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0")) {
|
||||
module.exports = withSentryConfig(nextConfig, { silent: true }, { hideSourceMaps: true });
|
||||
} else {
|
||||
module.exports = nextConfig;
|
||||
}
|
||||
|
||||
+2
-1
@@ -21,15 +21,16 @@
|
||||
"@plane/lite-text-editor": "*",
|
||||
"@plane/rich-text-editor": "*",
|
||||
"@plane/ui": "*",
|
||||
"@sentry/nextjs": "^7.85.0",
|
||||
"axios": "^1.3.4",
|
||||
"clsx": "^2.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lowlight": "^2.9.0",
|
||||
"lucide-react": "^0.293.0",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react-lite": "^4.0.3",
|
||||
"next": "^14.0.3",
|
||||
"next-images": "^1.8.5",
|
||||
"next-themes": "^0.2.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file configures the initialization of Sentry on the browser.
|
||||
// The config you add here will be used whenever a page is visited.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever middleware or an Edge route handles a request.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
defaults.url=https://sentry.io/
|
||||
defaults.org=plane
|
||||
defaults.project=plane-space
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever the server handles a request.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
@@ -24,7 +24,10 @@ const cycleService = new CycleService();
|
||||
export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
|
||||
// store
|
||||
const { cycle: cycleStore, trackEvent: { postHogEventTracker } } = useMobxStore();
|
||||
const {
|
||||
cycle: cycleStore,
|
||||
trackEvent: { postHogEventTracker },
|
||||
} = useMobxStore();
|
||||
// states
|
||||
const [activeProject, setActiveProject] = useState<string>(projectId);
|
||||
// toast
|
||||
@@ -41,26 +44,20 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
title: "Success!",
|
||||
message: "Cycle created successfully.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"CYCLE_CREATE",
|
||||
{
|
||||
...res,
|
||||
state: "SUCCESS",
|
||||
}
|
||||
);
|
||||
postHogEventTracker("CYCLE_CREATE", {
|
||||
...res,
|
||||
state: "SUCCESS",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Error in creating cycle. Please try again.",
|
||||
message: err.detail ?? "Error in creating cycle. Please try again.",
|
||||
});
|
||||
postHogEventTracker("CYCLE_CREATE", {
|
||||
state: "FAILED",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"CYCLE_CREATE",
|
||||
{
|
||||
state: "FAILED",
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -76,11 +73,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
message: "Cycle updated successfully.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Error in updating cycle. Please try again.",
|
||||
message: err.detail ?? "Error in updating cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -42,6 +43,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
cycleIssuesFilter: { issueFilters, updateFilters },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout;
|
||||
@@ -99,6 +101,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const cyclesList = cycleStore.projectCycles;
|
||||
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -190,16 +195,18 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||
|
||||
@@ -8,15 +8,24 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const CyclesHeader: FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// store
|
||||
const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
|
||||
const {
|
||||
project: projectStore,
|
||||
user: { currentProjectRole },
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
|
||||
const canUserCreateCycle =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex w-full flex-shrink-0 flex-row items-center justify-between h-[3.75rem] gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@@ -46,20 +55,21 @@ export const CyclesHeader: FC = observer(() => {
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLES_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateCycleModal(true);
|
||||
}
|
||||
}
|
||||
>
|
||||
Add Cycle
|
||||
</Button>
|
||||
</div>
|
||||
{canUserCreateCycle && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLES_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateCycleModal(true);
|
||||
}}
|
||||
>
|
||||
Add Cycle
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -41,6 +42,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
trackEvent: { setTrackElement },
|
||||
projectLabel: { projectLabels },
|
||||
moduleIssuesFilter: { issueFilters, updateFilters },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const { currentProjectDetails } = projectStore;
|
||||
@@ -100,6 +102,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined;
|
||||
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -191,16 +196,18 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("MODULE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("MODULE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||
|
||||
@@ -11,17 +11,25 @@ import { Breadcrumbs, Button, Tooltip, DiceIcon } from "@plane/ui";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// constants
|
||||
import { MODULE_VIEW_LAYOUTS } from "constants/module";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ModulesListHeader: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// store
|
||||
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const {
|
||||
project: projectStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
|
||||
const { storedValue: modulesView, setValue: setModulesView } = useLocalStorage("modules_view", "grid");
|
||||
|
||||
const canUserCreateModule =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex w-full flex-shrink-0 flex-row items-center justify-between h-[3.75rem] gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@@ -72,14 +80,16 @@ export const ModulesListHeader: React.FC = observer(() => {
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => commandPaletteStore.toggleCreateModuleModal(true)}
|
||||
>
|
||||
Add Module
|
||||
</Button>
|
||||
{canUserCreateModule && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => commandPaletteStore.toggleCreateModuleModal(true)}
|
||||
>
|
||||
Add Module
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -36,6 +37,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
// issue filters
|
||||
projectIssuesFilter: { issueFilters, updateFilters },
|
||||
projectIssues: {},
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
@@ -87,6 +89,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -200,16 +205,18 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
@@ -16,7 +17,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { Plus } from "lucide-react";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
@@ -35,6 +36,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
viewIssuesFilter: { issueFilters, updateFilters },
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
@@ -85,6 +87,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined;
|
||||
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<div className="relative w-full flex items-center z-10 h-[3.75rem] justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -170,16 +175,18 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
{
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -266,11 +266,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be created. Please try again.",
|
||||
message: err.detail ?? "Issue could not be created. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"ISSUE_CREATED",
|
||||
@@ -312,11 +312,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
|
||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be created. Please try again.",
|
||||
message: err.detail ?? "Issue could not be created. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -347,11 +347,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be updated. Please try again.",
|
||||
message: err.detail ?? "Issue could not be updated. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"ISSUE_UPDATED",
|
||||
|
||||
@@ -32,7 +32,11 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
||||
|
||||
const [activeProject, setActiveProject] = useState<string | null>(null);
|
||||
|
||||
const { project: projectStore, module: moduleStore, trackEvent: { postHogEventTracker } } = useMobxStore();
|
||||
const {
|
||||
project: projectStore,
|
||||
module: moduleStore,
|
||||
trackEvent: { postHogEventTracker },
|
||||
} = useMobxStore();
|
||||
|
||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
||||
|
||||
@@ -60,26 +64,20 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
||||
title: "Success!",
|
||||
message: "Module created successfully.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"MODULE_CREATED",
|
||||
{
|
||||
...res,
|
||||
state: "SUCCESS"
|
||||
}
|
||||
);
|
||||
postHogEventTracker("MODULE_CREATED", {
|
||||
...res,
|
||||
state: "SUCCESS",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Module could not be created. Please try again.",
|
||||
message: err.detail ?? "Module could not be created. Please try again.",
|
||||
});
|
||||
postHogEventTracker("MODULE_CREATED", {
|
||||
state: "FAILED",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"MODULE_CREATED",
|
||||
{
|
||||
state: "FAILED"
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -96,26 +94,20 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
||||
title: "Success!",
|
||||
message: "Module updated successfully.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"MODULE_UPDATED",
|
||||
{
|
||||
...res,
|
||||
state: "SUCCESS"
|
||||
}
|
||||
);
|
||||
postHogEventTracker("MODULE_UPDATED", {
|
||||
...res,
|
||||
state: "SUCCESS",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Module could not be updated. Please try again.",
|
||||
message: err.detail ?? "Module could not be updated. Please try again.",
|
||||
});
|
||||
postHogEventTracker("MODULE_UPDATED", {
|
||||
state: "FAILED",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"MODULE_UPDATED",
|
||||
{
|
||||
state: "FAILED"
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
const {
|
||||
page: { createPage, updatePage },
|
||||
trackEvent: { postHogEventTracker },
|
||||
workspace: { currentWorkspace }
|
||||
workspace: { currentWorkspace },
|
||||
} = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -56,24 +56,25 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
gorupId: currentWorkspace?.id!
|
||||
gorupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Page could not be created. Please try again.",
|
||||
message: err.detail ?? "Page could not be created. Please try again.",
|
||||
});
|
||||
postHogEventTracker("PAGE_CREATED",
|
||||
postHogEventTracker(
|
||||
"PAGE_CREATED",
|
||||
{
|
||||
state: "FAILED",
|
||||
},
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
gorupId: currentWorkspace?.id!
|
||||
gorupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -90,7 +91,8 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
title: "Success!",
|
||||
message: "Page updated successfully.",
|
||||
});
|
||||
postHogEventTracker("PAGE_UPDATED",
|
||||
postHogEventTracker(
|
||||
"PAGE_UPDATED",
|
||||
{
|
||||
...res,
|
||||
state: "SUCCESS",
|
||||
@@ -98,24 +100,25 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
gorupId: currentWorkspace?.id!
|
||||
gorupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Page could not be updated. Please try again.",
|
||||
message: err.detail ?? "Page could not be updated. Please try again.",
|
||||
});
|
||||
postHogEventTracker("PAGE_UPDATED",
|
||||
postHogEventTracker(
|
||||
"PAGE_UPDATED",
|
||||
{
|
||||
state: "FAILED",
|
||||
},
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
gorupId: currentWorkspace?.id!
|
||||
gorupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug } = router.query;
|
||||
// store
|
||||
const {
|
||||
workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation },
|
||||
workspaceMember: { removeMember, updateMember, updateMemberInvitation, deleteWorkspaceInvitation },
|
||||
user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings, leaveWorkspace },
|
||||
} = useMobxStore();
|
||||
// states
|
||||
@@ -206,15 +206,26 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
||||
onChange={(value: TUserWorkspaceRole | undefined) => {
|
||||
if (!workspaceSlug || !value) return;
|
||||
|
||||
updateMember(workspaceSlug.toString(), member.id, {
|
||||
role: value,
|
||||
}).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "An error occurred while updating member role. Please try again.",
|
||||
if (!member?.status)
|
||||
updateMemberInvitation(workspaceSlug.toString(), member.id, {
|
||||
role: value,
|
||||
}).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
else
|
||||
updateMember(workspaceSlug.toString(), member.id, {
|
||||
role: value,
|
||||
}).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
});
|
||||
}}
|
||||
disabled={!hasRoleChangeAccess}
|
||||
placement="bottom-end"
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@
|
||||
"@plane/rich-text-editor": "*",
|
||||
"@plane/ui": "*",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/nextjs": "^7.36.0",
|
||||
"@sentry/nextjs": "^7.85.0",
|
||||
"axios": "^1.1.3",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
@@ -63,9 +63,9 @@
|
||||
"@types/node": "18.0.6",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-datepicker": "^4.8.0",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
"@typescript-eslint/parser": "^5.48.2",
|
||||
|
||||
@@ -166,6 +166,18 @@ export class WorkspaceService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateWorkspaceInvitation(
|
||||
workspaceSlug: string,
|
||||
invitationId: string,
|
||||
data: Partial<IWorkspaceMember>
|
||||
): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteWorkspaceInvitations(workspaceSlug: string, invitationId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/`)
|
||||
.then((response) => response?.data)
|
||||
|
||||
@@ -19,6 +19,11 @@ export interface IWorkspaceMemberStore {
|
||||
updateMember: (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => Promise<void>;
|
||||
removeMember: (workspaceSlug: string, memberId: string) => Promise<void>;
|
||||
inviteMembersToWorkspace: (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => Promise<any>;
|
||||
updateMemberInvitation: (
|
||||
workspaceSlug: string,
|
||||
memberId: string,
|
||||
data: Partial<IWorkspaceMemberInvitation>
|
||||
) => Promise<void>;
|
||||
deleteWorkspaceInvitation: (workspaceSlug: string, memberId: string) => Promise<void>;
|
||||
// computed
|
||||
workspaceMembers: IWorkspaceMember[] | null;
|
||||
@@ -53,6 +58,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
updateMember: action,
|
||||
removeMember: action,
|
||||
inviteMembersToWorkspace: action,
|
||||
updateMemberInvitation: action,
|
||||
deleteWorkspaceInvitation: action,
|
||||
// computed
|
||||
workspaceMembers: computed,
|
||||
@@ -183,6 +189,55 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* update workspace member invitation using workspace slug and member id and data
|
||||
* @param workspaceSlug
|
||||
* @param memberId
|
||||
* @param data
|
||||
*/
|
||||
updateMemberInvitation = async (
|
||||
workspaceSlug: string,
|
||||
memberId: string,
|
||||
data: Partial<IWorkspaceMemberInvitation>
|
||||
) => {
|
||||
const originalMemberInvitations = [...this.memberInvitations?.[workspaceSlug]]; // in case of error, we will revert back to original members
|
||||
|
||||
const memberInvitations = [...this.memberInvitations?.[workspaceSlug]];
|
||||
|
||||
const index = memberInvitations.findIndex((m) => m.id === memberId);
|
||||
memberInvitations[index] = { ...memberInvitations[index], ...data };
|
||||
|
||||
// optimistic update
|
||||
runInAction(() => {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
this.memberInvitations = {
|
||||
...this.memberInvitations,
|
||||
[workspaceSlug]: memberInvitations,
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
await this.workspaceService.updateWorkspaceInvitation(workspaceSlug, memberId, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
this.memberInvitations = {
|
||||
...this.memberInvitations,
|
||||
[workspaceSlug]: originalMemberInvitations,
|
||||
};
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* delete the workspace invitation
|
||||
* @param workspaceSlug
|
||||
|
||||
@@ -2249,7 +2249,7 @@
|
||||
"@sentry/utils" "7.85.0"
|
||||
localforage "^1.8.1"
|
||||
|
||||
"@sentry/nextjs@^7.36.0":
|
||||
"@sentry/nextjs@^7.85.0":
|
||||
version "7.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.85.0.tgz#ac9d736b4fcf797864e6a8102285701096782e51"
|
||||
integrity sha512-EmSEEW2JUG/agq3o0W+4TWElyRWE01t80cBMPc7DMo24UdC+WLSgONE45iClkN1dou9xgroJRwTk9rPe+6l+4A==
|
||||
@@ -2797,7 +2797,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.39", "@types/react@^18.2.42":
|
||||
"@types/react@*", "@types/react@^18.2.39", "@types/react@^18.2.42":
|
||||
version "18.2.42"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
||||
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
||||
@@ -4002,7 +4002,7 @@ dotenv@16.0.3:
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
|
||||
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
|
||||
|
||||
dotenv@^16.0.3:
|
||||
dotenv@^16.0.3, dotenv@^16.3.1:
|
||||
version "16.3.1"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
|
||||
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
|
||||
@@ -4910,14 +4910,6 @@ file-entry-cache@^6.0.1:
|
||||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
|
||||
file-loader@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
|
||||
integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==
|
||||
dependencies:
|
||||
loader-utils "^2.0.0"
|
||||
schema-utils "^3.0.0"
|
||||
|
||||
file-selector@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc"
|
||||
@@ -6413,7 +6405,7 @@ mime-db@1.52.0:
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12, mime-types@^2.1.27:
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
@@ -6517,14 +6509,6 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
next-images@^1.8.5:
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/next-images/-/next-images-1.8.5.tgz#2eb5535bb1d6c58a5c4e03bc3be6c72c8a053a45"
|
||||
integrity sha512-YLBERp92v+Nu2EVxI9+wa32KRuxyxTC8ItbiHUWVPlatUoTl0yRqsNtP39c2vYv27VRvY4LlYcUGjNRBSMUIZA==
|
||||
dependencies:
|
||||
file-loader "^6.2.0"
|
||||
url-loader "^4.1.0"
|
||||
|
||||
next-pwa@^5.6.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.6.0.tgz#f7b1960c4fdd7be4253eb9b41b612ac773392bf4"
|
||||
@@ -7762,7 +7746,7 @@ schema-utils@^2.6.5:
|
||||
ajv "^6.12.4"
|
||||
ajv-keywords "^3.5.2"
|
||||
|
||||
schema-utils@^3.0.0, schema-utils@^3.1.1:
|
||||
schema-utils@^3.1.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe"
|
||||
integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==
|
||||
@@ -8737,15 +8721,6 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
url-loader@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2"
|
||||
integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==
|
||||
dependencies:
|
||||
loader-utils "^2.0.0"
|
||||
mime-types "^2.1.27"
|
||||
schema-utils "^3.0.0"
|
||||
|
||||
use-callback-ref@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5"
|
||||
|
||||
Reference in New Issue
Block a user