Compare commits

..
71 changed files with 1739 additions and 2427 deletions
+1
View File
@@ -6,6 +6,7 @@ on:
branches:
- master
- preview
- develop
release:
types: [released, prereleased]
+1 -2
View File
@@ -1,9 +1,8 @@
name: "CodeQL"
on:
workflow_dispatch:
push:
branches: ["develop", "preview", "master"]
branches: ["master"]
pull_request:
branches: ["develop", "preview", "master"]
schedule:
+1 -1
View File
@@ -52,7 +52,7 @@ If you want more control over your data, prefer to self-host Plane, please refer
| Installation Methods | Documentation Link |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/self-hosting/methods/docker-compose) |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.17.0"
"version": "0.16.0"
}
+15 -16
View File
@@ -1,33 +1,32 @@
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from lxml import html
# Django imports
from django.utils import timezone
from lxml import html
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
# Third party imports
from rest_framework import serializers
# Module imports
from plane.db.models import (
User,
Issue,
IssueActivity,
State,
IssueAssignee,
IssueAttachment,
IssueComment,
Label,
IssueLabel,
IssueLink,
Label,
IssueComment,
IssueAttachment,
IssueActivity,
ProjectMember,
State,
User,
)
from .base import BaseSerializer
from .cycle import CycleLiteSerializer, CycleSerializer
from .module import ModuleLiteSerializer, ModuleSerializer
from .state import StateLiteSerializer
from .cycle import CycleSerializer, CycleLiteSerializer
from .module import ModuleSerializer, ModuleLiteSerializer
from .user import UserLiteSerializer
from .state import StateLiteSerializer
class IssueSerializer(BaseSerializer):
@@ -80,7 +79,7 @@ class IssueSerializer(BaseSerializer):
data["description_html"] = parsed_str
except Exception as e:
raise serializers.ValidationError("Invalid HTML passed")
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
# Validate assignees are from project
if data.get("assignees", []):
@@ -295,7 +294,7 @@ class IssueLinkSerializer(BaseSerializer):
raise serializers.ValidationError("Invalid URL format.")
# Check URL scheme
if not value.startswith(("http://", "https://")):
if not value.startswith(('http://', 'https://')):
raise serializers.ValidationError("Invalid URL scheme.")
return value
@@ -367,7 +366,7 @@ class IssueCommentSerializer(BaseSerializer):
data["comment_html"] = parsed_str
except Exception as e:
raise serializers.ValidationError("Invalid HTML passed")
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
return data
+16 -18
View File
@@ -1,33 +1,31 @@
from django.urls import path
from plane.app.views import (
UserWorkspaceInvitationsViewSet,
WorkSpaceViewSet,
WorkspaceJoinEndpoint,
WorkSpaceMemberViewSet,
WorkspaceInvitationsViewset,
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
WorkSpaceAvailabilityCheckEndpoint,
ExportWorkspaceUserActivityEndpoint,
TeamMemberViewSet,
UserLastProjectWithWorkspaceEndpoint,
UserWorkspaceInvitationsViewSet,
WorkSpaceAvailabilityCheckEndpoint,
WorkspaceCyclesEndpoint,
WorkspaceEstimatesEndpoint,
WorkspaceInvitationsViewset,
WorkspaceJoinEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
WorkSpaceMemberViewSet,
WorkspaceModulesEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceStatesEndpoint,
WorkspaceThemeViewSet,
WorkspaceUserProfileStatsEndpoint,
WorkspaceUserActivityEndpoint,
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserProfileStatsEndpoint,
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
ExportWorkspaceUserActivityEndpoint,
WorkspaceModulesEndpoint,
WorkspaceCyclesEndpoint,
WorkSpaceViewSet,
)
urlpatterns = [
path(
"workspace-slug-check/",
+1 -1
View File
@@ -38,7 +38,7 @@ from .workspace.base import (
WorkSpaceAvailabilityCheckEndpoint,
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,
ExportWorkspaceUserActivityEndpoint
ExportWorkspaceUserActivityEndpoint,
)
from .workspace.member import (
+107 -79
View File
@@ -1,56 +1,56 @@
# Python imports
import json
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
# Django imports
from django.db.models import (
Case,
CharField,
Count,
Exists,
F,
Func,
OuterRef,
Prefetch,
F,
Q,
UUIDField,
Value,
Exists,
OuterRef,
Count,
Prefetch,
Case,
When,
Value,
CharField,
Subquery,
IntegerField,
)
from django.db.models.functions import Coalesce
from django.utils import timezone
from rest_framework import status
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import UUIDField
from django.db.models.functions import Coalesce
# Third party imports
from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet, BaseAPIView, WebhookMixin
from plane.app.serializers import (
CycleSerializer,
CycleFavoriteSerializer,
CycleWriteSerializer,
CycleUserPropertiesSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
ProjectLitePermission,
)
from plane.app.serializers import (
CycleFavoriteSerializer,
CycleSerializer,
CycleUserPropertiesSerializer,
CycleWriteSerializer,
from plane.db.models import (
User,
Cycle,
CycleIssue,
Issue,
CycleFavorite,
Label,
CycleUserProperties,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.db.models import (
Cycle,
CycleFavorite,
CycleIssue,
CycleUserProperties,
Issue,
Label,
User,
)
from plane.utils.analytics_plot import burndown_plot
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
class CycleViewSet(WebhookMixin, BaseViewSet):
serializer_class = CycleSerializer
@@ -73,6 +73,60 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
cancelled_issues = (
Issue.issue_objects.filter(
state__group="cancelled",
issue_cycle__cycle_id=OuterRef("pk"),
)
.values("issue_cycle__cycle_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
completed_issues = (
Issue.issue_objects.filter(
state__group="completed",
issue_cycle__cycle_id=OuterRef("pk"),
)
.values("issue_cycle__cycle_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
started_issues = (
Issue.issue_objects.filter(
state__group="started",
issue_cycle__cycle_id=OuterRef("pk"),
)
.values("issue_cycle__cycle_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
unstarted_issues = (
Issue.issue_objects.filter(
state__group="unstarted",
issue_cycle__cycle_id=OuterRef("pk"),
)
.values("issue_cycle__cycle_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
backlog_issues = (
Issue.issue_objects.filter(
state__group="backlog",
issue_cycle__cycle_id=OuterRef("pk"),
)
.values("issue_cycle__cycle_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
total_issues = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=OuterRef("pk"),
)
.values("issue_cycle__cycle_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
return self.filter_queryset(
super()
.get_queryset()
@@ -102,68 +156,39 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(is_favorite=Exists(favorite_subquery))
.annotate(
total_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
completed_issues=Coalesce(
Subquery(completed_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
completed_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
cancelled_issues=Coalesce(
Subquery(cancelled_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
started_issues=Coalesce(
Subquery(started_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
unstarted_issues=Coalesce(
Subquery(unstarted_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
backlog_issues=Coalesce(
Subquery(backlog_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
total_issues=Coalesce(
Subquery(total_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
@@ -192,6 +217,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
distinct=True,
filter=~Q(
issue_cycle__issue__assignees__id__isnull=True
)
& Q(
issue_cycle__issue__assignees__member_project__is_active=True
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -356,8 +384,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"external_id",
"progress_snapshot",
# meta fields
"is_favorite",
"total_issues",
"is_favorite",
"cancelled_issues",
"completed_issues",
"started_issues",
+36 -41
View File
@@ -1,57 +1,54 @@
# Python imports
import json
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import (
Count,
Exists,
F,
Func,
IntegerField,
OuterRef,
Prefetch,
Q,
Subquery,
UUIDField,
Value,
)
from django.db.models.functions import Coalesce
# Django Imports
from django.utils import timezone
from rest_framework import status
from django.db.models import (
Prefetch,
F,
OuterRef,
Exists,
Count,
Q,
Func,
Subquery,
IntegerField,
)
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import Value, UUIDField
from django.db.models.functions import Coalesce
# Third party imports
from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet, BaseAPIView, WebhookMixin
from plane.app.serializers import (
ModuleWriteSerializer,
ModuleSerializer,
ModuleLinkSerializer,
ModuleFavoriteSerializer,
ModuleUserPropertiesSerializer,
ModuleDetailSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
ProjectLitePermission,
)
from plane.app.serializers import (
ModuleDetailSerializer,
ModuleFavoriteSerializer,
ModuleLinkSerializer,
ModuleSerializer,
ModuleUserPropertiesSerializer,
ModuleWriteSerializer,
from plane.db.models import (
Module,
ModuleIssue,
Project,
Issue,
ModuleLink,
ModuleFavorite,
ModuleUserProperties,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.db.models import (
Issue,
Module,
ModuleFavorite,
ModuleIssue,
ModuleLink,
ModuleUserProperties,
Project,
)
from plane.utils.analytics_plot import burndown_plot
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
class ModuleViewSet(WebhookMixin, BaseViewSet):
model = Module
@@ -395,11 +392,9 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"completion_chart": {},
}
# Fetch the modules
modules = queryset.first()
if modules and modules.start_date and modules.target_date:
if queryset.first().start_date and queryset.first().target_date:
data["distribution"]["completion_chart"] = burndown_plot(
queryset=modules,
queryset=queryset.first(),
slug=slug,
project_id=project_id,
module_id=pk,
@@ -3,10 +3,15 @@ from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.app.permissions import WorkspaceEntityPermission
from plane.app.serializers import WorkspaceEstimateSerializer
from plane.app.views.base import BaseAPIView
from plane.db.models import Estimate, Project
from plane.db.models import Project, Estimate
from plane.app.permissions import WorkspaceEntityPermission
# Django imports
from django.db.models import (
Prefetch,
)
from plane.utils.cache import cache_response
@@ -20,11 +25,15 @@ class WorkspaceEstimatesEndpoint(BaseAPIView):
estimate_ids = Project.objects.filter(
workspace__slug=slug, estimate__isnull=False
).values_list("estimate_id", flat=True)
estimates = (
Estimate.objects.filter(pk__in=estimate_ids, workspace__slug=slug)
.prefetch_related("points")
.select_related("workspace", "project")
estimates = Estimate.objects.filter(
pk__in=estimate_ids
).prefetch_related(
Prefetch(
"points",
queryset=Project.objects.select_related(
"estimate", "workspace", "project"
),
)
)
serializer = WorkspaceEstimateSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -0,0 +1,311 @@
# Python imports
import io
import json
import logging
import zipfile
# Third party imports
import boto3
from botocore.client import Config
from celery import shared_task
# Django imports
from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.serializers.json import DjangoJSONEncoder
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.html import strip_tags
# Module imports
from plane.db.models import (
APIToken,
CommentReaction,
Cycle,
CycleFavorite,
CycleIssue,
CycleUserProperties,
Estimate,
EstimatePoint,
FileAsset,
Inbox,
InboxIssue,
Issue,
IssueActivity,
IssueAssignee,
IssueAttachment,
IssueComment,
IssueLabel,
IssueLink,
IssueMention,
IssueProperty,
IssueReaction,
IssueRelation,
IssueSequence,
IssueSubscriber,
IssueView,
IssueViewFavorite,
IssueVote,
Label,
Module,
ModuleFavorite,
ModuleIssue,
ModuleLink,
ModuleMember,
ModuleUserProperties,
Notification,
Page,
PageFavorite,
PageLabel,
PageLog,
Project,
ProjectDeployBoard,
ProjectFavorite,
ProjectIdentifier,
ProjectMember,
ProjectMemberInvite,
ProjectPublicMember,
State,
User,
UserNotificationPreference,
Webhook,
Workspace,
WorkspaceMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
)
from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
def create_zip_file(files):
# Create zip
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
for file in files:
filename = file.get("filename")
file_content = file.get("data")
zipf.writestr(filename, file_content)
zip_buffer.seek(0)
return zip_buffer
def upload_to_s3(zip_file, workspace_id, slug):
# Upload the zip to s3
file_name = f"{workspace_id}/export-{slug}-{timezone.now()}.zip"
expires_in = 7 * 24 * 60 * 60
if settings.USE_MINIO:
s3 = boto3.client(
"s3",
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
s3.upload_fileobj(
zip_file,
settings.AWS_STORAGE_BUCKET_NAME,
file_name,
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
)
presigned_url = s3.generate_presigned_url(
"get_object",
Params={
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Key": file_name,
},
ExpiresIn=expires_in,
)
# Create the new url with updated domain and protocol
presigned_url = presigned_url.replace(
f"{settings.AWS_S3_ENDPOINT_URL}/{settings.AWS_STORAGE_BUCKET_NAME}/",
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/",
)
else:
s3 = boto3.client(
"s3",
region_name=settings.AWS_REGION,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
s3.upload_fileobj(
zip_file,
settings.AWS_STORAGE_BUCKET_NAME,
file_name,
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
)
presigned_url = s3.generate_presigned_url(
"get_object",
Params={
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Key": file_name,
},
ExpiresIn=expires_in,
)
return presigned_url
@shared_task
def workspace_export(workspace_id, email):
# Get the workspace
workspace = Workspace.objects.get(pk=workspace_id)
slug = workspace.slug
# Store all files
files = []
# Users that need to be exported
emails = WorkspaceMember.objects.filter(
workspace_id=workspace_id
).values_list("member__email", flat=True)
users = User.objects.filter(email__in=emails).values()
users_json = json.dumps(list(users), cls=DjangoJSONEncoder)
files.append({"filename": "users.json", "data": users_json})
workspace = list(Workspace.objects.filter(pk=workspace_id).values())
workspace_json = json.dumps(workspace, cls=DjangoJSONEncoder)
files.append({"filename": "workspaces.json", "data": workspace_json})
models = {
# Workspace
WorkspaceMemberInvite: "workspace_member_invites.json",
WorkspaceMember: "workspace_members.json",
WorkspaceTheme: "workspace_themes.json",
WorkspaceUserProperties: "workspace_user_properties.json",
# Projects
Project: "projects.json",
ProjectDeployBoard: "project_deploy_boards.json",
ProjectFavorite: "project_favorites.json",
ProjectIdentifier: "project_identifier.json",
ProjectMember: "project_members.json",
ProjectMemberInvite: "project_member_invites.json",
ProjectPublicMember: "project_public_members.json",
# APIToken
APIToken: "api_tokens.json",
# Assets
FileAsset: "file_assets.json",
# States
State: "states.json",
# Issues
Issue: "issues.json",
IssueAssignee: "issue_assignees.json",
Label: "labels.json",
IssueLabel: "issue_labels.json",
IssueLink: "issue_links.json",
IssueMention: "issue_mention.json",
IssueVote: "issue_votes.json",
IssueSubscriber: "issue_subscribers.json",
IssueProperty: "issue_properties.json",
IssueSequence: "issue_sequences.json",
IssueReaction: "issue_reactions.json",
IssueRelation: "issue_relations.json",
IssueAttachment: "issue_attachments.json",
IssueActivity: "issue_activities.json",
CommentReaction: "comment_reactions.json",
IssueComment: "issue_comments.json",
# Cycles
Cycle: "cycles.json",
CycleIssue: "cycle_issues.json",
CycleFavorite: "cycle_favorites.json",
CycleUserProperties: "cycle_user_properties.json",
# Modules
Module: "modules.json",
ModuleIssue: "module_issues.json",
ModuleFavorite: "module_favorites.json",
ModuleLink: "module_links.json",
ModuleMember: "module_members.json",
ModuleUserProperties: "module_user_properties.json",
# Page
Page: "pages.json",
PageLog: "page_logs.json",
PageLabel: "page_labels.json",
PageFavorite: "page_favorites.json",
# Estimate
Estimate: "estimates.json",
EstimatePoint: "estimate_points.json",
# Webhook
Webhook: "webhooks.json",
# Views
IssueView: "views.json",
IssueViewFavorite: "view_favorites.json",
# Notification
Notification: "notifications.json",
UserNotificationPreference: "user_notification_preferences.json",
# Inbox
Inbox: "inboxes.json",
InboxIssue: "inbox_issues.json",
}
# Loop through the models
for model in models:
file_name = models[model]
files.append(
{
"filename": file_name,
"data": json.dumps(
list(
model.objects.filter(
workspace_id=workspace_id
).values()
),
cls=DjangoJSONEncoder,
),
}
)
# Create zip
zip_buffer = create_zip_file(files)
# Get the presigned url
url = upload_to_s3(
workspace_id=workspace_id, slug=slug, zip_file=zip_buffer
)
# Send mail
try:
(
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_FROM,
) = get_email_configuration()
# Send the mail
subject = "Your Plane Export Link"
context = {"url": url, "email": email}
html_content = render_to_string(
"emails/exports/workspace_exports.html", context
)
text_content = strip_tags(html_content)
connection = get_connection(
host=EMAIL_HOST,
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
)
msg = EmailMultiAlternatives(
subject=subject,
body=text_content,
from_email=EMAIL_FROM,
to=[email],
connection=connection,
)
msg.attach_alternative(html_content, "text/html")
msg.send()
logging.getLogger("plane").info("Email sent successfully.")
return
except Exception as e:
log_exception(e)
return
@@ -0,0 +1,475 @@
# Python imports
import json
# Third party imports
from celery import shared_task
from plane.db.models import (
APIToken,
CommentReaction,
Cycle,
CycleFavorite,
CycleIssue,
CycleUserProperties,
Estimate,
EstimatePoint,
FileAsset,
Inbox,
InboxIssue,
Issue,
IssueActivity,
IssueAssignee,
IssueAttachment,
IssueComment,
IssueLabel,
IssueLink,
IssueMention,
IssueProperty,
IssueReaction,
IssueRelation,
IssueSequence,
IssueSubscriber,
IssueView,
IssueViewFavorite,
IssueVote,
Label,
Module,
ModuleFavorite,
ModuleIssue,
ModuleLink,
ModuleMember,
ModuleUserProperties,
Notification,
Page,
PageFavorite,
PageLabel,
PageLog,
Project,
ProjectDeployBoard,
ProjectFavorite,
ProjectIdentifier,
ProjectMember,
ProjectMemberInvite,
ProjectPublicMember,
State,
User,
UserNotificationPreference,
Webhook,
Workspace,
WorkspaceMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
)
from plane.utils.exception_logger import log_exception
def replace_model_data(fields, model_data, user_maps):
for field in fields:
model_data[field] = user_maps.get(
model_data.get(field), model_data.get(field)
)
return model_data
def data_transformer(model, model_data, user_maps):
# model name mapper
mapper = {
"workspace": [
"created_by_id",
"updated_by_id",
"owner_id",
],
"workspacememberinvite": [
"created_by_id",
"updated_by_id",
],
"workspacetheme": [
"created_by_id",
"updated_by_id",
],
"workspacemember": [
"created_by_id",
"updated_by_id",
"member_id",
],
"workspaceuserproperties": [
"created_by_id",
"updated_by_id",
"user_id",
],
"project": [
"created_by_id",
"updated_by_id",
"default_assignee_id",
"project_lead_id",
],
"projectdeployboard": [
"created_by_id",
"updated_by_id",
],
"projectfavorite": [
"user_id",
"created_by_id",
"updated_by_id",
],
"projectmember": [
"created_by_id",
"updated_by_id",
"member_id",
],
"projectidentifier": [
"created_by_id",
"updated_by_id",
],
"projectmemberinvite": [
"created_by_id",
"updated_by_id",
],
"projectpublicmember": [
"created_by_id",
"updated_by_id",
"member_id",
],
"state": [
"created_by_id",
"updated_by_id",
],
"label": [
"created_by_id",
"updated_by_id",
],
"estimate": [
"created_by_id",
"updated_by_id",
],
"estimatepoint": [
"created_by_id",
"updated_by_id",
],
"issue": [
"created_by_id",
"updated_by_id",
],
"issuecomment": [
"created_by_id",
"updated_by_id",
"actor_id",
],
"issueassignee": [
"created_by_id",
"updated_by_id",
"assignee_id",
],
"issuelabel": [
"created_by_id",
"updated_by_id",
],
"issuelink": [
"created_by_id",
"updated_by_id",
],
"issuemention": [
"created_by_id",
"updated_by_id",
"mention_id",
],
"issuevote": [
"created_by_id",
"updated_by_id",
"actor_id",
],
"issuesubscriber": [
"created_by_id",
"updated_by_id",
"subscriber_id",
],
"issueproperty": [
"created_by_id",
"updated_by_id",
"user_id",
],
"issuesequence": [
"created_by_id",
"updated_by_id",
],
"issuereaction": [
"created_by_id",
"updated_by_id",
"actor_id",
],
"issuerelation": [
"created_by_id",
"updated_by_id",
],
"issueattachment": [
"created_by_id",
"updated_by_id",
],
"issueactivity": [
"created_by_id",
"updated_by_id",
"actor_id",
],
"apitoken": [
"created_by_id",
"updated_by_id",
"user_id",
],
"fileasset": [
"created_by_id",
"updated_by_id",
],
"commentreaction": [
"created_by_id",
"updated_by_id",
"actor_id",
],
"cycle": [
"created_by_id",
"updated_by_id",
"owned_by_id",
],
"cycleissue": [
"created_by_id",
"updated_by_id",
],
"cyclefavorite": [
"user_id",
"created_by_id",
"updated_by_id",
],
"cycleuserproperties": [
"user_id",
"created_by_id",
"updated_by_id",
],
"module": [
"created_by_id",
"updated_by_id",
"lead_id",
],
"moduleissue": [
"created_by_id",
"updated_by_id",
],
"modulefavorite": [
"user_id",
"created_by_id",
"updated_by_id",
],
"modulelink": [
"created_by_id",
"updated_by_id",
],
"modulemember": [
"created_by_id",
"updated_by_id",
"member_id",
],
"moduleuserproperties": [
"user_id",
"created_by_id",
"updated_by_id",
],
"page": [
"created_by_id",
"updated_by_id",
"owned_by_id",
],
"pagelog": [
"created_by_id",
"updated_by_id",
],
"pagelabel": [
"created_by_id",
"updated_by_id",
],
"pagefavorite": [
"user_id",
"created_by_id",
"updated_by_id",
],
"webhook": [
"created_by_id",
"updated_by_id",
],
"issueview": [
"created_by_id",
"updated_by_id",
],
"issueviewfavorite": [
"user_id",
"created_by_id",
"updated_by_id",
],
"notification": [
"created_by_id",
"updated_by_id",
"triggered_by_id",
"receiver_id",
],
"usernotificationpreference": [
"user_id",
"created_by_id",
"updated_by_id",
],
"inbox": [
"created_by_id",
"updated_by_id",
],
"inboxissue": [
"created_by_id",
"updated_by_id",
],
}
# Get all the fields for the current model
fields = mapper[model._meta.model_name]
if fields:
model_data = replace_model_data(
fields=fields, model_data=model_data, user_maps=user_maps
)
# return modified model data
return model_data
# return model data
return model_data
@shared_task
def workspace_import(workspace_data):
try:
# Create Users
users = json.loads(workspace_data.get("users.json"))
# get user emails
user_emails = [user.get("email") for user in users]
imported_users = {user.get("email"): user.get("id") for user in users}
existing_users = User.objects.filter(email__in=user_emails).values(
"id", "email"
)
user_maps = {
imported_users.get(exuser.get("email")): str(exuser.get("id"))
for exuser in existing_users
if imported_users.get(exuser.get("email"))
}
User.objects.bulk_create(
[User(**user) for user in (users)],
batch_size=100,
ignore_conflicts=True,
)
# Workspaces
workspaces = workspace_data.get("workspaces.json")
Workspace.objects.bulk_create(
[
Workspace(
**data_transformer(
model=Workspace,
model_data=workspace,
user_maps=user_maps,
)
)
for workspace in json.loads(workspaces)
],
batch_size=100,
ignore_conflicts=True,
)
models = {
# Workspace
WorkspaceMemberInvite: "workspace_member_invites.json",
WorkspaceMember: "workspace_members.json",
WorkspaceTheme: "workspace_themes.json",
WorkspaceUserProperties: "workspace_user_properties.json",
# Projects
Project: "projects.json",
ProjectDeployBoard: "project_deploy_boards.json",
ProjectFavorite: "project_favorites.json",
ProjectIdentifier: "project_identifier.json",
ProjectMember: "project_members.json",
ProjectMemberInvite: "project_member_invites.json",
ProjectPublicMember: "project_public_members.json",
# States
State: "states.json",
# Labels
Label: "labels.json",
# Estimate
Estimate: "estimates.json",
EstimatePoint: "estimate_points.json",
# Issues
Issue: "issues.json",
IssueComment: "issue_comments.json",
IssueAssignee: "issue_assignees.json",
IssueLabel: "issue_labels.json",
IssueLink: "issue_links.json",
IssueMention: "issue_mention.json",
IssueVote: "issue_votes.json",
IssueSubscriber: "issue_subscribers.json",
IssueProperty: "issue_properties.json",
IssueSequence: "issue_sequences.json",
IssueReaction: "issue_reactions.json",
IssueRelation: "issue_relations.json",
IssueAttachment: "issue_attachments.json",
IssueActivity: "issue_activities.json",
# APIToken
APIToken: "api_tokens.json",
# Assets
FileAsset: "file_assets.json",
CommentReaction: "comment_reactions.json",
# Cycles
Cycle: "cycles.json",
CycleIssue: "cycle_issues.json",
CycleFavorite: "cycle_favorites.json",
CycleUserProperties: "cycle_user_properties.json",
# Modules
Module: "modules.json",
ModuleIssue: "module_issues.json",
ModuleFavorite: "module_favorites.json",
ModuleLink: "module_links.json",
ModuleMember: "module_members.json",
ModuleUserProperties: "module_user_properties",
# Page
Page: "pages.json",
PageLog: "page_logs.json",
PageLabel: "page_labels.json",
PageFavorite: "page_favorites.json",
# Webhook
Webhook: "webhooks.json",
# Views
IssueView: "views.json",
IssueViewFavorite: "view_favorites.json",
# Notification
Notification: "notifications.json",
UserNotificationPreference: "user_notification_preferences.json",
# Inbox
Inbox: "inboxes.json",
InboxIssue: "inbox_issues.json",
}
for model in models:
file_name = models[model]
data = workspace_data.get(file_name)
# Loop through all the models and create the records accordingly
model.objects.bulk_create(
[
model(
**data_transformer(
model=model,
model_data=model_data,
user_maps=user_maps,
)
)
for model_data in json.loads(data)
],
batch_size=100,
ignore_conflicts=True,
)
return
except Exception as e:
log_exception(e)
return
+1 -1
View File
@@ -1,6 +1,6 @@
# Django imports
from django.db import models
from django.conf import settings
from django.db import models
# Module imports
from . import ProjectBaseModel
+1 -2
View File
@@ -1,12 +1,11 @@
# Django imports
from django.db import models
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
# Module imports
from . import BaseModel
ROLE_CHOICES = (
(20, "Owner"),
(15, "Admin"),
@@ -4,4 +4,6 @@ from .instance import (
InstanceConfigurationEndpoint,
InstanceAdminSignInEndpoint,
SignUpScreenVisitedEndpoint,
ExportWorkspaceEndpoint,
ImportWorkspaceEndpoint,
)
+97 -10
View File
@@ -1,33 +1,39 @@
# Python imports
import uuid
import zipfile
# Django imports
from django.utils import timezone
from django.contrib.auth.hashers import make_password
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.utils import timezone
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
# Module imports
from plane.app.serializers.workspace import WorkspaceLiteSerializer
from plane.app.views import BaseAPIView
from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration
from plane.license.api.serializers import (
InstanceSerializer,
InstanceAdminSerializer,
InstanceConfigurationSerializer,
)
from plane.bgtasks.workspace_export_task import workspace_export
from plane.bgtasks.workspace_import_task import workspace_import
from plane.db.models import User, Workspace
from plane.license.api.permissions import (
InstanceAdminPermission,
)
from plane.db.models import User
from plane.license.api.serializers import (
InstanceAdminSerializer,
InstanceConfigurationSerializer,
InstanceSerializer,
)
from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration
from plane.license.utils.encryption import encrypt_data
from plane.utils.cache import cache_response, invalidate_cache
class InstanceEndpoint(BaseAPIView):
def get_permissions(self):
if self.request.method == "PATCH":
@@ -272,3 +278,84 @@ class SignUpScreenVisitedEndpoint(BaseAPIView):
instance.is_signup_screen_visited = True
instance.save()
return Response(status=status.HTTP_204_NO_CONTENT)
class InstanceWorkspacesEndpoint(BaseAPIView):
permission_classes = [
InstanceAdminPermission,
]
def get(self, request):
workspaces = Workspace.objects.all()
serializer = WorkspaceLiteSerializer(workspaces, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class ExportWorkspaceEndpoint(BaseAPIView):
permission_classes = [
InstanceAdminPermission,
]
def post(self, request):
workspace_id = request.data.get("workspace_id", False)
if not workspace_id:
return Response(
{"error": "Workspace ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace_export.delay(
workspace_id=workspace_id,
email=request.user.email,
)
return Response(
{
"message": "An email will be sent to download the exports when they are ready"
},
status=status.HTTP_200_OK,
)
class ImportWorkspaceEndpoint(BaseAPIView):
parser_classes = (
MultiPartParser,
FormParser,
JSONParser,
)
permission_classes = [
InstanceAdminPermission,
]
def post(self, request):
file_obj = request.FILES.get("zip_file")
if file_obj is None:
return Response(
"No file uploaded.", status=status.HTTP_400_BAD_REQUEST
)
# Ensure the uploaded file is a ZIP file
if not zipfile.is_zipfile(file_obj):
return Response(
"Uploaded file is not a valid zip file.",
status=status.HTTP_400_BAD_REQUEST,
)
# Reading contents of the ZIP file
file_contents = {}
with zipfile.ZipFile(file_obj, "r") as zip_ref:
for file_name in zip_ref.namelist():
with zip_ref.open(file_name) as file:
# Assuming the file content is text. Use file.read() for binary content.
content = file.read().decode("utf-8")
file_contents[file_name] = content
workspace_import.delay(workspace_data=file_contents)
return Response(
{"message": "Files processed.", "file_count": len(file_contents)},
status=status.HTTP_200_OK,
)
+14 -2
View File
@@ -1,10 +1,12 @@
from django.urls import path
from plane.license.api.views import (
InstanceEndpoint,
ExportWorkspaceEndpoint,
ImportWorkspaceEndpoint,
InstanceAdminEndpoint,
InstanceConfigurationEndpoint,
InstanceAdminSignInEndpoint,
InstanceConfigurationEndpoint,
InstanceEndpoint,
SignUpScreenVisitedEndpoint,
)
@@ -39,4 +41,14 @@ urlpatterns = [
SignUpScreenVisitedEndpoint.as_view(),
name="instance-sign-up",
),
path(
"export-workspace/",
ExportWorkspaceEndpoint.as_view(),
name="workspace-exports",
),
path(
"import-workspace/",
ImportWorkspaceEndpoint.as_view(),
name="workspace-imports",
),
]
@@ -0,0 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
Hey there,<br />
Your requested url {{ url }}
</html>
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -5,11 +5,11 @@ x-app-env: &app-env
- NGINX_PORT=${NGINX_PORT:-80}
- WEB_URL=${WEB_URL:-http://localhost}
- DEBUG=${DEBUG:-0}
- SENTRY_DSN=${SENTRY_DSN}
- SENTRY_DSN=${SENTRY_DSN:-""}
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS}
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-""}
# Gunicorn Workers
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-1}
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}
#DB SETTINGS
- PGHOST=${PGHOST:-plane-db}
- PGDATABASE=${PGDATABASE:-plane}
@@ -17,11 +17,11 @@ x-app-env: &app-env
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
- POSTGRES_DB=${POSTGRES_DB:-plane}
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
- DATABASE_URL=${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${PGHOST}/${PGDATABASE}}
# REDIS SETTINGS
- REDIS_HOST=${REDIS_HOST:-plane-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_URL=${REDIS_URL:-redis://plane-redis:6379/}
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
# Application secret
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
# DATA STORE SETTINGS
@@ -39,7 +39,7 @@ x-app-env: &app-env
services:
web:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-latest}
pull_policy: ${PULL_POLICY:-always}
restart: unless-stopped
command: /usr/local/bin/start.sh web/server.js web
@@ -51,7 +51,7 @@ services:
space:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-latest}
pull_policy: ${PULL_POLICY:-always}
restart: unless-stopped
command: /usr/local/bin/start.sh space/server.js space
@@ -64,7 +64,7 @@ services:
api:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest}
pull_policy: ${PULL_POLICY:-always}
restart: unless-stopped
command: ./bin/takeoff
@@ -78,7 +78,7 @@ services:
worker:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest}
pull_policy: ${PULL_POLICY:-always}
restart: unless-stopped
command: ./bin/worker
@@ -91,7 +91,7 @@ services:
beat-worker:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest}
pull_policy: ${PULL_POLICY:-always}
restart: unless-stopped
command: ./bin/beat
@@ -104,7 +104,7 @@ services:
migrator:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest}
pull_policy: ${PULL_POLICY:-always}
restart: no
command: >
@@ -118,7 +118,7 @@ services:
plane-db:
<<: *app-env
image: postgres:15.5-alpine
image: postgres:15.2-alpine
pull_policy: if_not_present
restart: unless-stopped
command: postgres -c 'max_connections=1000'
@@ -126,7 +126,7 @@ services:
- pgdata:/var/lib/postgresql/data
plane-redis:
<<: *app-env
image: redis:7.2.4-alpine
image: redis:6.2.7-alpine
pull_policy: if_not_present
restart: unless-stopped
volumes:
@@ -144,7 +144,7 @@ services:
# Comment this if you already have a reverse proxy running
proxy:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-latest}
pull_policy: ${PULL_POLICY:-always}
ports:
- ${NGINX_PORT}:80
+26 -54
View File
@@ -17,16 +17,16 @@ function print_header() {
clear
cat <<"EOF"
--------------------------------------------
____ _ /////////
| _ \| | __ _ _ __ ___ /////////
| |_) | |/ _` | '_ \ / _ \ ///// /////
| __/| | (_| | | | | __/ ///// /////
|_| |_|\__,_|_| |_|\___| ////
////
--------------------------------------------
---------------------------------------
____ _
| _ \| | __ _ _ __ ___
| |_) | |/ _` | '_ \ / _ \
| __/| | (_| | | | | __/
|_| |_|\__,_|_| |_|\___|
---------------------------------------
Project management tool from the future
--------------------------------------------
---------------------------------------
EOF
}
@@ -66,7 +66,7 @@ function buildLocalImage() {
cd $PLANE_TEMP_CODE_DIR
if [ "$BRANCH" == "master" ];
then
export APP_RELEASE=stable
export APP_RELEASE=latest
fi
docker compose -f build.yml build --no-cache >&2
@@ -99,17 +99,17 @@ function download() {
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/docker-compose.yaml https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/docker-compose.yml?$(date +%s)
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/variables-upgrade.env https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/variables.env?$(date +%s)
if [ -f "$DOCKER_ENV_PATH" ];
if [ -f "$PLANE_INSTALL_DIR/.env" ];
then
cp $DOCKER_ENV_PATH $PLANE_INSTALL_DIR/archive/$TS.env
cp $PLANE_INSTALL_DIR/.env $PLANE_INSTALL_DIR/archive/$TS.env
else
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
mv $PLANE_INSTALL_DIR/variables-upgrade.env $PLANE_INSTALL_DIR/.env
fi
if [ "$BRANCH" != "master" ];
then
cp $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/temp.yaml
sed -e 's@${APP_RELEASE:-stable}@'"$BRANCH"'@g' \
sed -e 's@${APP_RELEASE:-latest}@'"$BRANCH"'@g' \
$PLANE_INSTALL_DIR/temp.yaml > $PLANE_INSTALL_DIR/docker-compose.yaml
rm $PLANE_INSTALL_DIR/temp.yaml
@@ -131,9 +131,9 @@ function download() {
fi
echo ""
echo "Most recent Stable version is now available for you to use"
echo "Latest version is now available for you to use"
echo ""
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in 'plane.env 'file."
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
echo ""
}
@@ -144,7 +144,7 @@ function startServices() {
if [ -n "$migrator_container_id" ]; then
local idx=0
while docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do
local message=">> Waiting for Data Migration to finish"
local message=">>> Waiting for Data Migration to finish"
local dots=$(printf '%*s' $idx | tr ' ' '.')
echo -ne "\r$message$dots"
((idx++))
@@ -152,18 +152,13 @@ function startServices() {
done
fi
printf "\r\033[K"
echo ""
echo " Data Migration completed successfully ✅"
# if migrator exit status is not 0, show error message and exit
if [ -n "$migrator_container_id" ]; then
local migrator_exit_code=$(docker inspect --format='{{.State.ExitCode}}' $migrator_container_id)
if [ $migrator_exit_code -ne 0 ]; then
echo "Plane Server failed to start ❌"
# stopServices
echo
echo "Please check the logs for the 'migrator' service and resolve the issue(s)."
echo "Stop the services by running the command: ./setup.sh stop"
stopServices
exit 1
fi
fi
@@ -172,35 +167,26 @@ function startServices() {
local idx2=0
while ! docker logs $api_container_id 2>&1 | grep -m 1 -i "Application startup complete" | grep -q ".";
do
local message=">> Waiting for API Service to Start"
local message=">>> Waiting for API Service to Start"
local dots=$(printf '%*s' $idx2 | tr ' ' '.')
echo -ne "\r$message$dots"
((idx2++))
sleep 1
done
printf "\r\033[K"
echo " API Service started successfully ✅"
source "${DOCKER_ENV_PATH}"
echo " Plane Server started successfully ✅"
echo ""
echo " You can access the application at $WEB_URL"
echo ""
}
function stopServices() {
docker compose -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH down
}
function restartServices() {
# docker compose -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH restart
stopServices
startServices
docker compose -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH restart
}
function upgrade() {
echo "***** STOPPING SERVICES ****"
stopServices
echo
echo "***** DOWNLOADING STABLE VERSION ****"
echo "***** DOWNLOADING LATEST VERSION ****"
download
echo "***** PLEASE VALIDATE AND START SERVICES ****"
@@ -317,15 +303,15 @@ function askForAction() {
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ]
then
startServices
# askForAction
askForAction
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ]
then
stopServices
# askForAction
askForAction
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ]
then
restartServices
# askForAction
askForAction
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ]
then
upgrade
@@ -357,7 +343,7 @@ fi
if [ "$BRANCH" == "master" ];
then
export APP_RELEASE=stable
export APP_RELEASE=latest
fi
# REMOVE SPECIAL CHARACTERS FROM BRANCH NAME
@@ -368,21 +354,7 @@ fi
mkdir -p $PLANE_INSTALL_DIR/archive
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
# BACKWARD COMPATIBILITY
OLD_DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/.env
if [ -f "$OLD_DOCKER_ENV_PATH" ];
then
mv "$OLD_DOCKER_ENV_PATH" "$DOCKER_ENV_PATH"
OS_NAME=$(uname)
if [ "$OS_NAME" == "Darwin" ];
then
sed -i '' -e 's@APP_RELEASE=latest@APP_RELEASE=stable@' "$DOCKER_ENV_PATH"
else
sed -i -e 's@APP_RELEASE=latest@APP_RELEASE=stable@' "$DOCKER_ENV_PATH"
fi
fi
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/.env
print_header
askForAction $@
+4 -4
View File
@@ -1,4 +1,4 @@
APP_RELEASE=stable
APP_RELEASE=latest
WEB_REPLICAS=1
SPACE_REPLICAS=1
@@ -18,12 +18,12 @@ POSTGRES_USER=plane
POSTGRES_PASSWORD=plane
POSTGRES_DB=plane
PGDATA=/var/lib/postgresql/data
DATABASE_URL=
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${PGHOST}/${PGDATABASE}
# REDIS SETTINGS
REDIS_HOST=plane-redis
REDIS_PORT=6379
REDIS_URL=
REDIS_URL=redis://${REDIS_HOST}:6379/
# Secret Key
SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
@@ -41,4 +41,4 @@ BUCKET_NAME=uploads
FILE_SIZE_LIMIT=5242880
# Gunicorn Workers
GUNICORN_WORKERS=1
GUNICORN_WORKERS=2
+1 -1
View File
@@ -30,7 +30,7 @@ http {
}
location /${BUCKET_NAME}/ {
proxy_pass http://plane-minio:9000/${BUCKET_NAME}/;
proxy_pass http://plane-minio:9000/uploads/;
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
"version": "0.17.0",
"version": "0.16.0",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor-core",
"version": "0.17.0",
"version": "0.16.0",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
@@ -1,6 +1,5 @@
import { Editor } from "@tiptap/react";
import { FC, ReactNode } from "react";
import { cn } from "src/lib/utils";
interface EditorContainerProps {
editor: Editor | null;
@@ -54,7 +53,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
onMouseLeave={() => {
hideDragHandle?.();
}}
className={cn(`cursor-text`, { "active-editor": editor?.isFocused && editor?.isEditable }, editorClassNames)}
className={`cursor-text ${editorClassNames}`}
>
{children}
</div>
@@ -22,7 +22,7 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.querySelector(".active-editor") ?? document.querySelector("#editor-container"),
appendTo: () => document.querySelector("#editor-container"),
content: reactRenderer.element,
showOnCreate: true,
interactive: true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/document-editor",
"version": "0.17.0",
"version": "0.16.0",
"description": "Package that powers Plane's Pages Editor",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor-extensions",
"version": "0.17.0",
"version": "0.16.0",
"description": "Package that powers Plane's Editor with extensions",
"private": true,
"main": "./dist/index.mjs",
@@ -330,7 +330,7 @@ const renderItems = () => {
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.querySelector(".active-editor") ?? document.querySelector("#editor-container"),
appendTo: () => document.querySelector("#editor-container"),
content: component.element,
showOnCreate: true,
interactive: true,
@@ -1,6 +1,6 @@
{
"name": "@plane/lite-text-editor",
"version": "0.17.0",
"version": "0.16.0",
"description": "Package that powers Plane's Comment Editor",
"private": true,
"main": "./dist/index.mjs",
@@ -1,6 +1,6 @@
{
"name": "@plane/rich-text-editor",
"version": "0.17.0",
"version": "0.16.0",
"description": "Rich Text Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "eslint-config-custom",
"private": true,
"version": "0.17.0",
"version": "0.16.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tailwind-config-custom",
"version": "0.17.0",
"version": "0.16.0",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tsconfig",
"version": "0.17.0",
"version": "0.16.0",
"private": true,
"files": [
"base.json",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.17.0",
"version": "0.16.0",
"private": true,
"main": "./src/index.d.ts"
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.17.0",
"version": "0.16.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
+1 -1
View File
@@ -91,7 +91,7 @@ const CustomSelect = (props: ICustomSelectProps) => {
)}
</>
{isOpen && (
<Listbox.Options className="fixed z-20" onClick={() => closeDropdown()} static>
<Listbox.Options className="fixed z-10" onClick={() => closeDropdown()} static>
<div
className={cn(
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap",
+2 -6
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/** @type {import('next').NextConfig} */
require("dotenv").config({ path: ".env" });
const { withSentryConfig } = require("@sentry/nextjs");
@@ -27,11 +26,8 @@ const nextConfig = {
output: "standalone",
};
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) {
module.exports = withSentryConfig(nextConfig,
{ silent: true, authToken: process.env.SENTRY_AUTH_TOKEN },
{ hideSourceMaps: true }
);
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0")) {
module.exports = withSentryConfig(nextConfig, { silent: true }, { hideSourceMaps: true });
} else {
module.exports = nextConfig;
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.17.0",
"version": "0.16.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -22,7 +22,7 @@
"@plane/rich-text-editor": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@sentry/nextjs": "^7.108.0",
"@sentry/nextjs": "^7.85.0",
"axios": "^1.3.4",
"clsx": "^2.0.0",
"dotenv": "^16.3.1",
+18 -6
View File
@@ -17,25 +17,37 @@
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG",
"SENTRY_AUTH_TOKEN"
"JITSU_TRACKER_ACCESS_KEY",
"JITSU_TRACKER_HOST"
],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
"dependsOn": [
"^build"
],
"outputs": [
".next/**",
"dist/**"
]
},
"develop": {
"cache": false,
"persistent": true,
"dependsOn": ["^build"]
"dependsOn": [
"^build"
]
},
"dev": {
"cache": false,
"persistent": true,
"dependsOn": ["^build"]
"dependsOn": [
"^build"
]
},
"test": {
"dependsOn": ["^build"],
"dependsOn": [
"^build"
],
"outputs": []
},
"lint": {
@@ -1,7 +1,9 @@
// types
// icons
import { Triangle } from "lucide-react";
import { IDefaultAnalyticsResponse, TStateGroups } from "@plane/types";
// constants
// types
import { STATE_GROUPS } from "@/constants/state";
// constants
type Props = {
defaultAnalytics: IDefaultAnalyticsResponse;
@@ -14,7 +16,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
<h4 className="text-base font-medium text-custom-text-100">Total open tasks</h4>
<h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3>
</div>
<div className="space-y-6 pb-2">
<div className="space-y-6">
{defaultAnalytics?.open_issues_classified.map((group) => {
const percentage = ((group.state_count / defaultAnalytics.total_issues) * 100).toFixed(0);
@@ -48,5 +50,14 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
);
})}
</div>
<div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
<p className="flex items-center gap-1 text-custom-text-200">
<Triangle className="h-4 w-4" />
<span>Estimate Demand:</span>
</p>
<p className="font-medium">
{defaultAnalytics.open_estimate_sum}/{defaultAnalytics.total_estimate_sum}
</p>
</div>
</div>
);
@@ -69,9 +69,7 @@ export const CommandPaletteHelpActions: React.FC<Props> = (props) => {
<Command.Item
onSelect={() => {
closePalette();
if (window) {
window.$crisp.push(["do", "chat:show"]);
}
(window as any)?.$crisp.push(["do", "chat:open"]);
}}
className="focus:outline-none"
>
@@ -40,7 +40,7 @@ const issueService = new IssueService();
export const CommandModal: React.FC = observer(() => {
// hooks
const { getProjectById, workspaceProjectIds } = useProject();
const { getProjectById } = useProject();
const { isMobile } = usePlatformOS();
// states
const [placeholder, setPlaceholder] = useState("Type a command or search...");
@@ -282,24 +282,22 @@ export const CommandModal: React.FC = observer(() => {
setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)}
/>
)}
{workspaceSlug && workspaceProjectIds && workspaceProjectIds.length > 0 && (
<Command.Group heading="Issue">
<Command.Item
onSelect={() => {
closePalette();
setTrackElement("Command Palette");
toggleCreateIssueModal(true);
}}
className="focus:bg-custom-background-80"
>
<div className="flex items-center gap-2 text-custom-text-200">
<LayersIcon className="h-3.5 w-3.5" />
Create new issue
</div>
<kbd>C</kbd>
</Command.Item>
</Command.Group>
)}
<Command.Group heading="Issue">
<Command.Item
onSelect={() => {
closePalette();
setTrackElement("Command Palette");
toggleCreateIssueModal(true);
}}
className="focus:bg-custom-background-80"
>
<div className="flex items-center gap-2 text-custom-text-200">
<LayersIcon className="h-3.5 w-3.5" />
Create new issue
</div>
<kbd>C</kbd>
</Command.Item>
</Command.Group>
{workspaceSlug && (
<Command.Group heading="Project">
@@ -113,6 +113,8 @@ export const CommandPalette: FC = observer(() => {
const canPerformWorkspaceCreateActions = useCallback(
(showToast: boolean = true) => {
const isAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
console.log("currentWorkspaceRole", currentWorkspaceRole);
console.log("isAllowed", isAllowed);
if (!isAllowed && showToast)
setToast({
type: TOAST_TYPE.ERROR,
@@ -36,18 +36,18 @@ export const ShortcutsModal: FC<Props> = (props) => {
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative flex h-full items-center justify-center">
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="h-full w-full">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<div className="flex h-[61vh] w-full flex-col space-y-4 overflow-hidden rounded-lg bg-custom-background-100 p-5 shadow-custom-shadow-md transition-all sm:w-[28rem]">
<Dialog.Title as="h3" className="flex justify-between">
<span className="text-lg font-medium">Keyboard shortcuts</span>
@@ -71,9 +71,9 @@ export const ShortcutsModal: FC<Props> = (props) => {
</div>
<ShortcutCommandsList searchQuery={query} />
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
+1 -1
View File
@@ -144,7 +144,7 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
useEffect(() => {
if (!unsplashImages || value !== null) return;
onChange(unsplashImages[0]?.urls.regular);
onChange(unsplashImages[0].urls.regular);
}, [value, onChange, unsplashImages]);
const handleClose = () => {
@@ -201,7 +201,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
as="div"
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
>
{cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? (
{cycleIssues.length > 0 ? (
cycle.distribution?.assignees?.map((assignee, index) => {
if (assignee.assignee_id)
return (
@@ -246,8 +246,8 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
as="div"
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
>
{cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? (
cycle.distribution.labels?.map((label, index) => (
{cycleIssues.length > 0 ? (
cycle.distribution?.labels?.map((label, index) => (
<SingleProgressStats
key={label.label_id ?? `no-label-${index}`}
title={
+2 -2
View File
@@ -56,7 +56,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.detail ?? "Error in creating cycle. Please try again.",
message: err.detail ?? "Error in creating cycle. Please try again.",
});
captureCycleEvent({
eventName: CYCLE_CREATED,
@@ -90,7 +90,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.detail ?? "Error in updating cycle. Please try again.",
message: err.detail ?? "Error in updating cycle. Please try again.",
});
});
};
@@ -1,6 +1,6 @@
import Image from "next/image";
import { useTheme } from "next-themes";
import { Home, Zap } from "lucide-react";
import { LayoutGrid, Zap } from "lucide-react";
// images
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
@@ -25,7 +25,9 @@ export const WorkspaceDashboardHeader = () => {
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Home" icon={<Home className="h-4 w-4 text-custom-text-300" />} />}
link={
<BreadcrumbLink label="Dashboard" icon={<LayoutGrid className="h-4 w-4 text-custom-text-300" />} />
}
/>
</Breadcrumbs>
</div>
@@ -1,11 +1,6 @@
import React from "react";
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
// icons
import { UserCog2 } from "lucide-react";
// ui
import { getButtonStyling } from "@plane/ui";
// images
import instanceNotReady from "public/instance/plane-instance-not-ready.webp";
import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
@@ -30,10 +25,6 @@ export const InstanceNotReady = () => {
<div className="flex w-full flex-col items-center gap-5 py-12 pb-20">
<h3 className="text-2xl font-medium">Your Plane instance isn{"'"}t ready yet</h3>
<p className="text-sm">Ask your Instance Admin to complete set-up first.</p>
<Link href="/god-mode" className={`${getButtonStyling("primary", "md")} mt-4`}>
<UserCog2 className="h-3.5 w-3.5" />
Get started
</Link>
</div>
</div>
</div>
@@ -73,7 +73,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
setToast({
title: "Error",
type: TOAST_TYPE.ERROR,
message: err?.detail ?? "Failed to perform this action",
message: err.detail ?? "Failed to perform this action",
});
});
}
@@ -143,7 +143,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
setToast({
title: "Error",
type: TOAST_TYPE.ERROR,
message: err?.detail ?? "Failed to perform this action",
message: err.detail ?? "Failed to perform this action",
});
});
}
@@ -1,21 +1,20 @@
import { useState } from "react";
import omit from "lodash/omit";
import { observer } from "mobx-react";
// icons
import { Pencil, Trash2 } from "lucide-react";
// types
// hooks
import { Copy, Pencil, Trash2 } from "lucide-react";
import { TIssue } from "@plane/types";
// ui
import { CustomMenu } from "@plane/ui";
// components
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
// constant
import { EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useEventTracker, useIssues, useUser } from "@/hooks/store";
// ui
// components
// helpers
// types
import { IQuickActionProps } from "../list/list-view-types";
// constant
export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
const { issue, handleDelete, handleUpdate, customActionButton, portalElement, readOnly = false } = props;
@@ -90,6 +89,19 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
</div>
</CustomMenu.MenuItem>
)}
{isEditingAllowed && (
<CustomMenu.MenuItem
onClick={() => {
setTrackElement(activeLayout);
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Copy className="h-3 w-3" />
Make a copy
</div>
</CustomMenu.MenuItem>
)}
{isDeletingAllowed && (
<CustomMenu.MenuItem
onClick={() => {
@@ -84,7 +84,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
const payload = {
...changesMade,
name: changesMade?.name && changesMade?.name?.trim() !== "" ? changesMade.name?.trim() : "Untitled",
name: changesMade?.name && changesMade?.name?.trim() === "" ? changesMade.name?.trim() : "Untitled",
};
await issueDraftService
@@ -26,7 +26,6 @@ import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper"
import { getChangedIssuefields } from "@/helpers/issue.helper";
import { shouldRenderProject } from "@/helpers/project.helper";
import { useApplication, useEstimate, useIssueDetail, useMention, useProject, useWorkspace } from "@/hooks/store";
import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties";
// services
import { AIService } from "@/services/ai.service";
import { FileService } from "@/services/file.service";
@@ -122,7 +121,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
// store hooks
const {
config: { envConfig },
router: { projectId: routeProjectId },
} = useApplication();
const { getProjectById } = useProject();
const { areEstimatesEnabledForProject } = useEstimate();
@@ -130,7 +128,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const {
issue: { getIssueById },
} = useIssueDetail();
const { fetchCycles } = useProjectIssueProperties();
// form info
const {
formState: { errors, isDirty, isSubmitting, dirtyFields },
@@ -163,7 +160,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
parent_id: formData.parent_id,
});
}
if (projectId && routeProjectId !== projectId) fetchCycles(workspaceSlug, projectId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
@@ -1,5 +1,4 @@
import React from "react";
import { observer } from "mobx-react";
import { RefreshCw } from "lucide-react";
import { TIssue } from "@plane/types";
// types
@@ -10,7 +9,7 @@ type Props = {
issueDetail?: TIssue;
};
export const IssueUpdateStatus: React.FC<Props> = observer((props) => {
export const IssueUpdateStatus: React.FC<Props> = (props) => {
const { isSubmitting, issueDetail } = props;
// hooks
const { getProjectById } = useProject();
@@ -34,4 +33,4 @@ export const IssueUpdateStatus: React.FC<Props> = observer((props) => {
</div>
</>
);
});
};
+2 -2
View File
@@ -68,7 +68,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.detail ?? "Module could not be created. Please try again.",
message: err.detail ?? "Module could not be created. Please try again.",
});
captureModuleEvent({
eventName: MODULE_CREATED,
@@ -99,7 +99,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.detail ?? "Module could not be updated. Please try again.",
message: err.detail ?? "Module could not be updated. Please try again.",
});
captureModuleEvent({
eventName: MODULE_UPDATED,
@@ -10,11 +10,11 @@ import {
ContrastIcon,
FileText,
LayersIcon,
LayoutGrid,
PenSquare,
Search,
Settings,
Bell,
Home,
} from "lucide-react";
import { IWorkspace } from "@plane/types";
import { Avatar, DiceIcon, PhotoFilterIcon } from "@plane/ui";
@@ -26,8 +26,8 @@ import projectEmoji from "public/emoji/project-emoji.svg";
const workspaceLinks = [
{
Icon: Home,
name: "Home",
Icon: LayoutGrid,
name: "Dashboard",
},
{
Icon: BarChart2,
@@ -209,10 +209,8 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
[val.type]: logoValue,
});
}}
defaultIconColor={value.in_use && value.in_use === "icon" ? value.icon?.color : undefined}
defaultOpen={
value.in_use && value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON
}
defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined}
defaultOpen={value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON}
/>
)}
/>
@@ -226,7 +224,7 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
control={control}
name="name"
rules={{
required: "Name is required",
required: "Title is required",
maxLength: {
value: 255,
message: "Title should be less than 255 characters",
@@ -240,7 +238,7 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
value={value}
onChange={handleNameChange(onChange)}
hasError={Boolean(errors.name)}
placeholder="Project name"
placeholder="Project title"
className="w-full focus:border-blue-400"
tabIndex={1}
/>
+35 -58
View File
@@ -1,7 +1,7 @@
import { FC, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
// icons
import { Info, Lock } from "lucide-react";
import { Lock } from "lucide-react";
import { IProject, IWorkspace } from "@plane/types";
// ui
import {
@@ -13,7 +13,6 @@ import {
setToast,
CustomEmojiIconPicker,
EmojiIconPickerTypes,
Tooltip,
} from "@plane/ui";
// components
import { ImagePickerPopover } from "@/components/core";
@@ -25,7 +24,6 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
import { useEventTracker, useProject } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// services
import { ProjectService } from "@/services/project";
// types
@@ -44,7 +42,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
// store hooks
const { captureProjectEvent } = useEventTracker();
const { updateProject } = useProject();
const { isMobile } = usePlatformOS();
// form info
const {
handleSubmit,
@@ -169,10 +166,8 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
[val.type]: logoValue,
});
}}
defaultIconColor={value?.in_use && value.in_use === "icon" ? value?.icon?.color : undefined}
defaultOpen={
value.in_use && value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON
}
defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined}
defaultOpen={value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON}
disabled={!isAdmin}
/>
)}
@@ -232,9 +227,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
/>
)}
/>
<span className="text-xs text-red-500">
<>{errors?.name?.message}</>
</span>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Description</h4>
@@ -255,54 +247,39 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
)}
/>
</div>
<div className="flex w-full justify-between gap-10">
<div className="flex w-full items-center justify-between gap-10">
<div className="flex w-1/2 flex-col gap-1">
<h4 className="text-sm">Project ID</h4>
<div className="relative">
<Controller
control={control}
name="identifier"
rules={{
required: "Project ID is required",
validate: (value) =>
/^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) ||
"Only Alphanumeric & Non-latin characters are allowed.",
minLength: {
value: 1,
message: "Project ID must at least be of 1 character",
},
maxLength: {
value: 5,
message: "Project ID must at most be of 5 characters",
},
}}
render={({ field: { value, ref } }) => (
<Input
id="identifier"
name="identifier"
type="text"
value={value}
onChange={handleIdentifierChange}
ref={ref}
hasError={Boolean(errors.identifier)}
placeholder="Enter Project ID"
className="w-full font-medium"
disabled={!isAdmin}
/>
)}
/>
<Tooltip
isMobile={isMobile}
tooltipContent="Helps you identify issues in the project uniquely, (e.g. APP-123). Max 5 characters."
className="text-sm"
position="right-top"
>
<Info className="absolute right-2 top-2.5 h-4 w-4 text-custom-text-400" />
</Tooltip>
</div>
<span className="text-xs text-red-500">
<>{errors?.identifier?.message}</>
</span>
<h4 className="text-sm">Identifier</h4>
<Controller
control={control}
name="identifier"
rules={{
required: "Identifier is required",
validate: (value) => /^[A-Z0-9]+$/.test(value.toUpperCase()) || "Identifier must be in uppercase.",
minLength: {
value: 1,
message: "Identifier must at least be of 1 character",
},
maxLength: {
value: 12,
message: "Identifier must at most be of 5 characters",
},
}}
render={({ field: { value, ref } }) => (
<Input
id="identifier"
name="identifier"
type="text"
value={value}
onChange={handleIdentifierChange}
ref={ref}
hasError={Boolean(errors.identifier)}
placeholder="Enter identifier"
className="w-full font-medium"
disabled={!isAdmin}
/>
)}
/>
</div>
<div className="flex w-1/2 flex-col gap-1">
<h4 className="text-sm">Network</h4>
+3 -3
View File
@@ -11,7 +11,7 @@ type Props = {
export const ProjectLogo: React.FC<Props> = (props) => {
const { className, logo } = props;
if (logo?.in_use === "icon" && logo?.icon)
if (logo && logo.in_use === "icon" && logo.icon)
return (
<span
style={{
@@ -23,12 +23,12 @@ export const ProjectLogo: React.FC<Props> = (props) => {
</span>
);
if (logo?.in_use === "emoji" && logo?.emoji)
if (logo && logo.in_use === "emoji" && logo.emoji)
return (
<span className={cn("text-base", className)}>
{logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))}
</span>
);
return <></>;
return <span />;
};
+1 -1
View File
@@ -54,7 +54,7 @@ export const CreateUpdateProjectViewModal: FC<Props> = observer((props) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.detail ?? "Something went wrong. Please try again.",
message: err.detail ?? "Something went wrong. Please try again.",
})
);
};
-89
View File
@@ -1,89 +0,0 @@
import { useCycle, useEstimate, useLabel, useMember, useModule, useProjectState } from "./store";
export const useProjectIssueProperties = () => {
const { fetchProjectStates } = useProjectState();
const {
project: { fetchProjectMembers },
} = useMember();
const { fetchProjectLabels } = useLabel();
const { fetchAllCycles: fetchProjectAllCycles } = useCycle();
const { fetchModules: fetchProjectAllModules } = useModule();
const { fetchProjectEstimates } = useEstimate();
// fetching project states
const fetchStates = async (
workspaceSlug: string | string[] | undefined,
projectId: string | string[] | undefined
) => {
if (workspaceSlug && projectId) {
await fetchProjectStates(workspaceSlug.toString(), projectId.toString());
}
};
// fetching project members
const fetchMembers = async (
workspaceSlug: string | string[] | undefined,
projectId: string | string[] | undefined
) => {
if (workspaceSlug && projectId) {
await fetchProjectMembers(workspaceSlug.toString(), projectId.toString());
}
};
// fetching project labels
const fetchLabels = async (
workspaceSlug: string | string[] | undefined,
projectId: string | string[] | undefined
) => {
if (workspaceSlug && projectId) {
await fetchProjectLabels(workspaceSlug.toString(), projectId.toString());
}
};
// fetching project cycles
const fetchCycles = async (
workspaceSlug: string | string[] | undefined,
projectId: string | string[] | undefined
) => {
if (workspaceSlug && projectId) {
await fetchProjectAllCycles(workspaceSlug.toString(), projectId.toString());
}
};
// fetching project modules
const fetchModules = async (
workspaceSlug: string | string[] | undefined,
projectId: string | string[] | undefined
) => {
if (workspaceSlug && projectId) {
await fetchProjectAllModules(workspaceSlug.toString(), projectId.toString());
}
};
// fetching project estimates
const fetchEstimates = async (
workspaceSlug: string | string[] | undefined,
projectId: string | string[] | undefined
) => {
if (workspaceSlug && projectId) {
await fetchProjectEstimates(workspaceSlug.toString(), projectId.toString());
}
};
const fetchAll = async (workspaceSlug: string | string[] | undefined, projectId: string | string[] | undefined) => {
if (workspaceSlug && projectId) {
await fetchStates(workspaceSlug, projectId);
await fetchMembers(workspaceSlug, projectId);
await fetchLabels(workspaceSlug, projectId);
await fetchCycles(workspaceSlug, projectId);
await fetchModules(workspaceSlug, projectId);
await fetchEstimates(workspaceSlug, projectId);
}
};
return {
fetchAll,
fetchStates,
fetchMembers,
fetchLabels,
fetchCycles,
fetchModules,
fetchEstimates,
};
};
@@ -1,5 +1,4 @@
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useRouter } from "next/router";
// hooks
@@ -7,7 +6,7 @@ import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "@/constants/works
import { useUser } from "@/hooks/store";
// constants
export const WorkspaceSettingsSidebar = observer(() => {
export const WorkspaceSettingsSidebar = () => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -45,4 +44,4 @@ export const WorkspaceSettingsSidebar = observer(() => {
</div>
</div>
);
});
};
+2 -6
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-var-requires */
require("dotenv").config({ path: ".env" });
const { withSentryConfig } = require("@sentry/nextjs");
@@ -25,11 +24,8 @@ const nextConfig = {
output: "standalone",
};
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) {
module.exports = withSentryConfig(nextConfig,
{ silent: true, authToken: process.env.SENTRY_AUTH_TOKEN },
{ hideSourceMaps: true }
);
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0")) {
module.exports = withSentryConfig(nextConfig, { silent: true }, { hideSourceMaps: true });
} else {
module.exports = nextConfig;
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.17.0",
"version": "0.16.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -28,7 +28,7 @@
"@plane/types": "*",
"@plane/ui": "*",
"@popperjs/core": "^2.11.8",
"@sentry/nextjs": "^7.108.0",
"@sentry/nextjs": "^7.85.0",
"axios": "^1.1.3",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
+1 -1
View File
@@ -14,7 +14,7 @@ import { NextPageWithLayout } from "@/lib/types";
const WorkspacePage: NextPageWithLayout = observer(() => {
const { currentWorkspace } = useWorkspace();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Home` : undefined;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Dashboard` : undefined;
return (
<>
+1 -1
View File
@@ -220,7 +220,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
description="You can see here if someone invites you to a workspace."
image={emptyInvitation}
primaryButton={{
text: "Back to home",
text: "Back to dashboard",
onClick: () => router.push("/"),
}}
/>
+2 -2
View File
@@ -81,7 +81,7 @@ const WorkspaceInvitationPage: NextPageWithLayout = observer(() => {
title={`You are already a member of ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
>
<EmptySpaceItem Icon={Boxes} title="Continue to home" href="/" />
<EmptySpaceItem Icon={Boxes} title="Continue to Dashboard" href="/" />
</EmptySpace>
</>
) : (
@@ -105,7 +105,7 @@ const WorkspaceInvitationPage: NextPageWithLayout = observer(() => {
{!currentUser ? (
<EmptySpaceItem Icon={User2} title="Sign in to continue" href="/" />
) : (
<EmptySpaceItem Icon={Boxes} title="Continue to home" href="/" />
<EmptySpaceItem Icon={Boxes} title="Continue to Dashboard" href="/" />
)}
<EmptySpaceItem Icon={Star} title="Star us on GitHub" href="https://github.com/makeplane" />
<EmptySpaceItem
+90 -101
View File
@@ -2149,46 +2149,35 @@
dependencies:
"@daybrush/utils" "^1.4.0"
"@sentry-internal/feedback@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.108.0.tgz#7033352abd304f1383ec47640e056a0dfd5132b7"
integrity sha512-8JcgZEnk1uWrXJhsd3iRvFtEiVeaWOEhN0NZwhwQXHfvODqep6JtrkY1yCIyxbpA37aZmrPc2JhyotRERGfUjg==
"@sentry-internal/feedback@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.91.0.tgz#be09e5aec2959fcf503e2cf78496d5e2d263bc5a"
integrity sha512-SJKTSaz68F5YIwF79EttBm915M2LnacgZMYRnRumyTmMKnebGhYQLwWbZdpaDvOa1U18dgRajDX8Qed/8A3tXw==
dependencies:
"@sentry/core" "7.108.0"
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry/core" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
"@sentry-internal/replay-canvas@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.108.0.tgz#641133c19c0e1c423617b8d791f53d6cd0b0a862"
integrity sha512-R5tvjGqWUV5vSk0N1eBgVW7wIADinrkfDEBZ9FyKP2mXHBobsyNGt30heJDEqYmVqluRqjU2NuIRapsnnrpGnA==
"@sentry-internal/tracing@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.91.0.tgz#fbb6e1e3383e1eeee08633384e004da73ac1c37d"
integrity sha512-JH5y6gs6BS0its7WF2DhySu7nkhPDfZcdpAXldxzIlJpqFkuwQKLU5nkYJpiIyZz1NHYYtW5aum2bV2oCOdDRA==
dependencies:
"@sentry/core" "7.108.0"
"@sentry/replay" "7.108.0"
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry/core" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
"@sentry-internal/tracing@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.108.0.tgz#d1e660701fb860cfae72b6ebfa8fb267533421fa"
integrity sha512-zuK5XsTsb+U+hgn3SPetYDAogrXsM16U/LLoMW7+TlC6UjlHGYQvmX3o+M2vntejoU1QZS8m1bCAZSMWEypAEw==
"@sentry/browser@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.91.0.tgz#de3b9ae3ca7716a35cfabc97ac376944a67e6e34"
integrity sha512-lJv3x/xekzC/biiyAsVCioq2XnKNOZhI6jY3ZzLJZClYV8eKRi7D3KCsHRvMiCdGak1d/6sVp8F4NYY+YiWy1Q==
dependencies:
"@sentry/core" "7.108.0"
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry/browser@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.108.0.tgz#b95810bb6572b63781f253615896f5afb1a3a5c0"
integrity sha512-FNpzsdTvGvdHJMUelqEouUXMZU7jC+dpN7CdT6IoHVVFEkoAgrjMVUhXZoQ/dmCkdKWHmFSQhJ8Fm6V+e9Aq0A==
dependencies:
"@sentry-internal/feedback" "7.108.0"
"@sentry-internal/replay-canvas" "7.108.0"
"@sentry-internal/tracing" "7.108.0"
"@sentry/core" "7.108.0"
"@sentry/replay" "7.108.0"
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry-internal/feedback" "7.91.0"
"@sentry-internal/tracing" "7.91.0"
"@sentry/core" "7.91.0"
"@sentry/replay" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
"@sentry/cli@^1.77.1":
version "1.77.1"
@@ -2202,95 +2191,95 @@
proxy-from-env "^1.1.0"
which "^2.0.2"
"@sentry/core@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.108.0.tgz#a27e8d6f85f59c5730ce86071474f15ac899fde0"
integrity sha512-I/VNZCFgLASxHZaD0EtxZRM34WG9w2gozqgrKGNMzAymwmQ3K9g/1qmBy4e6iS3YRptb7J5UhQkZQHrcwBbjWQ==
"@sentry/core@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.91.0.tgz#229334d7f03dd5d90a17495e61ce4215ab730b2a"
integrity sha512-tu+gYq4JrTdrR+YSh5IVHF0fJi/Pi9y0HZ5H9HnYy+UMcXIotxf6hIEaC6ZKGeLWkGXffz2gKpQLe/g6vy/lPA==
dependencies:
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
"@sentry/integrations@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.108.0.tgz#307c61966208f2a126c1a93e712277c8f86a3d3c"
integrity sha512-b/WbK1f3x2rQ4aJJSA4VSwpBXrXFm1Nzrca3Y9qW0MI1wjZEYsDDrh9m6ulLdVBl4YDc2VqYp1COwU/NjuHlog==
"@sentry/integrations@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.91.0.tgz#b0496c9e404783bc433b1d2464d8f9aa180ebc8e"
integrity sha512-LGRfb+WfG3FaWHtDnJIhtupweat0imCQr2z/5SSbQKzqxHhtlaEU+9IExBmBdzq90n4lRBaVQHA3zGuU02uOhg==
dependencies:
"@sentry/core" "7.108.0"
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry/core" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
localforage "^1.8.1"
"@sentry/nextjs@^7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.108.0.tgz#31d903d75bbf4b4530046360daff4d51dcf62f53"
integrity sha512-etBrMSLRbNAzozetBeL6D+lR9lRAyHmV7NUBGCX9lQvgmcdxkQa15EX8pIKjsMejZ8xAZNsqYVIByIs67A77rg==
"@sentry/nextjs@^7.85.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.91.0.tgz#42eb3af10ff230e8a3fe9f0e50cdbac94b7d290e"
integrity sha512-wE83+OTEH4yYnDrhMw9eVEARSfZc6xY5qJb9xyYm5rW3+gVjNQZQaUY+wkM61Xdo0T35BN+7U4T88HbwzGeMqA==
dependencies:
"@rollup/plugin-commonjs" "24.0.0"
"@sentry/core" "7.108.0"
"@sentry/integrations" "7.108.0"
"@sentry/node" "7.108.0"
"@sentry/react" "7.108.0"
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry/vercel-edge" "7.108.0"
"@sentry/core" "7.91.0"
"@sentry/integrations" "7.91.0"
"@sentry/node" "7.91.0"
"@sentry/react" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
"@sentry/vercel-edge" "7.91.0"
"@sentry/webpack-plugin" "1.21.0"
chalk "3.0.0"
resolve "1.22.8"
rollup "2.78.0"
stacktrace-parser "^0.1.10"
"@sentry/node@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.108.0.tgz#ed113dc1d39aaae32f7e9f681dcea41cf28eb5f1"
integrity sha512-pMxc9txnDDkU4Z8k2Uw/DPSLPehNtWV3mjJ3+my0AMORGYrXLkJI93tddlE5z/7k+GEJdj1HsOLgxUN0OU+HGA==
"@sentry/node@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.91.0.tgz#26bf13c3daf988f9725afd1a3cc38ba2ff90d62a"
integrity sha512-hTIfSQxD7L+AKIqyjoq8CWBRkEQrrMZmA3GSZgPI5JFWBHgO0HBo5TH/8TU81oEJh6kqqHAl2ObMhmcnaFqlzg==
dependencies:
"@sentry-internal/tracing" "7.108.0"
"@sentry/core" "7.108.0"
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry-internal/tracing" "7.91.0"
"@sentry/core" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
https-proxy-agent "^5.0.0"
"@sentry/react@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.108.0.tgz#26a50324f6d7a9576f3753c099e7bcd8def94f3c"
integrity sha512-C60arh5/gtO42eMU9l34aWlKDLZUO+1j1goaEf/XRSwUcyJS9tbJrs+mT4nbKxUsEG714It2gRbfSEvh1eXmCg==
"@sentry/react@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.91.0.tgz#620e6ce9452af025d2cc1b2eca3dd1dd730dc439"
integrity sha512-7JH2rWaX3WKHHvBcZQ4f/KnkYIXTf7hMojRFncUwPocdtDlhJw/JUvjAYNpEysixXIgsMes3B32lmtZjGjRhwQ==
dependencies:
"@sentry/browser" "7.108.0"
"@sentry/core" "7.108.0"
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry/browser" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
hoist-non-react-statics "^3.3.2"
"@sentry/replay@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.108.0.tgz#baa679bd19b4e3729e607d3f84cff5048aeb3415"
integrity sha512-jo8fDOzcZJclP1+4n9jUtVxTlBFT9hXwxhAMrhrt70FV/nfmCtYQMD3bzIj79nwbhUtFP6pN39JH1o7Xqt1hxQ==
"@sentry/replay@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.91.0.tgz#95077868aee3c3cc670affe13156434f858e1755"
integrity sha512-XwbesnLLNtaVXKtDoyBB96GxJuhGi9zy3a662Ba/McmumCnkXrMQYpQPh08U7MgkTyDRgjDwm7PXDhiKpcb03g==
dependencies:
"@sentry-internal/tracing" "7.108.0"
"@sentry/core" "7.108.0"
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry-internal/tracing" "7.91.0"
"@sentry/core" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
"@sentry/types@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.108.0.tgz#5ceb959c4dabe511fc441fec8c2465f2d624900f"
integrity sha512-bKtHITmBN3kqtqE5eVvL8mY8znM05vEodENwRpcm6TSrrBjC2RnwNWVwGstYDdHpNfFuKwC8mLY9bgMJcENo8g==
"@sentry/types@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.91.0.tgz#5b68954e08986fecb0d4bef168df58eef62c32c7"
integrity sha512-bcQnb7J3P3equbCUc+sPuHog2Y47yGD2sCkzmnZBjvBT0Z1B4f36fI/5WjyZhTjLSiOdg3F2otwvikbMjmBDew==
"@sentry/utils@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.108.0.tgz#0231042956aed2ef35809891592238530349dfd9"
integrity sha512-a45yEFD5qtgZaIFRAcFkG8C8lnDzn6t4LfLXuV4OafGAy/3ZAN3XN8wDnrruHkiUezSSANGsLg3bXaLW/JLvJw==
"@sentry/utils@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.91.0.tgz#3b1a94c053c885877908cd3e1365e3d23e21a73f"
integrity sha512-fvxjrEbk6T6Otu++Ax9ntlQ0sGRiwSC179w68aC3u26Wr30FAIRKqHTCCdc2jyWk7Gd9uWRT/cq+g8NG/8BfSg==
dependencies:
"@sentry/types" "7.108.0"
"@sentry/types" "7.91.0"
"@sentry/vercel-edge@7.108.0":
version "7.108.0"
resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-7.108.0.tgz#c5ca35094bc46029ec5a72f7ee09fd3705582baa"
integrity sha512-dUuUEswaVIzsJnzTfaJxrvkfOowrlJxxHo2AybPDym2rob7CdaLdDJIYJa83X7QeAKMkTgLny/gYSQYC0E4UyA==
"@sentry/vercel-edge@7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-7.91.0.tgz#df67ee39d10570b71eccf831a181c064974d62b1"
integrity sha512-CounqhXPwFh67zf6L/q4ACBHHqknT6YY9LdgIAnUd0GGgHzrJPyKcthvh8Je4lNdpo5LFg2gnR+6g6JS8DDYDQ==
dependencies:
"@sentry-internal/tracing" "7.108.0"
"@sentry/core" "7.108.0"
"@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0"
"@sentry-internal/tracing" "7.91.0"
"@sentry/core" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
"@sentry/webpack-plugin@1.21.0":
version "1.21.0"
@@ -2733,7 +2722,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
"@types/react@*", "@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==