Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c1b100ee0 | ||
|
|
8e725b0f75 | ||
|
|
cf75cb35b1 | ||
|
|
0487579f4e | ||
|
|
60232130f4 |
@@ -6,6 +6,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- preview
|
||||
- develop
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["develop", "preview", "master"]
|
||||
branches: ["master"]
|
||||
pull_request:
|
||||
branches: ["develop", "preview", "master"]
|
||||
schedule:
|
||||
|
||||
@@ -52,7 +52,7 @@ If you want more control over your data, prefer to self-host Plane, please refer
|
||||
|
||||
| Installation Methods | Documentation Link |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Docker | [](https://docs.plane.so/self-hosting/methods/docker-compose) |
|
||||
| Docker | [](https://docs.plane.so/docker-compose) |
|
||||
| Kubernetes | [](https://docs.plane.so/kubernetes) |
|
||||
|
||||
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.17.0"
|
||||
"version": "0.16.0"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -38,7 +38,7 @@ from .workspace.base import (
|
||||
WorkSpaceAvailabilityCheckEndpoint,
|
||||
UserWorkspaceDashboardEndpoint,
|
||||
WorkspaceThemeViewSet,
|
||||
ExportWorkspaceUserActivityEndpoint
|
||||
ExportWorkspaceUserActivityEndpoint,
|
||||
)
|
||||
|
||||
from .workspace.member import (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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,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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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 $@
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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,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,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,7 +1,7 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"private": true,
|
||||
"version": "0.17.0",
|
||||
"version": "0.16.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {},
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "tsconfig",
|
||||
"version": "0.17.0",
|
||||
"version": "0.16.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.17.0",
|
||||
"version": "0.16.0",
|
||||
"private": true,
|
||||
"main": "./src/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />;
|
||||
};
|
||||
|
||||
@@ -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.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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("/"),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user