Compare commits

...

5 Commits

Author SHA1 Message Date
LAKHAN BAHETI 24f993dbee fix: proper toast message for lower roles 2023-12-08 16:18:23 +05:30
LAKHAN BAHETI 1ff0538077 chore: access control to create issue, cycle, module, pages 2023-12-07 17:47:35 +05:30
Nikhil 0e055666e7 dev: instance refactor (#3015)
* dev: remove license engine communication

* dev: remove license engine base url

* dev: update instance configuration function

* chore: removed the print statement

* chore: changed config variables

* chore: cleanup

* chore: added SKIP_ENV_VAR

* chore: changed the EMAIL_FROM

* dev: patch endpoint for workspace

* dev: custom port for takeoff script

* chore: changed my sequence

* fix: update operaton for member invitations in workspace

* clean-up: remove logs

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-12-07 14:51:27 +05:30
sriram veeraghanta 1b98b65a80 fix: adding sentry configs on space app and updated docker ignore (#3018) 2023-12-07 14:43:33 +05:30
rahulramesha 03c8aeed57 Chore: Minify build for plane packages (#3017)
* minify @plane/ui build

* minify all the packages
2023-12-07 14:26:17 +05:30
50 changed files with 756 additions and 736 deletions
+13 -2
View File
@@ -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/
+1 -1
View File
@@ -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):
+1
View File
@@ -65,6 +65,7 @@ urlpatterns = [
{
"delete": "destroy",
"get": "retrieve",
"patch": "partial_update",
}
),
name="workspace-invitations",
+44 -52
View File
@@ -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(
+40 -37
View File
@@ -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
+80 -69
View File
@@ -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)
+28 -15
View File
@@ -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 = {
+40 -41
View File
@@ -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,
+8 -9
View File
@@ -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,
+61 -33
View File
@@ -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),
-4
View File
@@ -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(
+63 -55
View File
@@ -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>"),
},
]
)
)
+3 -6
View File
@@ -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"
+2 -2
View File
@@ -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"
]
}
}
+2 -2
View File
@@ -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"
]
}
}
+2 -2
View File
@@ -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"
]
}
}
+2 -2
View File
@@ -17,7 +17,7 @@
}
},
"scripts": {
"build": "tsup",
"build": "tsup --minify",
"dev": "tsup --watch",
"check-types": "tsc --noEmit"
},
@@ -48,4 +48,4 @@
"nextjs",
"react"
]
}
}
+2 -2
View File
@@ -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"
}
}
}
+1 -1
View File
@@ -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
+5 -8
View File
@@ -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
View File
@@ -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",
+18
View File
@@ -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
});
+18
View File
@@ -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
});
+3
View File
@@ -0,0 +1,3 @@
defaults.url=https://sentry.io/
defaults.org=plane
defaults.project=plane-space
+18
View File
@@ -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
});
+15 -18
View File
@@ -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.",
});
});
};
+17 -10
View File
@@ -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"
+25 -15
View File
@@ -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>
);
});
+17 -10
View File
@@ -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"
+19 -9
View File
@@ -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>
);
+17 -10
View File
@@ -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>
</>
+18 -11
View File
@@ -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>
);
+6 -6
View File
@@ -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",
+23 -31
View File
@@ -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
View File
@@ -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",
+12
View File
@@ -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
+5 -30
View File
@@ -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"