Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
205437921b | ||
|
|
0e4c40f5f1 |
@@ -291,4 +291,5 @@ jobs:
|
||||
${{ github.workspace }}/deploy/selfhost/swarm.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/swarm-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
|
||||
+1
-2
@@ -88,7 +88,6 @@ deploy/selfhost/plane-app/
|
||||
*storybook.log
|
||||
output.css
|
||||
|
||||
dev-editor
|
||||
# Redis
|
||||
*.rdb
|
||||
*.rdb.gz
|
||||
*.rdb.gz
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "^14.2.26",
|
||||
"next": "^14.2.25",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -39,15 +39,7 @@ class CycleSerializer(BaseSerializer):
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("end_date", None) is not None
|
||||
):
|
||||
project_id = self.initial_data.get("project_id") or (
|
||||
self.instance.project_id
|
||||
if self.instance and hasattr(self.instance, "project_id")
|
||||
else None
|
||||
)
|
||||
|
||||
if not project_id:
|
||||
raise serializers.ValidationError("Project ID is required")
|
||||
|
||||
project_id = self.initial_data.get("project_id") or self.instance.project_id
|
||||
is_start_date_end_date_equal = (
|
||||
True
|
||||
if str(data.get("start_date")) == str(data.get("end_date"))
|
||||
|
||||
@@ -16,6 +16,7 @@ class ProjectSerializer(BaseSerializer):
|
||||
member_role = serializers.IntegerField(read_only=True)
|
||||
is_deployed = serializers.BooleanField(read_only=True)
|
||||
cover_image_url = serializers.CharField(read_only=True)
|
||||
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
|
||||
@@ -4,6 +4,16 @@ from plane.api.views import IntakeIssueAPIEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
|
||||
@@ -39,7 +39,7 @@ from plane.db.models import (
|
||||
UserFavorite,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.utils.host import base_host
|
||||
|
||||
from .base import BaseAPIView
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
|
||||
@@ -141,10 +141,8 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
if pk:
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
|
||||
data = CycleSerializer(
|
||||
queryset,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
queryset, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||
@@ -156,11 +154,8 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
start_date__lte=timezone.now(), end_date__gte=timezone.now()
|
||||
)
|
||||
data = CycleSerializer(
|
||||
queryset,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
queryset, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -171,11 +166,8 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data,
|
||||
)
|
||||
|
||||
@@ -186,11 +178,8 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data,
|
||||
)
|
||||
|
||||
@@ -201,11 +190,8 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data,
|
||||
)
|
||||
|
||||
@@ -218,22 +204,16 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data,
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
context={"project": project},
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data,
|
||||
)
|
||||
|
||||
@@ -279,7 +259,7 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -351,7 +331,7 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -722,7 +702,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# Return all Cycle Issues
|
||||
return Response(
|
||||
@@ -1196,7 +1176,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -18,7 +18,7 @@ from plane.api.serializers import IntakeIssueSerializer, IssueSerializer
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State
|
||||
from plane.utils.host import base_host
|
||||
|
||||
from .base import BaseAPIView
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=False,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -56,8 +56,6 @@ from plane.db.models import (
|
||||
from plane.settings.storage import S3Storage
|
||||
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.host import base_host
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
|
||||
|
||||
class WorkspaceIssueAPIEndpoint(BaseAPIView):
|
||||
@@ -323,17 +321,6 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
|
||||
# Send the model activity
|
||||
model_activity.delay(
|
||||
model_name="issue",
|
||||
model_id=str(serializer.data["id"]),
|
||||
requested_data=request.data,
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -1061,7 +1048,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
# Get the storage metadata
|
||||
@@ -1121,7 +1108,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
current_instance=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
# Update the attachment
|
||||
|
||||
@@ -33,7 +33,6 @@ from plane.db.models import (
|
||||
|
||||
from .base import BaseAPIView
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ModuleAPIEndpoint(BaseAPIView):
|
||||
@@ -175,7 +174,7 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
module = Module.objects.get(pk=serializer.data["id"])
|
||||
serializer = ModuleSerializer(module)
|
||||
@@ -227,7 +226,7 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -281,7 +280,6 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps({"module_name": str(module.name)}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
module.delete()
|
||||
# Delete the module issues
|
||||
@@ -451,7 +449,6 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
|
||||
return Response(
|
||||
|
||||
@@ -30,7 +30,6 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ProjectAPIEndpoint(BaseAPIView):
|
||||
@@ -229,7 +228,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
serializer = ProjectSerializer(project)
|
||||
@@ -239,7 +238,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
@@ -248,7 +247,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
def patch(self, request, slug, pk):
|
||||
@@ -259,7 +258,9 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
intake_view = request.data.get("intake_view", project.intake_view)
|
||||
intake_view = request.data.get(
|
||||
"inbox_view", request.data.get("intake_view", project.intake_view)
|
||||
)
|
||||
|
||||
if project.archived_at:
|
||||
return Response(
|
||||
@@ -296,7 +297,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
serializer = ProjectSerializer(project)
|
||||
@@ -306,7 +307,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||
return Response(
|
||||
@@ -315,7 +316,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
def delete(self, request, slug, pk):
|
||||
@@ -333,7 +334,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
new_value=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
current_site=base_host(request=request, is_app=True),
|
||||
current_site=request.META.get("HTTP_ORIGIN"),
|
||||
event_id=project.id,
|
||||
old_identifier=None,
|
||||
new_identifier=None,
|
||||
|
||||
@@ -352,19 +352,8 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
@@ -394,19 +383,8 @@ class RelatedIssueSerializer(BaseSerializer):
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
class IssueAssigneeSerializer(BaseSerializer):
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.app.views import (
|
||||
IntakeViewSet,
|
||||
IntakeIssueViewSet,
|
||||
IntakeWorkItemDescriptionVersionEndpoint,
|
||||
)
|
||||
from plane.app.views import IntakeViewSet, IntakeIssueViewSet
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -57,14 +53,4 @@ urlpatterns = [
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-work-items/<uuid:work_item_id>/description-versions/",
|
||||
IntakeWorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="intake-work-item-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-work-items/<uuid:work_item_id>/description-versions/<uuid:pk>/",
|
||||
IntakeWorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="intake-work-item-versions",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -25,7 +25,7 @@ from plane.app.views import (
|
||||
IssueAttachmentV2Endpoint,
|
||||
IssueBulkUpdateDateEndpoint,
|
||||
IssueVersionEndpoint,
|
||||
WorkItemDescriptionVersionEndpoint,
|
||||
IssueDescriptionVersionEndpoint,
|
||||
IssueMetaEndpoint,
|
||||
IssueDetailIdentifierEndpoint,
|
||||
)
|
||||
@@ -263,22 +263,22 @@ urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
|
||||
IssueVersionEndpoint.as_view(),
|
||||
name="issue-versions",
|
||||
name="page-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
|
||||
IssueVersionEndpoint.as_view(),
|
||||
name="issue-versions",
|
||||
name="page-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:work_item_id>/description-versions/",
|
||||
WorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="work-item-versions",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
|
||||
IssueDescriptionVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:work_item_id>/description-versions/<uuid:pk>/",
|
||||
WorkItemDescriptionVersionEndpoint.as_view(),
|
||||
name="work-item-versions",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
|
||||
IssueDescriptionVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/meta/",
|
||||
|
||||
@@ -144,7 +144,7 @@ from .issue.sub_issue import SubIssuesEndpoint
|
||||
|
||||
from .issue.subscriber import IssueSubscriberViewSet
|
||||
|
||||
from .issue.version import IssueVersionEndpoint, WorkItemDescriptionVersionEndpoint
|
||||
from .issue.version import IssueVersionEndpoint, IssueDescriptionVersionEndpoint
|
||||
|
||||
from .module.base import (
|
||||
ModuleViewSet,
|
||||
@@ -184,11 +184,7 @@ from .estimate.base import (
|
||||
EstimatePointEndpoint,
|
||||
)
|
||||
|
||||
from .intake.base import (
|
||||
IntakeViewSet,
|
||||
IntakeIssueViewSet,
|
||||
IntakeWorkItemDescriptionVersionEndpoint,
|
||||
)
|
||||
from .intake.base import IntakeViewSet, IntakeIssueViewSet
|
||||
|
||||
from .analytic.base import (
|
||||
AnalyticsEndpoint,
|
||||
|
||||
@@ -51,7 +51,8 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.host import base_host
|
||||
|
||||
# Module imports
|
||||
from .. import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.utils.timezone_converter import convert_to_utc, user_timezone_converter
|
||||
@@ -334,7 +335,7 @@ class CycleViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(cycle, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -427,7 +428,7 @@ class CycleViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(cycle, status=status.HTTP_200_OK)
|
||||
@@ -540,7 +541,7 @@ class CycleViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# TODO: Soft delete the cycle break the onetoone relationship with cycle issue
|
||||
cycle.delete()
|
||||
@@ -1079,7 +1080,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -27,7 +27,7 @@ from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class CycleIssueViewSet(BaseViewSet):
|
||||
serializer_class = CycleIssueSerializer
|
||||
@@ -291,7 +291,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -317,7 +317,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
cycle_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -27,23 +27,16 @@ from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
IssueDescriptionVersion,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueCreateSerializer,
|
||||
IssueDetailSerializer,
|
||||
IssueSerializer,
|
||||
IntakeSerializer,
|
||||
IntakeIssueSerializer,
|
||||
IntakeIssueDetailSerializer,
|
||||
IssueDescriptionVersionDetailSerializer,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.bgtasks.issue_description_version_task import issue_description_version_task
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
from plane.utils.global_paginator import paginate
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IntakeViewSet(BaseViewSet):
|
||||
@@ -94,7 +87,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
serializer_class = IntakeIssueSerializer
|
||||
model = IntakeIssue
|
||||
|
||||
filterset_fields = ["status"]
|
||||
filterset_fields = ["statulls"]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
@@ -225,7 +218,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=ROLE.GUEST.value,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
@@ -290,16 +283,9 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
updated_issue=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
issue_id=str(serializer.data["id"]),
|
||||
user_id=request.user.id,
|
||||
is_creating=True,
|
||||
)
|
||||
intake_issue = (
|
||||
IntakeIssue.objects.select_related("issue")
|
||||
.prefetch_related("issue__labels", "issue__assignees")
|
||||
@@ -399,16 +385,13 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
),
|
||||
"description": issue_data.get("description", issue.description),
|
||||
}
|
||||
current_instance = json.dumps(
|
||||
IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
issue_serializer = IssueCreateSerializer(
|
||||
issue, data=issue_data, partial=True, context={"project_id": project_id}
|
||||
)
|
||||
|
||||
if issue_serializer.is_valid():
|
||||
|
||||
current_instance = issue
|
||||
# Log all the updates
|
||||
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
||||
if issue is not None:
|
||||
@@ -418,18 +401,15 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
updated_issue=current_instance,
|
||||
issue_id=str(pk),
|
||||
user_id=request.user.id,
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
return Response(
|
||||
@@ -487,7 +467,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=False,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=(intake_issue.id),
|
||||
)
|
||||
|
||||
@@ -569,7 +549,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=ROLE.GUEST.value,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
@@ -577,7 +557,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
issue = IntakeIssueDetailSerializer(intake_issue).data
|
||||
return Response(issue, status=status.HTTP_200_OK)
|
||||
@@ -604,81 +584,3 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
|
||||
intake_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class IntakeWorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
|
||||
def process_paginated_result(self, fields, results, timezone):
|
||||
paginated_data = results.values(*fields)
|
||||
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
paginated_data = user_timezone_converter(
|
||||
paginated_data, datetime_fields, timezone
|
||||
)
|
||||
|
||||
return paginated_data
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, work_item_id, pk=None):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=work_item_id
|
||||
)
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=ROLE.GUEST.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue.created_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
if pk:
|
||||
issue_description_version = IssueDescriptionVersion.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=work_item_id,
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
serializer = IssueDescriptionVersionDetailSerializer(
|
||||
issue_description_version
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
cursor = request.GET.get("cursor", None)
|
||||
|
||||
required_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"last_saved_at",
|
||||
"owned_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
|
||||
issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=work_item_id
|
||||
)
|
||||
|
||||
paginated_data = paginate(
|
||||
base_queryset=issue_description_versions_queryset,
|
||||
queryset=issue_description_versions_queryset,
|
||||
cursor=cursor,
|
||||
on_result=lambda results: self.process_paginated_result(
|
||||
required_fields, results, request.user.user_timezone
|
||||
),
|
||||
)
|
||||
return Response(paginated_data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -37,7 +37,7 @@ from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.utils.error_codes import ERROR_CODES
|
||||
from plane.utils.host import base_host
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet, BaseAPIView
|
||||
|
||||
@@ -259,7 +259,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue.archived_at = timezone.now().date()
|
||||
issue.save()
|
||||
@@ -287,7 +287,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue.archived_at = None
|
||||
issue.save()
|
||||
@@ -333,7 +333,7 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue.archived_at = timezone.now().date()
|
||||
bulk_archive_issues.append(issue)
|
||||
|
||||
@@ -21,7 +21,7 @@ from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.settings.storage import S3Storage
|
||||
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueAttachmentEndpoint(BaseAPIView):
|
||||
serializer_class = IssueAttachmentSerializer
|
||||
@@ -48,7 +48,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
current_instance=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -67,7 +67,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -155,7 +155,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -213,7 +213,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||
current_instance=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
# Update the attachment
|
||||
|
||||
@@ -61,7 +61,6 @@ from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.global_paginator import paginate
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.bgtasks.issue_description_version_task import issue_description_version_task
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueListEndpoint(BaseAPIView):
|
||||
@@ -380,7 +379,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue = (
|
||||
issue_queryset_grouper(
|
||||
@@ -430,7 +429,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
@@ -566,7 +565,7 @@ class IssueViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
recent_visited_task.delay(
|
||||
@@ -633,7 +632,7 @@ class IssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
current_instance = json.dumps(
|
||||
IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
@@ -651,7 +650,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
model_activity.delay(
|
||||
model_name="issue",
|
||||
@@ -660,7 +659,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
@@ -692,8 +691,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance={},
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
subscriber=False,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -1035,17 +1033,9 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
|
||||
"""
|
||||
Validate that start date is before target date.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
start = new_start or current_start
|
||||
target = new_target or current_target
|
||||
|
||||
# Convert string dates to datetime objects if they're strings
|
||||
if isinstance(start, str):
|
||||
start = datetime.strptime(start, "%Y-%m-%d").date()
|
||||
if isinstance(target, str):
|
||||
target = datetime.strptime(target, "%Y-%m-%d").date()
|
||||
|
||||
if start and target and start > target:
|
||||
return False
|
||||
return True
|
||||
@@ -1287,7 +1277,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
recent_visited_task.delay(
|
||||
|
||||
@@ -17,7 +17,7 @@ from plane.app.serializers import IssueCommentSerializer, CommentReactionSeriali
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.db.models import IssueComment, ProjectMember, CommentReaction, Project, Issue
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueCommentViewSet(BaseViewSet):
|
||||
serializer_class = IssueCommentSerializer
|
||||
@@ -87,7 +87,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -121,7 +121,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -144,7 +144,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -188,7 +188,7 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -222,7 +222,7 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
comment_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -15,7 +15,7 @@ from plane.app.serializers import IssueLinkSerializer
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import IssueLink
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueLinkViewSet(BaseViewSet):
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
@@ -52,7 +52,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -77,7 +77,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -98,7 +98,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue_link.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -15,7 +15,7 @@ from plane.app.serializers import IssueReactionSerializer
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.db.models import IssueReaction
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueReactionViewSet(BaseViewSet):
|
||||
serializer_class = IssueReactionSerializer
|
||||
@@ -53,7 +53,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -78,7 +78,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -27,7 +27,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.issue_relation_mapper import get_actual_relation
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueRelationViewSet(BaseViewSet):
|
||||
serializer_class = IssueRelationSerializer
|
||||
@@ -253,7 +253,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
if relation_type in ["blocking", "start_after", "finish_after"]:
|
||||
@@ -290,6 +290,6 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -22,7 +22,7 @@ from plane.db.models import Issue, IssueLink, FileAsset, CycleIssue
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
from collections import defaultdict
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class SubIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
@@ -176,7 +176,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
current_instance=json.dumps({"parent": str(sub_issue_id)}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for sub_issue_id in sub_issue_ids
|
||||
]
|
||||
|
||||
@@ -3,13 +3,7 @@ from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
IssueVersion,
|
||||
IssueDescriptionVersion,
|
||||
Project,
|
||||
ProjectMember,
|
||||
Issue,
|
||||
)
|
||||
from plane.db.models import IssueVersion, IssueDescriptionVersion
|
||||
from ..base import BaseAPIView
|
||||
from plane.app.serializers import (
|
||||
IssueVersionDetailSerializer,
|
||||
@@ -72,7 +66,7 @@ class IssueVersionEndpoint(BaseAPIView):
|
||||
return Response(paginated_data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
class IssueDescriptionVersionEndpoint(BaseAPIView):
|
||||
def process_paginated_result(self, fields, results, timezone):
|
||||
paginated_data = results.values(*fields)
|
||||
|
||||
@@ -84,34 +78,10 @@ class WorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
return paginated_data
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, work_item_id, pk=None):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=work_item_id
|
||||
)
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=ROLE.GUEST.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue.created_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, issue_id, pk=None):
|
||||
if pk:
|
||||
issue_description_version = IssueDescriptionVersion.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=work_item_id,
|
||||
pk=pk,
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
|
||||
serializer = IssueDescriptionVersionDetailSerializer(
|
||||
@@ -135,8 +105,8 @@ class WorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=work_item_id
|
||||
).order_by("-created_at")
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id
|
||||
)
|
||||
paginated_data = paginate(
|
||||
base_queryset=issue_description_versions_queryset,
|
||||
queryset=issue_description_versions_queryset,
|
||||
|
||||
@@ -61,7 +61,7 @@ from plane.utils.timezone_converter import user_timezone_converter
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from .. import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ModuleViewSet(BaseViewSet):
|
||||
model = Module
|
||||
@@ -376,7 +376,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
module = user_timezone_converter(
|
||||
@@ -768,7 +768,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
@@ -795,7 +795,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
current_instance=json.dumps({"module_name": str(module.name)}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for issue in module_issues
|
||||
]
|
||||
|
||||
@@ -34,7 +34,7 @@ from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPagina
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ModuleIssueViewSet(BaseViewSet):
|
||||
serializer_class = ModuleIssueSerializer
|
||||
@@ -221,7 +221,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for issue in issues
|
||||
]
|
||||
@@ -261,7 +261,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for module in modules
|
||||
]
|
||||
@@ -284,7 +284,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
module_issue.delete()
|
||||
|
||||
@@ -309,7 +309,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
module_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -39,7 +39,6 @@ from plane.utils.cache import cache_response
|
||||
from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ProjectViewSet(BaseViewSet):
|
||||
@@ -332,7 +331,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
serializer = ProjectListSerializer(project)
|
||||
@@ -342,7 +341,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
@@ -351,7 +350,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
except serializers.ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, pk=None):
|
||||
@@ -410,7 +409,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
serializer = ProjectListSerializer(project)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -420,7 +419,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The project name is already taken"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||
return Response(
|
||||
@@ -429,7 +428,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
except serializers.ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, pk):
|
||||
@@ -455,7 +454,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
new_value=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
current_site=base_host(request=request, is_app=True),
|
||||
current_site=request.META.get("HTTP_ORIGIN"),
|
||||
event_id=project.id,
|
||||
old_identifier=None,
|
||||
new_identifier=None,
|
||||
|
||||
@@ -27,7 +27,7 @@ from plane.db.models import (
|
||||
IssueUserProperty,
|
||||
)
|
||||
from plane.db.models.project import ProjectNetwork
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ProjectInvitationsViewset(BaseViewSet):
|
||||
serializer_class = ProjectMemberInviteSerializer
|
||||
@@ -99,7 +99,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
project_invitations = ProjectMemberInvite.objects.bulk_create(
|
||||
project_invitations, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
current_site = base_host(request=request, is_app=True)
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
|
||||
# Send invitations
|
||||
for invitation in project_invitations:
|
||||
|
||||
@@ -432,7 +432,7 @@ class IssueViewViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = IssueViewSerializer(issue_view)
|
||||
|
||||
@@ -29,7 +29,7 @@ class WebhookEndpoint(BaseAPIView):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"error": "URL already exists for the workspace"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
raise IntegrityError
|
||||
|
||||
|
||||
@@ -119,9 +119,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Get total members and role
|
||||
total_members = WorkspaceMember.objects.filter(
|
||||
workspace_id=serializer.data["id"]
|
||||
).count()
|
||||
total_members=WorkspaceMember.objects.filter(workspace_id=serializer.data["id"]).count()
|
||||
data = serializer.data
|
||||
data["total_members"] = total_members
|
||||
data["role"] = 20
|
||||
@@ -136,7 +134,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"slug": "The workspace with the slug already exists"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
@@ -169,9 +167,10 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
|
||||
role = WorkspaceMember.objects.filter(
|
||||
workspace=OuterRef("id"), member=request.user, is_active=True
|
||||
).values("role")
|
||||
role = (
|
||||
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member=request.user, is_active=True)
|
||||
.values("role")
|
||||
)
|
||||
|
||||
workspace = (
|
||||
Workspace.objects.prefetch_related(
|
||||
|
||||
@@ -36,7 +36,7 @@ from plane.db.models import (
|
||||
from .. import BaseViewSet
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
model = DraftIssue
|
||||
@@ -241,7 +241,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
if request.data.get("cycle_id", None):
|
||||
@@ -270,7 +270,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
if request.data.get("module_ids", []):
|
||||
@@ -300,7 +300,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for module in request.data.get("module_ids", [])
|
||||
]
|
||||
|
||||
@@ -7,6 +7,7 @@ import jwt
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.db.models import Count
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party modules
|
||||
@@ -25,8 +26,7 @@ from plane.bgtasks.event_tracking_task import workspace_invite_event
|
||||
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||
from plane.db.models import User, Workspace, WorkspaceMember, WorkspaceMemberInvite
|
||||
from plane.utils.cache import invalidate_cache, invalidate_cache_directly
|
||||
from plane.utils.host import base_host
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
from .. import BaseViewSet
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
workspace_invitations, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
current_site = base_host(request=request, is_app=True)
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
|
||||
# Send invitations
|
||||
for invitation in workspace_invitations:
|
||||
@@ -213,7 +213,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
|
||||
user=user.id if user is not None else None,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=get_client_ip(request=request),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="MEMBER_ACCEPTED",
|
||||
accepted_from="EMAIL",
|
||||
)
|
||||
|
||||
@@ -15,8 +15,8 @@ from plane.db.models import Profile, User, WorkspaceMemberInvite
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from .error import AuthenticationException, AUTHENTICATION_ERROR_CODES
|
||||
from plane.bgtasks.user_activation_email_task import user_activation_email
|
||||
from plane.utils.host import base_host
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
from plane.authentication.utils.host import base_host
|
||||
|
||||
|
||||
class Adapter:
|
||||
"""Common interface for all auth providers"""
|
||||
@@ -108,7 +108,7 @@ class Adapter:
|
||||
user.last_login_medium = self.provider
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = get_client_ip(request=self.request)
|
||||
user.last_login_ip = self.request.META.get("REMOTE_ADDR")
|
||||
user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT")
|
||||
user.token_updated_at = timezone.now()
|
||||
# If user is not active, send the activation email and set the user as active
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest
|
||||
# Third party imports
|
||||
from rest_framework.request import Request
|
||||
|
||||
# Module imports
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: bool = False, is_app: bool = False) -> str:
|
||||
def base_host(request, is_admin=False, is_space=False, is_app=False):
|
||||
"""Utility function to return host / origin from the request"""
|
||||
# Calculate the base origin from request
|
||||
base_origin = settings.WEB_URL or settings.APP_BASE_URL
|
||||
base_origin = str(
|
||||
request.META.get("HTTP_ORIGIN")
|
||||
or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"
|
||||
or f"""{"https" if request.is_secure() else "http"}://{request.get_host()}"""
|
||||
)
|
||||
|
||||
# Admin redirections
|
||||
if is_admin:
|
||||
@@ -36,5 +38,5 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space:
|
||||
return base_origin
|
||||
|
||||
|
||||
def user_ip(request: Request | HttpRequest) -> str:
|
||||
return get_client_ip(request=request)
|
||||
def user_ip(request):
|
||||
return str(request.META.get("REMOTE_ADDR"))
|
||||
|
||||
@@ -3,8 +3,8 @@ from django.contrib.auth import login
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from plane.utils.host import base_host
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
from plane.authentication.utils.host import base_host
|
||||
|
||||
|
||||
def user_login(request, user, is_app=False, is_admin=False, is_space=False):
|
||||
login(request=request, user=user)
|
||||
@@ -15,7 +15,7 @@ def user_login(request, user, is_app=False, is_admin=False, is_space=False):
|
||||
|
||||
device_info = {
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
|
||||
"ip_address": get_client_ip(request=request),
|
||||
"ip_address": request.META.get("REMOTE_ADDR", ""),
|
||||
"domain": base_host(
|
||||
request=request, is_app=is_app, is_admin=is_admin, is_space=is_space
|
||||
),
|
||||
|
||||
@@ -19,7 +19,7 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class SignInAuthEndpoint(View):
|
||||
def post(self, request):
|
||||
@@ -34,7 +34,7 @@ class SignInAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
# Base URL join
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
@@ -58,7 +58,7 @@ class SignInAuthEndpoint(View):
|
||||
params = exc.get_error_dict()
|
||||
# Next path
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -76,7 +76,7 @@ class SignInAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -92,7 +92,7 @@ class SignInAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -111,7 +111,7 @@ class SignInAuthEndpoint(View):
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
path = str(next_path)
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
|
||||
@@ -121,7 +121,7 @@ class SignInAuthEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -141,7 +141,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -161,7 +161,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -179,7 +179,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -197,7 +197,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -216,7 +216,7 @@ class SignUpAuthEndpoint(View):
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
@@ -225,7 +225,7 @@ class SignUpAuthEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GitHubOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
@@ -35,7 +35,7 @@ class GitHubOauthInitiateEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -49,7 +49,7 @@ class GitHubOauthInitiateEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -70,7 +70,7 @@ class GitHubCallbackEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -81,7 +81,7 @@ class GitHubCallbackEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -94,7 +94,7 @@ class GitHubCallbackEndpoint(View):
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
@@ -103,6 +103,6 @@ class GitHubCallbackEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -16,7 +16,7 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GitLabOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
@@ -24,7 +24,7 @@ class GitLabOauthInitiateEndpoint(View):
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(validate_next_path(next_path))
|
||||
request.session["next_path"] = str(next_path)
|
||||
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
@@ -35,7 +35,7 @@ class GitLabOauthInitiateEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -49,7 +49,7 @@ class GitLabOauthInitiateEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -81,7 +81,7 @@ class GitLabCallbackEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -94,7 +94,7 @@ class GitLabCallbackEndpoint(View):
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
@@ -103,6 +103,6 @@ class GitLabCallbackEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -18,7 +18,7 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GoogleOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
@@ -36,7 +36,7 @@ class GoogleOauthInitiateEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -51,7 +51,7 @@ class GoogleOauthInitiateEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -72,7 +72,7 @@ class GoogleCallbackEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
if not code:
|
||||
@@ -82,7 +82,7 @@ class GoogleCallbackEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = next_path
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
@@ -95,11 +95,11 @@ class GoogleCallbackEndpoint(View):
|
||||
# Get the redirection path
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host, str(validate_next_path(next_path)) if next_path else path)
|
||||
url = urljoin(base_host, str(next_path) if next_path else path)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -26,7 +26,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class MagicGenerateEndpoint(APIView):
|
||||
@@ -44,13 +43,14 @@ class MagicGenerateEndpoint(APIView):
|
||||
)
|
||||
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
origin = request.META.get("HTTP_ORIGIN", "/")
|
||||
email = request.data.get("email", "").strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
adapter = MagicCodeProvider(request=request, key=email)
|
||||
key, token = adapter.initiate()
|
||||
# If the smtp is configured send through here
|
||||
magic_link.delay(email, key, token)
|
||||
magic_link.delay(email, key, token, origin)
|
||||
return Response({"key": str(key)}, status=status.HTTP_200_OK)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
@@ -73,7 +73,7 @@ class MagicSignInEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -89,7 +89,7 @@ class MagicSignInEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -122,7 +122,7 @@ class MagicSignInEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -145,7 +145,7 @@ class MagicSignUpEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -159,7 +159,7 @@ class MagicSignUpEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -177,7 +177,7 @@ class MagicSignUpEndpoint(View):
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
path = str(next_path)
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
@@ -187,7 +187,7 @@ class MagicSignUpEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
|
||||
@@ -80,7 +80,7 @@ class ForgotPasswordEndpoint(APIView):
|
||||
if user:
|
||||
# Get the reset token for user
|
||||
uidb64, token = generate_password_token(user=user)
|
||||
current_site = base_host(request=request, is_app=True)
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
# send the forgot password email
|
||||
forgot_password.delay(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
|
||||
@@ -17,7 +17,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class SignInAuthSpaceEndpoint(View):
|
||||
@@ -33,7 +32,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -52,7 +51,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -68,7 +67,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -83,7 +82,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -100,7 +99,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -118,7 +117,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -136,7 +135,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
# Validate the email
|
||||
@@ -152,7 +151,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -167,7 +166,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -184,6 +183,6 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -15,7 +15,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
@@ -35,7 +34,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -67,7 +66,7 @@ class GitHubCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -78,7 +77,7 @@ class GitHubCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -94,6 +93,6 @@ class GitHubCallbackSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -15,7 +15,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GitLabOauthInitiateSpaceEndpoint(View):
|
||||
@@ -35,7 +34,7 @@ class GitLabOauthInitiateSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -67,7 +66,7 @@ class GitLabCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -78,7 +77,7 @@ class GitLabCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -94,6 +93,6 @@ class GitLabCallbackSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -15,7 +15,6 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GoogleOauthInitiateSpaceEndpoint(View):
|
||||
@@ -34,7 +33,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -47,7 +46,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -66,7 +65,7 @@ class GoogleCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
if not code:
|
||||
@@ -76,7 +75,7 @@ class GoogleCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = next_path
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
@@ -90,6 +89,6 @@ class GoogleCallbackSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -23,7 +23,7 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class MagicGenerateSpaceEndpoint(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
@@ -38,14 +38,14 @@ class MagicGenerateSpaceEndpoint(APIView):
|
||||
)
|
||||
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
origin = base_host(request=request, is_space=True)
|
||||
email = request.data.get("email", "").strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
adapter = MagicCodeProvider(request=request, key=email)
|
||||
key, token = adapter.initiate()
|
||||
# If the smtp is configured send through here
|
||||
magic_link.delay(email, key, token)
|
||||
magic_link.delay(email, key, token, origin)
|
||||
return Response({"key": str(key)}, status=status.HTTP_200_OK)
|
||||
except AuthenticationException as e:
|
||||
return Response(e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -67,7 +67,7 @@ class MagicSignInSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -80,7 +80,7 @@ class MagicSignInSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -121,7 +121,7 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
# Existing User
|
||||
@@ -134,7 +134,7 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -152,6 +152,6 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -90,7 +90,7 @@ class ForgotPasswordSpaceEndpoint(APIView):
|
||||
if user:
|
||||
# Get the reset token for user
|
||||
uidb64, token = generate_password_token(user=user)
|
||||
current_site = base_host(request=request, is_space=True)
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
# send the forgot password email
|
||||
forgot_password.delay(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.utils import timezone
|
||||
# Module imports
|
||||
from plane.authentication.utils.host import base_host, user_ip
|
||||
from plane.db.models import User
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class SignOutAuthSpaceEndpoint(View):
|
||||
@@ -22,8 +21,8 @@ class SignOutAuthSpaceEndpoint(View):
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
url = f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{next_path}"
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
url = f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{next_path}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -32,7 +32,7 @@ from plane.settings.redis import redis_instance
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.bgtasks.webhook_task import webhook_activity
|
||||
from plane.utils.issue_relation_mapper import get_inverse_relation
|
||||
from plane.utils.uuid import is_valid_uuid
|
||||
from plane.utils.valid_uuid import is_valid_uuid
|
||||
|
||||
|
||||
# Track Changes in name
|
||||
@@ -307,10 +307,6 @@ def track_labels(
|
||||
|
||||
# Set of newly added labels
|
||||
for added_label in added_labels:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(added_label):
|
||||
continue
|
||||
|
||||
label = Label.objects.get(pk=added_label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -331,10 +327,6 @@ def track_labels(
|
||||
|
||||
# Set of dropped labels
|
||||
for dropped_label in dropped_labels:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(dropped_label):
|
||||
continue
|
||||
|
||||
label = Label.objects.get(pk=dropped_label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -381,10 +373,6 @@ def track_assignees(
|
||||
|
||||
bulk_subscribers = []
|
||||
for added_asignee in added_assignees:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(added_asignee):
|
||||
continue
|
||||
|
||||
assignee = User.objects.get(pk=added_asignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -418,10 +406,6 @@ def track_assignees(
|
||||
)
|
||||
|
||||
for dropped_assignee in dropped_assginees:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(dropped_assignee):
|
||||
continue
|
||||
|
||||
assignee = User.objects.get(pk=dropped_assignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
|
||||
@@ -16,7 +16,7 @@ from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task
|
||||
def magic_link(email, key, token):
|
||||
def magic_link(email, key, token, current_site):
|
||||
try:
|
||||
(
|
||||
EMAIL_HOST,
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# Django imports
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db.models import Max
|
||||
from django.db import connection, transaction
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Project, Issue, IssueSequence
|
||||
from plane.utils.uuid import convert_uuid_to_integer
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Fix duplicate sequences"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Positional argument
|
||||
parser.add_argument("issue_identifier", type=str, help="Issue Identifier")
|
||||
|
||||
def strict_str_to_int(self, s):
|
||||
if not s.isdigit() and not (s.startswith("-") and s[1:].isdigit()):
|
||||
raise ValueError("Invalid integer string")
|
||||
return int(s)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
workspace_slug = input("Workspace slug: ")
|
||||
|
||||
if not workspace_slug:
|
||||
raise CommandError("Workspace slug is required")
|
||||
|
||||
issue_identifier = options.get("issue_identifier", False)
|
||||
|
||||
# Validate issue_identifier
|
||||
if not issue_identifier:
|
||||
raise CommandError("Issue identifier is required")
|
||||
|
||||
# Validate issue identifier
|
||||
try:
|
||||
identifier = issue_identifier.split("-")
|
||||
|
||||
if len(identifier) != 2:
|
||||
raise ValueError("Invalid issue identifier format")
|
||||
|
||||
project_identifier = identifier[0]
|
||||
issue_sequence = self.strict_str_to_int(identifier[1])
|
||||
|
||||
# Fetch the project
|
||||
project = Project.objects.get(
|
||||
identifier__iexact=project_identifier, workspace__slug=workspace_slug
|
||||
)
|
||||
|
||||
# Get the issues
|
||||
issues = Issue.objects.filter(project=project, sequence_id=issue_sequence)
|
||||
# Check if there are duplicate issues
|
||||
if not issues.count() > 1:
|
||||
raise CommandError(
|
||||
"No duplicate issues found with the given identifier"
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"{issues.count()} issues found with identifier {issue_identifier}"
|
||||
)
|
||||
)
|
||||
with transaction.atomic():
|
||||
# This ensures only one transaction per project can execute this code at a time
|
||||
lock_key = convert_uuid_to_integer(project.id)
|
||||
|
||||
# Acquire an exclusive lock using the project ID as the lock key
|
||||
with connection.cursor() as cursor:
|
||||
# Get an exclusive lock using the project ID as the lock key
|
||||
cursor.execute("SELECT pg_advisory_xact_lock(%s)", [lock_key])
|
||||
|
||||
# Get the maximum sequence ID for the project
|
||||
last_sequence = IssueSequence.objects.filter(project=project).aggregate(
|
||||
largest=Max("sequence")
|
||||
)["largest"]
|
||||
|
||||
bulk_issues = []
|
||||
bulk_issue_sequences = []
|
||||
|
||||
issue_sequence_map = {
|
||||
isq.issue_id: isq
|
||||
for isq in IssueSequence.objects.filter(project=project)
|
||||
}
|
||||
|
||||
# change the ids of duplicate issues
|
||||
for index, issue in enumerate(issues[1:]):
|
||||
updated_sequence_id = last_sequence + index + 1
|
||||
issue.sequence_id = updated_sequence_id
|
||||
bulk_issues.append(issue)
|
||||
|
||||
# Find the same issue sequence instance from the above queryset
|
||||
sequence_identifier = issue_sequence_map.get(issue.id)
|
||||
if sequence_identifier:
|
||||
sequence_identifier.sequence = updated_sequence_id
|
||||
bulk_issue_sequences.append(sequence_identifier)
|
||||
|
||||
Issue.objects.bulk_update(bulk_issues, ["sequence_id"])
|
||||
IssueSequence.objects.bulk_update(bulk_issue_sequences, ["sequence"])
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Sequence IDs updated successfully"))
|
||||
except Exception as e:
|
||||
raise CommandError(str(e))
|
||||
@@ -1,78 +0,0 @@
|
||||
import time
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from plane.db.models import Workspace
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Updates the slug of a soft-deleted workspace by appending the epoch timestamp"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"slug",
|
||||
type=str,
|
||||
help="The slug of the workspace to update",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Run the command without making any changes",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
slug = options["slug"]
|
||||
dry_run = options["dry_run"]
|
||||
|
||||
# Get the workspace with the specified slug
|
||||
try:
|
||||
workspace = Workspace.all_objects.get(slug=slug)
|
||||
except Workspace.DoesNotExist:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f"Workspace with slug '{slug}' not found.")
|
||||
)
|
||||
return
|
||||
|
||||
# Check if the workspace is soft-deleted
|
||||
if workspace.deleted_at is None:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"Workspace '{workspace.name}' (slug: {workspace.slug}) is not deleted."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Check if the slug already has a timestamp appended
|
||||
if "__" in workspace.slug and workspace.slug.split("__")[-1].isdigit():
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"Workspace '{workspace.name}' (slug: {workspace.slug}) already has a timestamp appended."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Get the deletion timestamp
|
||||
deletion_timestamp = int(workspace.deleted_at.timestamp())
|
||||
|
||||
# Create the new slug with the deletion timestamp
|
||||
new_slug = f"{workspace.slug}__{deletion_timestamp}"
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(
|
||||
f"Would update workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
workspace.slug = new_slug
|
||||
workspace.save(update_fields=["slug"])
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Updated workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"Error updating workspace '{workspace.name}': {str(e)}"
|
||||
)
|
||||
)
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
# Generated by Django 4.2.17 on 2025-03-04 19:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0092_alter_deprecateddashboardwidget_unique_together_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="page",
|
||||
name="moved_to_page",
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="page",
|
||||
name="moved_to_project",
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="pageversion",
|
||||
name="sub_pages_data",
|
||||
field=models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
]
|
||||
@@ -82,4 +82,4 @@ from .label import Label
|
||||
|
||||
from .device import Device, DeviceSession
|
||||
|
||||
from .sticky import Sticky
|
||||
from .sticky import Sticky
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models, transaction, connection
|
||||
from django.db import models, transaction
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
from django import apps
|
||||
@@ -15,8 +15,8 @@ from django import apps
|
||||
from plane.utils.html_processor import strip_tags
|
||||
from plane.db.mixins import SoftDeletionManager
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from .base import BaseModel
|
||||
from .project import ProjectBaseModel
|
||||
from plane.utils.uuid import convert_uuid_to_integer
|
||||
|
||||
|
||||
def get_default_properties():
|
||||
@@ -209,18 +209,11 @@ class Issue(ProjectBaseModel):
|
||||
|
||||
if self._state.adding:
|
||||
with transaction.atomic():
|
||||
# Create a lock for this specific project using an advisory lock
|
||||
# This ensures only one transaction per project can execute this code at a time
|
||||
lock_key = convert_uuid_to_integer(self.project.id)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
# Get an exclusive lock using the project ID as the lock key
|
||||
cursor.execute("SELECT pg_advisory_xact_lock(%s)", [lock_key])
|
||||
|
||||
# Get the last sequence for the project
|
||||
last_sequence = IssueSequence.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(largest=models.Max("sequence"))["largest"]
|
||||
last_sequence = (
|
||||
IssueSequence.objects.filter(project=self.project)
|
||||
.select_for_update()
|
||||
.aggregate(largest=models.Max("sequence"))["largest"]
|
||||
)
|
||||
self.sequence_id = last_sequence + 1 if last_sequence else 1
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
|
||||
@@ -50,8 +50,6 @@ class Page(BaseModel):
|
||||
projects = models.ManyToManyField(
|
||||
"db.Project", related_name="pages", through="db.ProjectPage"
|
||||
)
|
||||
moved_to_page = models.UUIDField(null=True, blank=True)
|
||||
moved_to_project = models.UUIDField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Page"
|
||||
@@ -174,7 +172,6 @@ class PageVersion(BaseModel):
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
description_json = models.JSONField(default=dict, blank=True)
|
||||
sub_pages_data = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Page Version"
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# Python imports
|
||||
from django.db.models.functions import Ln
|
||||
import pytz
|
||||
import time
|
||||
from django.utils import timezone
|
||||
from typing import Optional, Any, Tuple, Dict
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
@@ -152,34 +149,6 @@ class Workspace(BaseModel):
|
||||
return self.logo
|
||||
return None
|
||||
|
||||
def delete(
|
||||
self,
|
||||
using: Optional[str] = None,
|
||||
soft: bool = True,
|
||||
*args: Any,
|
||||
**kwargs: Any
|
||||
):
|
||||
"""
|
||||
Override the delete method to append epoch timestamp to the slug when soft deleting.
|
||||
|
||||
Args:
|
||||
using: The database alias to use for the deletion.
|
||||
soft: Whether to perform a soft delete (True) or hard delete (False).
|
||||
*args: Additional positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
# Call the parent class's delete method first
|
||||
result = super().delete(using=using, soft=soft, *args, **kwargs)
|
||||
|
||||
# If it's a soft delete and the model still exists (not hard deleted)
|
||||
if soft and hasattr(self, 'deleted_at') and self.deleted_at:
|
||||
# Use the deleted_at timestamp to update the slug
|
||||
deletion_timestamp: int = int(self.deleted_at.timestamp())
|
||||
self.slug = f"{self.slug}__{deletion_timestamp}"
|
||||
self.save(update_fields=["slug"])
|
||||
|
||||
return result
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Workspace"
|
||||
verbose_name_plural = "Workspaces"
|
||||
@@ -422,7 +391,7 @@ class WorkspaceHomePreference(BaseModel):
|
||||
class WorkspaceUserPreference(BaseModel):
|
||||
"""Preference for the workspace for a user"""
|
||||
|
||||
class UserPreferenceKeys(models.TextChoices):
|
||||
class UserPreferenceKeys(models.TextChoices):
|
||||
VIEWS = "views", "Views"
|
||||
ACTIVE_CYCLES = "active_cycles", "Active Cycles"
|
||||
ANALYTICS = "analytics", "Analytics"
|
||||
|
||||
@@ -33,7 +33,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
|
||||
class InstanceAdminEndpoint(BaseAPIView):
|
||||
@@ -218,7 +217,7 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
user.is_active = True
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = get_client_ip(request=request)
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||
user.token_updated_at = timezone.now()
|
||||
user.save()
|
||||
@@ -345,7 +344,7 @@ class InstanceAdminSignInEndpoint(View):
|
||||
user.is_active = True
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = get_client_ip(request=request)
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||
user.token_updated_at = timezone.now()
|
||||
user.save()
|
||||
|
||||
@@ -109,5 +109,5 @@ class InstanceWorkSpaceEndpoint(BaseAPIView):
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"slug": "The workspace with the slug already exists"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Module imports
|
||||
from plane.db.models import APIActivityLog
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
|
||||
class APITokenLogMiddleware:
|
||||
def __init__(self, get_response):
|
||||
@@ -29,7 +28,7 @@ class APITokenLogMiddleware:
|
||||
response.content.decode("utf-8") if response.content else None
|
||||
),
|
||||
response_code=response.status_code,
|
||||
ip_address=get_client_ip(request=request),
|
||||
ip_address=request.META.get("REMOTE_ADDR", None),
|
||||
user_agent=request.META.get("HTTP_USER_AGENT", None),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# Python imports
|
||||
import logging
|
||||
import time
|
||||
|
||||
# Django imports
|
||||
from django.http import HttpRequest
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.request import Request
|
||||
|
||||
# Module imports
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
api_logger = logging.getLogger("plane.api")
|
||||
|
||||
|
||||
class RequestLoggerMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def _should_log_route(self, request: Request | HttpRequest) -> bool:
|
||||
"""
|
||||
Determines whether a route should be logged based on the request and status code.
|
||||
"""
|
||||
# Don't log health checks
|
||||
if request.path == "/" and request.method == "GET":
|
||||
return False
|
||||
return True
|
||||
|
||||
def __call__(self, request):
|
||||
# get the start time
|
||||
start_time = time.time()
|
||||
|
||||
# Get the response
|
||||
response = self.get_response(request)
|
||||
|
||||
# calculate the duration
|
||||
duration = time.time() - start_time
|
||||
|
||||
# Check if logging is required
|
||||
log_true = self._should_log_route(request=request)
|
||||
|
||||
# If logging is not required, return the response
|
||||
if not log_true:
|
||||
return response
|
||||
|
||||
user_id = (
|
||||
request.user.id
|
||||
if getattr(request, "user")
|
||||
and getattr(request.user, "is_authenticated", False)
|
||||
else None
|
||||
)
|
||||
|
||||
user_agent = request.META.get("HTTP_USER_AGENT", "")
|
||||
|
||||
# Log the request information
|
||||
api_logger.info(
|
||||
f"{request.method} {request.get_full_path()} {response.status_code}",
|
||||
extra={
|
||||
"path": request.path,
|
||||
"method": request.method,
|
||||
"status_code": response.status_code,
|
||||
"duration_ms": int(duration * 1000),
|
||||
"remote_addr": get_client_ip(request),
|
||||
"user_agent": user_agent,
|
||||
"user_id": user_id,
|
||||
},
|
||||
)
|
||||
|
||||
# return the response
|
||||
return response
|
||||
@@ -22,7 +22,7 @@ SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
|
||||
DEBUG = int(os.environ.get("DEBUG", "0"))
|
||||
|
||||
# Allowed Hosts
|
||||
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
@@ -59,7 +59,6 @@ MIDDLEWARE = [
|
||||
"crum.CurrentRequestUserMiddleware",
|
||||
"django.middleware.gzip.GZipMiddleware",
|
||||
"plane.middleware.api_log_middleware.APITokenLogMiddleware",
|
||||
"plane.middleware.logger.RequestLoggerMiddleware",
|
||||
]
|
||||
|
||||
# Rest Framework settings
|
||||
@@ -315,7 +314,7 @@ ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
|
||||
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
|
||||
APP_BASE_URL = os.environ.get("APP_BASE_URL")
|
||||
LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL")
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
||||
|
||||
|
||||
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
|
||||
|
||||
|
||||
@@ -37,31 +37,26 @@ if not os.path.exists(LOG_DIR):
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": True,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
"json": {
|
||||
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
||||
"fmt": "%(levelname)s %(asctime)s %(module)s %(name)s %(message)s",
|
||||
},
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "json",
|
||||
"formatter": "verbose",
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"plane.api": {"level": "INFO", "handlers": ["console"], "propagate": False},
|
||||
"plane.worker": {"level": "INFO", "handlers": ["console"], "propagate": False},
|
||||
"plane.exception": {
|
||||
"level": "ERROR",
|
||||
"django.request": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"plane": {"handlers": ["console"], "level": "DEBUG", "propagate": False},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,10 +26,11 @@ if not os.path.exists(LOG_DIR):
|
||||
# Logging configuration
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": True,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "%(asctime)s [%(process)d] %(levelname)s %(name)s: %(message)s"
|
||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
"json": {
|
||||
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
||||
@@ -39,7 +40,7 @@ LOGGING = {
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "json",
|
||||
"formatter": "verbose",
|
||||
"level": "INFO",
|
||||
},
|
||||
"file": {
|
||||
@@ -58,19 +59,15 @@ LOGGING = {
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"plane.api": {
|
||||
"level": "DEBUG" if DEBUG else "INFO",
|
||||
"handlers": ["console"],
|
||||
"django": {"handlers": ["console", "file"], "level": "INFO", "propagate": True},
|
||||
"django.request": {
|
||||
"handlers": ["console", "file"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.worker": {
|
||||
"level": "DEBUG" if DEBUG else "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": False,
|
||||
},
|
||||
"plane.exception": {
|
||||
"plane": {
|
||||
"level": "DEBUG" if DEBUG else "ERROR",
|
||||
"handlers": ["console"],
|
||||
"handlers": ["console", "file"],
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,4 +41,4 @@ class WorkSpaceCreateReadUpdateDelete(AuthenticatedAPITest):
|
||||
response = self.client.post(
|
||||
url, {"name": "Plane", "slug": "pla-ne"}, format="json"
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
|
||||
self.assertEqual(response.status_code, status.HTTP_410_GONE)
|
||||
|
||||
@@ -8,8 +8,8 @@ from django.conf import settings
|
||||
|
||||
def log_exception(e):
|
||||
# Log the error
|
||||
logger = logging.getLogger("plane.exception")
|
||||
logger.error(str(e))
|
||||
logger = logging.getLogger("plane")
|
||||
logger.error(e)
|
||||
|
||||
if settings.DEBUG:
|
||||
# Print the traceback if in debug mode
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpRequest
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.request import Request
|
||||
|
||||
# Module imports
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: bool = False, is_app: bool = False) -> str:
|
||||
def base_host(request, is_admin=False, is_space=False, is_app=False):
|
||||
"""Utility function to return host / origin from the request"""
|
||||
# Calculate the base origin from request
|
||||
base_origin = settings.WEB_URL or settings.APP_BASE_URL
|
||||
|
||||
if not base_origin:
|
||||
raise ImproperlyConfigured("APP_BASE_URL or WEB_URL is not set")
|
||||
base_origin = str(
|
||||
request.META.get("HTTP_ORIGIN")
|
||||
or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"
|
||||
or f"""{"https" if request.is_secure() else "http"}://{request.get_host()}"""
|
||||
)
|
||||
|
||||
# Admin redirections
|
||||
if is_admin:
|
||||
@@ -41,5 +38,5 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space:
|
||||
return base_origin
|
||||
|
||||
|
||||
def user_ip(request: Request | HttpRequest) -> str:
|
||||
return get_client_ip(request=request)
|
||||
def user_ip(request):
|
||||
return str(request.META.get("REMOTE_ADDR"))
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def validate_next_path(next_path: str) -> str:
|
||||
"""Validates that next_path is a valid path and extracts only the path component."""
|
||||
parsed_url = urlparse(next_path)
|
||||
|
||||
# Ensure next_path is not an absolute URL
|
||||
if parsed_url.scheme or parsed_url.netloc:
|
||||
next_path = parsed_url.path # Extract only the path component
|
||||
|
||||
# Ensure it starts with a forward slash (indicating a valid relative path)
|
||||
if not next_path.startswith("/"):
|
||||
return ""
|
||||
|
||||
# Ensure it does not contain dangerous path traversal sequences
|
||||
if ".." in next_path:
|
||||
return ""
|
||||
|
||||
return next_path
|
||||
@@ -1,22 +0,0 @@
|
||||
# Python imports
|
||||
import uuid
|
||||
import hashlib
|
||||
|
||||
|
||||
def is_valid_uuid(uuid_str):
|
||||
"""Check if a string is a valid UUID version 4"""
|
||||
try:
|
||||
uuid_obj = uuid.UUID(uuid_str)
|
||||
return uuid_obj.version == 4
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def convert_uuid_to_integer(uuid_val: uuid.UUID) -> int:
|
||||
"""Convert a UUID to a 64-bit signed integer"""
|
||||
# Ensure UUID is a string
|
||||
uuid_value: str = str(uuid_val)
|
||||
# Hash to 64-bit signed int
|
||||
h: bytes = hashlib.sha256(uuid_value.encode()).digest()
|
||||
bigint: int = int.from_bytes(h[:8], byteorder="big", signed=True)
|
||||
return bigint
|
||||
@@ -0,0 +1,8 @@
|
||||
import uuid
|
||||
|
||||
def is_valid_uuid(uuid_str):
|
||||
try:
|
||||
uuid.UUID(uuid_str, version=4)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
@@ -43,7 +43,7 @@ scout-apm==3.1.0
|
||||
# xlsx generation
|
||||
openpyxl==3.1.2
|
||||
# logging
|
||||
python-json-logger==3.3.0
|
||||
python-json-logger==2.0.7
|
||||
# html parser
|
||||
beautifulsoup4==4.12.3
|
||||
# analytics
|
||||
|
||||
@@ -51,15 +51,15 @@ x-app-env: &app-env
|
||||
API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
|
||||
MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
|
||||
|
||||
|
||||
services:
|
||||
web:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
|
||||
command: node web/server.js web
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
replicas: ${WEB_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
@@ -67,10 +67,11 @@ services:
|
||||
space:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
|
||||
command: node space/server.js space
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
replicas: ${SPACE_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
@@ -79,10 +80,11 @@ services:
|
||||
admin:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
|
||||
command: node admin/server.js admin
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
replicas: ${ADMIN_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
depends_on:
|
||||
- api
|
||||
- web
|
||||
@@ -92,10 +94,11 @@ services:
|
||||
command: node live/dist/server.js live
|
||||
environment:
|
||||
<<: [*live-env]
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
replicas: ${LIVE_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
depends_on:
|
||||
- api
|
||||
- web
|
||||
@@ -103,10 +106,11 @@ services:
|
||||
api:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-api.sh
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
replicas: ${API_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
volumes:
|
||||
- logs_api:/code/plane/logs
|
||||
environment:
|
||||
@@ -119,10 +123,11 @@ services:
|
||||
worker:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
replicas: ${WORKER_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
volumes:
|
||||
- logs_worker:/code/plane/logs
|
||||
environment:
|
||||
@@ -136,10 +141,11 @@ services:
|
||||
beat-worker:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
replicas: ${BEAT_WORKER_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
volumes:
|
||||
- logs_beat-worker:/code/plane/logs
|
||||
environment:
|
||||
@@ -153,10 +159,11 @@ services:
|
||||
migrator:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: no
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
volumes:
|
||||
- logs_migrator:/code/plane/logs
|
||||
environment:
|
||||
@@ -169,10 +176,8 @@ services:
|
||||
plane-db:
|
||||
image: postgres:15.7-alpine
|
||||
command: postgres -c 'max_connections=1000'
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
<<: *db-env
|
||||
volumes:
|
||||
@@ -180,19 +185,15 @@ services:
|
||||
|
||||
plane-redis:
|
||||
image: valkey/valkey:7.2.5-alpine
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
plane-mq:
|
||||
image: rabbitmq:3.13.6-management-alpine
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
<<: *mq-env
|
||||
volumes:
|
||||
@@ -202,10 +203,8 @@ services:
|
||||
plane-minio:
|
||||
image: minio/minio:latest
|
||||
command: server /export --console-address ":9090"
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
<<: *minio-env
|
||||
volumes:
|
||||
@@ -214,17 +213,12 @@ services:
|
||||
# Comment this if you already have a reverse proxy running
|
||||
proxy:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- target: 80
|
||||
published: ${NGINX_PORT:-80}
|
||||
protocol: tcp
|
||||
mode: host
|
||||
- ${NGINX_PORT:-80}:80
|
||||
environment:
|
||||
<<: *proxy-env
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
depends_on:
|
||||
- web
|
||||
- api
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
x-db-env: &db-env
|
||||
PGHOST: ${PGHOST:-plane-db}
|
||||
PGDATABASE: ${PGDATABASE:-plane}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-plane}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-plane}
|
||||
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
|
||||
PGDATA: ${PGDATA:-/var/lib/postgresql/data}
|
||||
|
||||
x-redis-env: &redis-env
|
||||
REDIS_HOST: ${REDIS_HOST:-plane-redis}
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_URL: ${REDIS_URL:-redis://plane-redis:6379/}
|
||||
|
||||
x-minio-env: &minio-env
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
|
||||
x-aws-s3-env: &aws-s3-env
|
||||
AWS_REGION: ${AWS_REGION:-}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-access-key}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
|
||||
x-proxy-env: &proxy-env
|
||||
NGINX_PORT: ${NGINX_PORT:-80}
|
||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||
|
||||
x-mq-env: &mq-env # RabbitMQ Settings
|
||||
RABBITMQ_HOST: ${RABBITMQ_HOST:-plane-mq}
|
||||
RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane}
|
||||
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
|
||||
RABBITMQ_VHOST: ${RABBITMQ_VHOST:-plane}
|
||||
|
||||
x-live-env: &live-env
|
||||
API_BASE_URL: ${API_BASE_URL:-http://api:8000}
|
||||
|
||||
x-app-env: &app-env
|
||||
WEB_URL: ${WEB_URL:-http://localhost}
|
||||
DEBUG: ${DEBUG:-0}
|
||||
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
|
||||
GUNICORN_WORKERS: 1
|
||||
USE_MINIO: ${USE_MINIO:-1}
|
||||
DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
|
||||
SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
|
||||
API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
|
||||
MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
|
||||
|
||||
services:
|
||||
web:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
|
||||
command: node web/server.js web
|
||||
deploy:
|
||||
replicas: ${WEB_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
|
||||
space:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
|
||||
command: node space/server.js space
|
||||
deploy:
|
||||
replicas: ${SPACE_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
admin:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
|
||||
command: node admin/server.js admin
|
||||
deploy:
|
||||
replicas: ${ADMIN_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
depends_on:
|
||||
- api
|
||||
- web
|
||||
|
||||
live:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-live:${APP_RELEASE:-stable}
|
||||
command: node live/dist/server.js live
|
||||
environment:
|
||||
<<: [*live-env]
|
||||
deploy:
|
||||
replicas: ${LIVE_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
depends_on:
|
||||
- api
|
||||
- web
|
||||
|
||||
api:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-api.sh
|
||||
deploy:
|
||||
replicas: ${API_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
volumes:
|
||||
- logs_api:/code/plane/logs
|
||||
environment:
|
||||
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
|
||||
worker:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
deploy:
|
||||
replicas: ${WORKER_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
volumes:
|
||||
- logs_worker:/code/plane/logs
|
||||
environment:
|
||||
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
|
||||
beat-worker:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
deploy:
|
||||
replicas: ${BEAT_WORKER_REPLICAS:-1}
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
volumes:
|
||||
- logs_beat-worker:/code/plane/logs
|
||||
environment:
|
||||
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
|
||||
migrator:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
volumes:
|
||||
- logs_migrator:/code/plane/logs
|
||||
environment:
|
||||
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
# Comment this if you already have a database running
|
||||
plane-db:
|
||||
image: postgres:15.7-alpine
|
||||
command: postgres -c 'max_connections=1000'
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
environment:
|
||||
<<: *db-env
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
plane-redis:
|
||||
image: valkey/valkey:7.2.5-alpine
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
plane-mq:
|
||||
image: rabbitmq:3.13.6-management-alpine
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
environment:
|
||||
<<: *mq-env
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
|
||||
# Comment this if you using any external s3 compatible storage
|
||||
plane-minio:
|
||||
image: minio/minio:latest
|
||||
command: server /export --console-address ":9090"
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
environment:
|
||||
<<: *minio-env
|
||||
volumes:
|
||||
- uploads:/export
|
||||
|
||||
# Comment this if you already have a reverse proxy running
|
||||
proxy:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
|
||||
ports:
|
||||
- target: 80
|
||||
published: ${NGINX_PORT:-80}
|
||||
protocol: tcp
|
||||
mode: host
|
||||
environment:
|
||||
<<: *proxy-env
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
depends_on:
|
||||
- web
|
||||
- api
|
||||
- space
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
uploads:
|
||||
logs_api:
|
||||
logs_worker:
|
||||
logs_beat-worker:
|
||||
logs_migrator:
|
||||
rabbitmq_data:
|
||||
+12
-15
@@ -16,7 +16,7 @@ OS_NAME=$(uname)
|
||||
# Create necessary directories
|
||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
|
||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yml
|
||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/swarm-compose.yml
|
||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
||||
|
||||
function print_header() {
|
||||
@@ -150,33 +150,33 @@ function updateEnvFile() {
|
||||
function download() {
|
||||
cd $SCRIPT_DIR || exit 1
|
||||
TS=$(date +%s)
|
||||
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yml" ]
|
||||
if [ -f "$PLANE_INSTALL_DIR/swarm-compose.yml" ]
|
||||
then
|
||||
mv $PLANE_INSTALL_DIR/docker-compose.yml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml
|
||||
mv $PLANE_INSTALL_DIR/swarm-compose.yml $PLANE_INSTALL_DIR/archive/$TS.swarm-compose.yml
|
||||
fi
|
||||
|
||||
echo $RELEASE_DOWNLOAD_URL
|
||||
echo $FALLBACK_DOWNLOAD_URL
|
||||
echo $APP_RELEASE
|
||||
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)")
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/swarm-compose.yml?$(date +%s)")
|
||||
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ "$STATUS" -eq 200 ]; then
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yml
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/swarm-compose.yml
|
||||
else
|
||||
# Fallback to download from the raw github url
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)")
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/swarm-compose.yml?$(date +%s)")
|
||||
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ "$STATUS" -eq 200 ]; then
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yml
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/swarm-compose.yml
|
||||
else
|
||||
echo "Failed to download docker-compose.yml. HTTP Status: $STATUS"
|
||||
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml"
|
||||
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml $PLANE_INSTALL_DIR/docker-compose.yml
|
||||
echo "Failed to download swarm-compose.yml. HTTP Status: $STATUS"
|
||||
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/swarm-compose.yml"
|
||||
mv $PLANE_INSTALL_DIR/archive/$TS.swarm-compose.yml $PLANE_INSTALL_DIR/swarm-compose.yml
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -198,7 +198,7 @@ function download() {
|
||||
else
|
||||
echo "Failed to download variables.env. HTTP Status: $STATUS"
|
||||
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env"
|
||||
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml $PLANE_INSTALL_DIR/docker-compose.yml
|
||||
mv $PLANE_INSTALL_DIR/archive/$TS.swarm-compose.yml $PLANE_INSTALL_DIR/swarm-compose.yml
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -266,11 +266,8 @@ function deployStack() {
|
||||
echo "Please check the logs for the 'migrator' service and resolve the issue(s)."
|
||||
echo "Stop the services by running the command: ./swarm.sh stop"
|
||||
exit 1
|
||||
else
|
||||
echo " Data Migration completed successfully ✅"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Could not find migrator container to check exit status"
|
||||
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -8,18 +8,3 @@ export const ISSUE_REACTION_EMOJI_CODES = [
|
||||
"9992",
|
||||
"128064",
|
||||
];
|
||||
|
||||
export const RANDOM_EMOJI_CODES = [
|
||||
"8986",
|
||||
"9200",
|
||||
"128204",
|
||||
"127773",
|
||||
"127891",
|
||||
"128076",
|
||||
"128077",
|
||||
"128187",
|
||||
"128188",
|
||||
"128512",
|
||||
"128522",
|
||||
"128578",
|
||||
];
|
||||
|
||||
@@ -1,97 +1,91 @@
|
||||
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
|
||||
|
||||
export enum EInboxIssueCurrentTab {
|
||||
OPEN = "open",
|
||||
CLOSED = "closed",
|
||||
OPEN = "open",
|
||||
CLOSED = "closed",
|
||||
}
|
||||
|
||||
export enum EInboxIssueStatus {
|
||||
PENDING = -2,
|
||||
DECLINED = -1,
|
||||
SNOOZED = 0,
|
||||
ACCEPTED = 1,
|
||||
DUPLICATE = 2,
|
||||
}
|
||||
|
||||
export enum EInboxIssueSource {
|
||||
IN_APP = "IN_APP",
|
||||
FORMS = "FORMS",
|
||||
EMAIL = "EMAIL",
|
||||
PENDING = -2,
|
||||
DECLINED = -1,
|
||||
SNOOZED = 0,
|
||||
ACCEPTED = 1,
|
||||
DUPLICATE = 2,
|
||||
}
|
||||
|
||||
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
|
||||
export type TInboxIssueStatus = EInboxIssueStatus;
|
||||
export type TInboxIssue = {
|
||||
id: string;
|
||||
status: TInboxIssueStatus;
|
||||
snoozed_till: Date | null;
|
||||
duplicate_to: string | undefined;
|
||||
source: EInboxIssueSource | undefined;
|
||||
issue: TIssue;
|
||||
created_by: string;
|
||||
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
|
||||
id: string;
|
||||
status: TInboxIssueStatus;
|
||||
snoozed_till: Date | null;
|
||||
duplicate_to: string | undefined;
|
||||
source: string;
|
||||
issue: TIssue;
|
||||
created_by: string;
|
||||
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
|
||||
};
|
||||
|
||||
export const INBOX_STATUS: {
|
||||
key: string;
|
||||
status: TInboxIssueStatus;
|
||||
i18n_title: string;
|
||||
i18n_description: () => string;
|
||||
key: string;
|
||||
status: TInboxIssueStatus;
|
||||
i18n_title: string;
|
||||
i18n_description: () => string;
|
||||
}[] = [
|
||||
{
|
||||
key: "pending",
|
||||
i18n_title: "inbox_issue.status.pending.title",
|
||||
status: EInboxIssueStatus.PENDING,
|
||||
i18n_description: () => `inbox_issue.status.pending.description`,
|
||||
},
|
||||
{
|
||||
key: "declined",
|
||||
i18n_title: "inbox_issue.status.declined.title",
|
||||
status: EInboxIssueStatus.DECLINED,
|
||||
i18n_description: () => `inbox_issue.status.declined.description`,
|
||||
},
|
||||
{
|
||||
key: "snoozed",
|
||||
i18n_title: "inbox_issue.status.snoozed.title",
|
||||
status: EInboxIssueStatus.SNOOZED,
|
||||
i18n_description: () => `inbox_issue.status.snoozed.description`,
|
||||
},
|
||||
{
|
||||
key: "accepted",
|
||||
i18n_title: "inbox_issue.status.accepted.title",
|
||||
status: EInboxIssueStatus.ACCEPTED,
|
||||
i18n_description: () => `inbox_issue.status.accepted.description`,
|
||||
},
|
||||
{
|
||||
key: "duplicate",
|
||||
i18n_title: "inbox_issue.status.duplicate.title",
|
||||
status: EInboxIssueStatus.DUPLICATE,
|
||||
i18n_description: () => `inbox_issue.status.duplicate.description`,
|
||||
},
|
||||
{
|
||||
key: "pending",
|
||||
i18n_title: "inbox_issue.status.pending.title",
|
||||
status: EInboxIssueStatus.PENDING,
|
||||
i18n_description: () => `inbox_issue.status.pending.description`,
|
||||
},
|
||||
{
|
||||
key: "declined",
|
||||
i18n_title: "inbox_issue.status.declined.title",
|
||||
status: EInboxIssueStatus.DECLINED,
|
||||
i18n_description: () => `inbox_issue.status.declined.description`,
|
||||
},
|
||||
{
|
||||
key: "snoozed",
|
||||
i18n_title: "inbox_issue.status.snoozed.title",
|
||||
status: EInboxIssueStatus.SNOOZED,
|
||||
i18n_description: () => `inbox_issue.status.snoozed.description`,
|
||||
},
|
||||
{
|
||||
key: "accepted",
|
||||
i18n_title: "inbox_issue.status.accepted.title",
|
||||
status: EInboxIssueStatus.ACCEPTED,
|
||||
i18n_description: () => `inbox_issue.status.accepted.description`,
|
||||
},
|
||||
{
|
||||
key: "duplicate",
|
||||
i18n_title: "inbox_issue.status.duplicate.title",
|
||||
status: EInboxIssueStatus.DUPLICATE,
|
||||
i18n_description: () => `inbox_issue.status.duplicate.description`,
|
||||
},
|
||||
];
|
||||
|
||||
export const INBOX_ISSUE_ORDER_BY_OPTIONS = [
|
||||
{
|
||||
key: "issue__created_at",
|
||||
i18n_label: "inbox_issue.order_by.created_at",
|
||||
},
|
||||
{
|
||||
key: "issue__updated_at",
|
||||
i18n_label: "inbox_issue.order_by.updated_at",
|
||||
},
|
||||
{
|
||||
key: "issue__sequence_id",
|
||||
i18n_label: "inbox_issue.order_by.id",
|
||||
},
|
||||
{
|
||||
key: "issue__created_at",
|
||||
i18n_label: "inbox_issue.order_by.created_at",
|
||||
},
|
||||
{
|
||||
key: "issue__updated_at",
|
||||
i18n_label: "inbox_issue.order_by.updated_at",
|
||||
},
|
||||
{
|
||||
key: "issue__sequence_id",
|
||||
i18n_label: "inbox_issue.order_by.id",
|
||||
},
|
||||
];
|
||||
|
||||
export const INBOX_ISSUE_SORT_BY_OPTIONS = [
|
||||
{
|
||||
key: "asc",
|
||||
i18n_label: "common.sort.asc",
|
||||
},
|
||||
{
|
||||
key: "desc",
|
||||
i18n_label: "common.sort.desc",
|
||||
},
|
||||
{
|
||||
key: "asc",
|
||||
i18n_label: "common.sort.asc",
|
||||
},
|
||||
{
|
||||
key: "desc",
|
||||
i18n_label: "common.sort.desc",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -14,7 +14,6 @@ export * from "./state";
|
||||
export * from "./swr";
|
||||
export * from "./tab-indices";
|
||||
export * from "./user";
|
||||
export * from "./payment";
|
||||
export * from "./workspace";
|
||||
export * from "./stickies";
|
||||
export * from "./cycle";
|
||||
@@ -30,5 +29,3 @@ export * from "./event-tracker";
|
||||
export * from "./spreadsheet";
|
||||
export * from "./dashboard";
|
||||
export * from "./page";
|
||||
export * from "./emoji";
|
||||
export * from "./subscription";
|
||||
|
||||
@@ -41,7 +41,6 @@ export enum EIssueGroupBYServerToProperty {
|
||||
export enum EIssueServiceType {
|
||||
ISSUES = "issues",
|
||||
EPICS = "epics",
|
||||
WORK_ITEMS = "work-items",
|
||||
}
|
||||
|
||||
export enum EIssuesStoreType {
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import { IPaymentProduct, TBillingFrequency, TProductBillingFrequency } from "@plane/types";
|
||||
|
||||
/**
|
||||
* Enum representing different product subscription types
|
||||
*/
|
||||
export enum EProductSubscriptionEnum {
|
||||
FREE = "FREE",
|
||||
ONE = "ONE",
|
||||
PRO = "PRO",
|
||||
BUSINESS = "BUSINESS",
|
||||
ENTERPRISE = "ENTERPRISE",
|
||||
}
|
||||
|
||||
/**
|
||||
* Default billing frequency for each product subscription type
|
||||
*/
|
||||
export const DEFAULT_PRODUCT_BILLING_FREQUENCY: TProductBillingFrequency = {
|
||||
[EProductSubscriptionEnum.FREE]: undefined,
|
||||
[EProductSubscriptionEnum.ONE]: undefined,
|
||||
[EProductSubscriptionEnum.PRO]: "month",
|
||||
[EProductSubscriptionEnum.BUSINESS]: "month",
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: "month",
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscription types that support billing frequency toggle (monthly/yearly)
|
||||
*/
|
||||
export const SUBSCRIPTION_WITH_BILLING_FREQUENCY = [
|
||||
EProductSubscriptionEnum.PRO,
|
||||
EProductSubscriptionEnum.BUSINESS,
|
||||
EProductSubscriptionEnum.ENTERPRISE,
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapping of product subscription types to their respective payment product details
|
||||
* Used to provide information about each product's pricing and features
|
||||
*/
|
||||
export const PLANE_COMMUNITY_PRODUCTS: Record<string, IPaymentProduct> = {
|
||||
[EProductSubscriptionEnum.PRO]: {
|
||||
id: EProductSubscriptionEnum.PRO,
|
||||
name: "Plane Pro",
|
||||
description:
|
||||
"More views, more cycles powers, more pages features, new reports, and better dashboards are waiting to be unlocked.",
|
||||
type: "PRO",
|
||||
prices: [
|
||||
{
|
||||
id: `price_monthly_${EProductSubscriptionEnum.PRO}`,
|
||||
unit_amount: 800,
|
||||
recurring: "month",
|
||||
currency: "usd",
|
||||
workspace_amount: 800,
|
||||
product: EProductSubscriptionEnum.PRO,
|
||||
},
|
||||
{
|
||||
id: `price_yearly_${EProductSubscriptionEnum.PRO}`,
|
||||
unit_amount: 7200,
|
||||
recurring: "year",
|
||||
currency: "usd",
|
||||
workspace_amount: 7200,
|
||||
product: EProductSubscriptionEnum.PRO,
|
||||
},
|
||||
],
|
||||
payment_quantity: 1,
|
||||
is_active: true,
|
||||
},
|
||||
[EProductSubscriptionEnum.BUSINESS]: {
|
||||
id: EProductSubscriptionEnum.BUSINESS,
|
||||
name: "Plane Business",
|
||||
description:
|
||||
"The earliest packaging of Business at $10 a seat a month billed annually, $12 a seat a month billed monthly for Plane Cloud",
|
||||
type: "BUSINESS",
|
||||
prices: [
|
||||
{
|
||||
id: `price_yearly_${EProductSubscriptionEnum.BUSINESS}`,
|
||||
unit_amount: 0,
|
||||
recurring: "year",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
product: EProductSubscriptionEnum.BUSINESS,
|
||||
},
|
||||
{
|
||||
id: `price_monthly_${EProductSubscriptionEnum.BUSINESS}`,
|
||||
unit_amount: 0,
|
||||
recurring: "month",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
product: EProductSubscriptionEnum.BUSINESS,
|
||||
},
|
||||
],
|
||||
payment_quantity: 1,
|
||||
is_active: false,
|
||||
},
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: {
|
||||
id: EProductSubscriptionEnum.ENTERPRISE,
|
||||
name: "Plane Enterprise",
|
||||
description: "",
|
||||
type: "ENTERPRISE",
|
||||
prices: [
|
||||
{
|
||||
id: `price_yearly_${EProductSubscriptionEnum.ENTERPRISE}`,
|
||||
unit_amount: 0,
|
||||
recurring: "year",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
product: EProductSubscriptionEnum.ENTERPRISE,
|
||||
},
|
||||
{
|
||||
id: `price_monthly_${EProductSubscriptionEnum.ENTERPRISE}`,
|
||||
unit_amount: 0,
|
||||
recurring: "month",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
product: EProductSubscriptionEnum.ENTERPRISE,
|
||||
},
|
||||
],
|
||||
payment_quantity: 1,
|
||||
is_active: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* URL for the "Talk to Sales" page where users can contact sales team
|
||||
*/
|
||||
export const TALK_TO_SALES_URL = "https://plane.so/talk-to-sales";
|
||||
|
||||
/**
|
||||
* Mapping of subscription types to their respective upgrade/redirection URLs based on billing frequency
|
||||
* Used for self-hosted installations to redirect users to appropriate upgrade pages
|
||||
*/
|
||||
export const SUBSCRIPTION_REDIRECTION_URLS: Record<EProductSubscriptionEnum, Record<TBillingFrequency, string>> = {
|
||||
[EProductSubscriptionEnum.FREE]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
year: TALK_TO_SALES_URL,
|
||||
},
|
||||
[EProductSubscriptionEnum.ONE]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
year: TALK_TO_SALES_URL,
|
||||
},
|
||||
[EProductSubscriptionEnum.PRO]: {
|
||||
month: "https://app.plane.so/upgrade/pro/self-hosted?plan=month",
|
||||
year: "https://app.plane.so/upgrade/pro/self-hosted?plan=year",
|
||||
},
|
||||
[EProductSubscriptionEnum.BUSINESS]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
year: TALK_TO_SALES_URL,
|
||||
},
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
year: TALK_TO_SALES_URL,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping of subscription types to their respective marketing webpage URLs
|
||||
* Used to direct users to learn more about each plan's features and pricing
|
||||
*/
|
||||
export const SUBSCRIPTION_WEBPAGE_URLS: Record<EProductSubscriptionEnum, string> = {
|
||||
[EProductSubscriptionEnum.FREE]: TALK_TO_SALES_URL,
|
||||
[EProductSubscriptionEnum.ONE]: TALK_TO_SALES_URL,
|
||||
[EProductSubscriptionEnum.PRO]: "https://plane.so/pro",
|
||||
[EProductSubscriptionEnum.BUSINESS]: "https://plane.so/business",
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: "https://plane.so/business",
|
||||
};
|
||||
@@ -1,7 +1,5 @@
|
||||
// plane imports
|
||||
import { IProject, TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
|
||||
// local imports
|
||||
import { RANDOM_EMOJI_CODES } from "./emoji";
|
||||
// icons
|
||||
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
|
||||
|
||||
export type TNetworkChoiceIconKey = "Lock" | "Globe2";
|
||||
|
||||
@@ -134,18 +132,3 @@ export const PROJECT_ERROR_MESSAGES = {
|
||||
i18n_message: "workspace_projects.error.issue_delete",
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_PROJECT_FORM_VALUES: Partial<IProject> = {
|
||||
cover_image_url: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)],
|
||||
description: "",
|
||||
logo_props: {
|
||||
in_use: "emoji",
|
||||
emoji: {
|
||||
value: RANDOM_EMOJI_CODES[Math.floor(Math.random() * RANDOM_EMOJI_CODES.length)],
|
||||
},
|
||||
},
|
||||
identifier: "",
|
||||
name: "",
|
||||
network: 2,
|
||||
project_lead: null,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||
export type TStateGroups =
|
||||
| "backlog"
|
||||
| "unstarted"
|
||||
| "started"
|
||||
| "completed"
|
||||
| "cancelled";
|
||||
|
||||
export type TDraggableData = {
|
||||
groupKey: TStateGroups;
|
||||
@@ -9,43 +14,40 @@ export const STATE_GROUPS: {
|
||||
[key in TStateGroups]: {
|
||||
key: TStateGroups;
|
||||
label: string;
|
||||
defaultStateName: string;
|
||||
color: string;
|
||||
};
|
||||
} = {
|
||||
backlog: {
|
||||
key: "backlog",
|
||||
label: "Backlog",
|
||||
defaultStateName: "Backlog",
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
unstarted: {
|
||||
key: "unstarted",
|
||||
label: "Unstarted",
|
||||
defaultStateName: "Todo",
|
||||
color: "#3f76ff",
|
||||
},
|
||||
started: {
|
||||
key: "started",
|
||||
label: "Started",
|
||||
defaultStateName: "In Progress",
|
||||
color: "#f59e0b",
|
||||
},
|
||||
completed: {
|
||||
key: "completed",
|
||||
label: "Completed",
|
||||
defaultStateName: "Done",
|
||||
color: "#16a34a",
|
||||
},
|
||||
cancelled: {
|
||||
key: "cancelled",
|
||||
label: "Canceled",
|
||||
defaultStateName: "Cancelled",
|
||||
color: "#dc2626",
|
||||
},
|
||||
};
|
||||
|
||||
export const ARCHIVABLE_STATE_GROUPS = [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key];
|
||||
export const ARCHIVABLE_STATE_GROUPS = [
|
||||
STATE_GROUPS.completed.key,
|
||||
STATE_GROUPS.cancelled.key,
|
||||
];
|
||||
export const COMPLETED_STATE_GROUPS = [STATE_GROUPS.completed.key];
|
||||
export const PENDING_STATE_GROUPS = [
|
||||
STATE_GROUPS.backlog.key,
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
export const ENTERPRISE_PLAN_FEATURES = [
|
||||
"Private + managed deployments",
|
||||
"GAC",
|
||||
"LDAP support",
|
||||
"Databases + Formulas",
|
||||
"Unlimited and full Automation Flows",
|
||||
"Full-suite professional services",
|
||||
];
|
||||
|
||||
export const BUSINESS_PLAN_FEATURES = [
|
||||
"Project Templates",
|
||||
"Workflows + Approvals",
|
||||
"Decision + Loops Automation",
|
||||
"Custom Reports",
|
||||
"Nested Pages",
|
||||
"Intake Forms",
|
||||
];
|
||||
|
||||
export const PRO_PLAN_FEATURES = [
|
||||
"Dashboards + Reports",
|
||||
"Full Time Tracking + Bulk Ops",
|
||||
"Teamspaces",
|
||||
"Trigger And Action",
|
||||
"Wikis",
|
||||
"Popular integrations",
|
||||
];
|
||||
|
||||
export const ONE_PLAN_FEATURES = [
|
||||
"OIDC + SAML for SSO",
|
||||
"Active Cycles",
|
||||
"Real-time collab + public views and page",
|
||||
"Link pages in issues and vice-versa",
|
||||
"Time-tracking + limited bulk ops",
|
||||
"Docker, Kubernetes and more",
|
||||
];
|
||||
|
||||
export const FREE_PLAN_UPGRADE_FEATURES = [
|
||||
"OIDC + SAML for SSO",
|
||||
"Time Tracking and Bulk Ops",
|
||||
"Integrations",
|
||||
"Public Views and Pages",
|
||||
];
|
||||
@@ -1,6 +1,20 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import {
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
flip,
|
||||
hide,
|
||||
shift,
|
||||
useDismiss,
|
||||
useFloating,
|
||||
useInteractions,
|
||||
} from "@floating-ui/react";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { EditorView } from "@tiptap/pm/view";
|
||||
import { Editor, ReactRenderer } from "@tiptap/react";
|
||||
// components
|
||||
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
||||
import { LinkView, LinkViewProps } from "@/components/links";
|
||||
import { AIFeaturesMenu, BlockMenu, EditorBubbleMenu } from "@/components/menus";
|
||||
// types
|
||||
import { TAIHandler, TDisplayConfig } from "@/types";
|
||||
@@ -17,24 +31,133 @@ type IPageRenderer = {
|
||||
|
||||
export const PageRenderer = (props: IPageRenderer) => {
|
||||
const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
|
||||
// states
|
||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [coordinates, setCoordinates] = useState<{ x: number; y: number }>();
|
||||
const [cleanup, setCleanup] = useState(() => () => {});
|
||||
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
open: isOpen,
|
||||
onOpenChange: setIsOpen,
|
||||
middleware: [flip(), shift(), hide({ strategy: "referenceHidden" })],
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
const dismiss = useDismiss(context, {
|
||||
ancestorScroll: true,
|
||||
});
|
||||
|
||||
const { getFloatingProps } = useInteractions([dismiss]);
|
||||
|
||||
const floatingElementRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const closeLinkView = () => setIsOpen(false);
|
||||
|
||||
const handleLinkHover = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (!editor) return;
|
||||
const target = event.target as HTMLElement;
|
||||
const view = editor.view as EditorView;
|
||||
|
||||
if (!target || !view) return;
|
||||
const pos = view.posAtDOM(target, 0);
|
||||
if (!pos || pos < 0) return;
|
||||
|
||||
if (target.nodeName !== "A") return;
|
||||
|
||||
const node = view.state.doc.nodeAt(pos) as Node;
|
||||
if (!node || !node.isAtom) return;
|
||||
|
||||
// we need to check if any of the marks are links
|
||||
const marks = node.marks;
|
||||
|
||||
if (!marks) return;
|
||||
|
||||
const linkMark = marks.find((mark) => mark.type.name === "link");
|
||||
|
||||
if (!linkMark) return;
|
||||
|
||||
if (floatingElementRef.current) {
|
||||
floatingElementRef.current?.remove();
|
||||
}
|
||||
|
||||
if (cleanup) cleanup();
|
||||
|
||||
const href = linkMark.attrs.href;
|
||||
const componentLink = new ReactRenderer(LinkView, {
|
||||
props: {
|
||||
view: "LinkPreview",
|
||||
url: href,
|
||||
editor: editor,
|
||||
from: pos,
|
||||
to: pos + node.nodeSize,
|
||||
},
|
||||
editor,
|
||||
});
|
||||
|
||||
const referenceElement = target as HTMLElement;
|
||||
const floatingElement = componentLink.element as HTMLElement;
|
||||
|
||||
floatingElementRef.current = floatingElement;
|
||||
|
||||
const cleanupFunc = autoUpdate(referenceElement, floatingElement, () => {
|
||||
computePosition(referenceElement, floatingElement, {
|
||||
placement: "bottom",
|
||||
middleware: [
|
||||
flip(),
|
||||
shift(),
|
||||
hide({
|
||||
strategy: "referenceHidden",
|
||||
}),
|
||||
],
|
||||
}).then(({ x, y }) => {
|
||||
setCoordinates({ x: x - 300, y: y - 50 });
|
||||
setIsOpen(true);
|
||||
setLinkViewProps({
|
||||
closeLinkView: closeLinkView,
|
||||
view: "LinkPreview",
|
||||
url: href,
|
||||
editor: editor,
|
||||
from: pos,
|
||||
to: pos + node.nodeSize,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
setCleanup(cleanupFunc);
|
||||
},
|
||||
[editor, cleanup]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="frame-renderer flex-grow w-full">
|
||||
<EditorContainer
|
||||
displayConfig={displayConfig}
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
id={id}
|
||||
>
|
||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||
{editor.isEditable && (
|
||||
<div>
|
||||
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
|
||||
<BlockMenu editor={editor} />
|
||||
<AIFeaturesMenu menu={aiHandler?.menu} />
|
||||
</div>
|
||||
)}
|
||||
</EditorContainer>
|
||||
</div>
|
||||
<>
|
||||
<div className="frame-renderer flex-grow w-full" onMouseOver={handleLinkHover}>
|
||||
<EditorContainer
|
||||
displayConfig={displayConfig}
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
id={id}
|
||||
>
|
||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||
{editor.isEditable && (
|
||||
<div>
|
||||
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
|
||||
<BlockMenu editor={editor} />
|
||||
<AIFeaturesMenu menu={aiHandler?.menu} />
|
||||
</div>
|
||||
)}
|
||||
</EditorContainer>
|
||||
</div>
|
||||
{isOpen && linkViewProps && coordinates && (
|
||||
<div
|
||||
style={{ ...floatingStyles, left: `${coordinates.x}px`, top: `${coordinates.y}px` }}
|
||||
className="absolute"
|
||||
ref={refs.setFloating}
|
||||
>
|
||||
<LinkView {...linkViewProps} style={floatingStyles} {...getFloatingProps()} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { FC, ReactNode, useRef } from "react";
|
||||
import { FC, ReactNode } from "react";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// types
|
||||
import { TDisplayConfig } from "@/types";
|
||||
// components
|
||||
import { LinkViewContainer } from "./link-view-container";
|
||||
|
||||
interface EditorContainerProps {
|
||||
children: ReactNode;
|
||||
displayConfig: TDisplayConfig;
|
||||
editor: Editor;
|
||||
editor: Editor | null;
|
||||
editorContainerClassName: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
const { children, displayConfig, editor, editorContainerClassName, id } = props;
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleContainerClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if (event.target !== event.currentTarget) return;
|
||||
@@ -47,23 +44,15 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the last node in the document
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const lastNodePos = editor.state.doc.resolve(Math.max(0, docSize - 2));
|
||||
const lastNode = lastNodePos.node();
|
||||
// Insert a new paragraph at the end of the document
|
||||
const endPosition = editor?.state.doc.content.size;
|
||||
editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run();
|
||||
|
||||
// Check if the last node is a not paragraph
|
||||
if (lastNode && lastNode.type.name !== "paragraph") {
|
||||
// If last node is not a paragraph, insert a new paragraph at the end
|
||||
const endPosition = editor?.state.doc.content.size;
|
||||
editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run();
|
||||
|
||||
// Focus the newly added paragraph for immediate editing
|
||||
editor
|
||||
.chain()
|
||||
.setTextSelection(endPosition + 1)
|
||||
.run();
|
||||
}
|
||||
// Focus the newly added paragraph for immediate editing
|
||||
editor
|
||||
.chain()
|
||||
.setTextSelection(endPosition + 1)
|
||||
.run();
|
||||
} catch (error) {
|
||||
console.error("An error occurred while handling container click to insert new empty node at bottom:", error);
|
||||
}
|
||||
@@ -77,26 +66,22 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={containerRef}
|
||||
id={`editor-container-${id}`}
|
||||
onClick={handleContainerClick}
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
className={cn(
|
||||
`editor-container cursor-text relative line-spacing-${displayConfig.lineSpacing ?? DEFAULT_DISPLAY_CONFIG.lineSpacing}`,
|
||||
{
|
||||
"active-editor": editor?.isFocused && editor?.isEditable,
|
||||
"wide-layout": displayConfig.wideLayout,
|
||||
},
|
||||
displayConfig.fontSize ?? DEFAULT_DISPLAY_CONFIG.fontSize,
|
||||
displayConfig.fontStyle ?? DEFAULT_DISPLAY_CONFIG.fontStyle,
|
||||
editorContainerClassName
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<LinkViewContainer editor={editor} containerRef={containerRef} />
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
id={`editor-container-${id}`}
|
||||
onClick={handleContainerClick}
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
className={cn(
|
||||
`editor-container cursor-text relative line-spacing-${displayConfig.lineSpacing ?? DEFAULT_DISPLAY_CONFIG.lineSpacing}`,
|
||||
{
|
||||
"active-editor": editor?.isFocused && editor?.isEditable,
|
||||
"wide-layout": displayConfig.wideLayout,
|
||||
},
|
||||
displayConfig.fontSize ?? DEFAULT_DISPLAY_CONFIG.fontSize,
|
||||
displayConfig.fontStyle ?? DEFAULT_DISPLAY_CONFIG.fontStyle,
|
||||
editorContainerClassName
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Editor, EditorContent } from "@tiptap/react";
|
||||
import { FC, ReactNode } from "react";
|
||||
import { Editor, EditorContent } from "@tiptap/react";
|
||||
|
||||
interface EditorContentProps {
|
||||
children?: ReactNode;
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import { autoUpdate, flip, hide, shift, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
|
||||
import { Editor, useEditorState } from "@tiptap/react";
|
||||
import { FC, useCallback, useEffect, useState } from "react";
|
||||
// components
|
||||
import { LinkView, LinkViewProps } from "@/components/links";
|
||||
|
||||
interface LinkViewContainerProps {
|
||||
editor: Editor;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containerRef }) => {
|
||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [virtualElement, setVirtualElement] = useState<any>(null);
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
selector: ({ editor }: { editor: Editor }) => ({
|
||||
linkExtensionStorage: editor.storage.link,
|
||||
}),
|
||||
});
|
||||
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
open: isOpen,
|
||||
onOpenChange: setIsOpen,
|
||||
elements: {
|
||||
reference: virtualElement,
|
||||
},
|
||||
middleware: [
|
||||
flip({
|
||||
fallbackPlacements: ["top", "bottom"],
|
||||
}),
|
||||
shift({
|
||||
padding: 5,
|
||||
}),
|
||||
hide(),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
placement: "bottom-start",
|
||||
});
|
||||
|
||||
const dismiss = useDismiss(context);
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]);
|
||||
|
||||
const handleLinkHover = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (!editor || editorState.linkExtensionStorage.isBubbleMenuOpen) return;
|
||||
|
||||
// Find the closest anchor tag from the event target
|
||||
const target = (event.target as HTMLElement)?.closest("a");
|
||||
if (!target) return;
|
||||
|
||||
const referenceProps = getReferenceProps();
|
||||
Object.entries(referenceProps).forEach(([key, value]) => {
|
||||
target.setAttribute(key, value as string);
|
||||
});
|
||||
|
||||
const view = editor.view;
|
||||
if (!view) return;
|
||||
|
||||
try {
|
||||
const pos = view.posAtDOM(target, 0);
|
||||
if (pos === undefined || pos < 0) return;
|
||||
|
||||
const node = view.state.doc.nodeAt(pos);
|
||||
if (!node) return;
|
||||
|
||||
const linkMark = node.marks?.find((mark) => mark.type.name === "link");
|
||||
if (!linkMark) return;
|
||||
|
||||
setVirtualElement(target);
|
||||
|
||||
// Only update if not already open or if hovering over a different link
|
||||
if (!isOpen || (linkViewProps && (linkViewProps.from !== pos || linkViewProps.to !== pos + node.nodeSize))) {
|
||||
setLinkViewProps({
|
||||
view: "LinkPreview", // Always start with preview for new links
|
||||
url: linkMark.attrs.href,
|
||||
text: node.text || "",
|
||||
editor: editor,
|
||||
from: pos,
|
||||
to: pos + node.nodeSize,
|
||||
closeLinkView: () => {
|
||||
setIsOpen(false);
|
||||
editorState.linkExtensionStorage.isPreviewOpen = false;
|
||||
},
|
||||
});
|
||||
setIsOpen(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling link hover:", error);
|
||||
}
|
||||
},
|
||||
[editor, editorState.linkExtensionStorage, getReferenceProps, isOpen, linkViewProps]
|
||||
);
|
||||
|
||||
// Set up event listeners
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
container.addEventListener("mouseover", handleLinkHover);
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("mouseover", handleLinkHover);
|
||||
};
|
||||
}, [handleLinkHover]);
|
||||
|
||||
// Close link view when bubble menu opens
|
||||
useEffect(() => {
|
||||
if (editorState.linkExtensionStorage.isBubbleMenuOpen && isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [editorState.linkExtensionStorage, isOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && linkViewProps && virtualElement && (
|
||||
<div ref={refs.setFloating} style={{ ...floatingStyles, zIndex: 100 }} {...getFloatingProps()}>
|
||||
<LinkView {...linkViewProps} style={floatingStyles} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./link-edit-view";
|
||||
export * from "./link-input-view";
|
||||
export * from "./link-preview";
|
||||
export * from "./link-view";
|
||||
|
||||
@@ -1,145 +1,131 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { Link2Off } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
// components
|
||||
import { LinkViewProps, LinkViews } from "@/components/links";
|
||||
import { LinkViewProps } from "@/components/links";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
|
||||
interface InputViewProps {
|
||||
const InputView = ({
|
||||
label,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
defaultValue: string;
|
||||
placeholder: string;
|
||||
onChange: (value: string) => void;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
const InputView = ({ label, value, placeholder, onChange, autoFocus }: InputViewProps) => (
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="inline-block font-semibold text-xs text-custom-text-400">{label}</label>
|
||||
<input
|
||||
placeholder={placeholder}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-[280px] outline-none bg-custom-background-90 text-custom-text-900 text-sm border border-custom-border-300 rounded-md p-2"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
autoFocus={autoFocus}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="w-[280px] outline-none bg-custom-background-90 text-custom-text-900 text-sm"
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface LinkEditViewProps {
|
||||
export const LinkEditView = ({
|
||||
viewProps,
|
||||
}: {
|
||||
viewProps: LinkViewProps;
|
||||
switchView: (view: LinkViews) => void;
|
||||
}
|
||||
switchView: (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => void;
|
||||
}) => {
|
||||
const { editor, from, to } = viewProps;
|
||||
|
||||
export const LinkEditView = ({ viewProps }: LinkEditViewProps) => {
|
||||
const { editor, from, to, url: initialUrl, text: initialText, closeLinkView } = viewProps;
|
||||
const [positionRef, setPositionRef] = useState({ from: from, to: to });
|
||||
const [localUrl, setLocalUrl] = useState(viewProps.url);
|
||||
|
||||
// State
|
||||
const [positionRef] = useState({ from, to });
|
||||
const [localUrl, setLocalUrl] = useState(initialUrl);
|
||||
const [localText, setLocalText] = useState(initialText ?? "");
|
||||
const [linkRemoved, setLinkRemoved] = useState(false);
|
||||
const hasSubmitted = useRef(false);
|
||||
const linkRemoved = useRef<boolean>();
|
||||
|
||||
const getText = (from: number, to: number) => {
|
||||
if (to >= editor.state.doc.content.size) return "";
|
||||
|
||||
const text = editor.state.doc.textBetween(from, to, "\n");
|
||||
return text;
|
||||
};
|
||||
|
||||
const handleUpdateLink = (url: string) => {
|
||||
setLocalUrl(url);
|
||||
};
|
||||
|
||||
// Effects
|
||||
useEffect(
|
||||
() =>
|
||||
// Cleanup effect: Remove link if not submitted and url is empty
|
||||
() => {
|
||||
if (!hasSubmitted.current && !linkRemoved && initialUrl === "") {
|
||||
try {
|
||||
removeLink();
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
[linkRemoved, initialUrl]
|
||||
() => () => {
|
||||
if (linkRemoved.current) return;
|
||||
|
||||
const url = isValidHttpUrl(localUrl) ? localUrl : viewProps.url;
|
||||
|
||||
if (to >= editor.state.doc.content.size) return;
|
||||
|
||||
editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link));
|
||||
editor.view.dispatch(editor.state.tr.addMark(from, to, editor.schema.marks.link.create({ href: url })));
|
||||
},
|
||||
[localUrl, editor, from, to, viewProps.url]
|
||||
);
|
||||
|
||||
// Sync state with props
|
||||
useEffect(() => {
|
||||
setLocalUrl(initialUrl);
|
||||
}, [initialUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialText) setLocalText(initialText);
|
||||
}, [initialText]);
|
||||
|
||||
// Handlers
|
||||
const handleTextChange = useCallback((value: string) => {
|
||||
if (value.trim() !== "") setLocalText(value);
|
||||
}, []);
|
||||
|
||||
const applyChanges = useCallback((): boolean => {
|
||||
if (linkRemoved) return false;
|
||||
hasSubmitted.current = true;
|
||||
|
||||
const { url, isValid } = isValidHttpUrl(localUrl);
|
||||
if (to >= editor.state.doc.content.size || !isValid) return false;
|
||||
|
||||
// Apply URL change
|
||||
const tr = editor.state.tr;
|
||||
tr.removeMark(from, to, editor.schema.marks.link).addMark(from, to, editor.schema.marks.link.create({ href: url }));
|
||||
editor.view.dispatch(tr);
|
||||
|
||||
// Apply text change if different
|
||||
if (localText !== initialText) {
|
||||
const node = editor.view.state.doc.nodeAt(from) as Node;
|
||||
if (!node || !node.marks) return false;
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.setTextSelection(from)
|
||||
.deleteRange({ from: positionRef.from, to: positionRef.to })
|
||||
.insertContent(localText)
|
||||
.setTextSelection({ from, to: from + localText.length })
|
||||
.run();
|
||||
//
|
||||
// Restore marks
|
||||
node.marks.forEach((mark) => {
|
||||
editor.chain().setMark(mark.type.name, mark.attrs).run();
|
||||
});
|
||||
const handleUpdateText = (text: string) => {
|
||||
if (text === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [editor, from, to, initialText, localText, localUrl]);
|
||||
const node = editor.view.state.doc.nodeAt(from) as Node;
|
||||
if (!node) return;
|
||||
const marks = node.marks;
|
||||
if (!marks) return;
|
||||
|
||||
const removeLink = useCallback(() => {
|
||||
editor.chain().setTextSelection(from).run();
|
||||
|
||||
editor.chain().deleteRange({ from: positionRef.from, to: positionRef.to }).run();
|
||||
editor.chain().insertContent(text).run();
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.setTextSelection({
|
||||
from: from,
|
||||
to: from + text.length,
|
||||
})
|
||||
.run();
|
||||
|
||||
setPositionRef({ from: from, to: from + text.length });
|
||||
|
||||
marks.forEach((mark) => {
|
||||
editor.chain().setMark(mark.type.name, mark.attrs).run();
|
||||
});
|
||||
};
|
||||
|
||||
const removeLink = () => {
|
||||
editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link));
|
||||
setLinkRemoved(true);
|
||||
closeLinkView();
|
||||
}, [editor, from, to, closeLinkView]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.stopPropagation();
|
||||
if (applyChanges()) {
|
||||
closeLinkView();
|
||||
setLocalUrl("");
|
||||
setLocalText("");
|
||||
}
|
||||
}
|
||||
},
|
||||
[applyChanges, closeLinkView]
|
||||
);
|
||||
linkRemoved.current = true;
|
||||
viewProps.closeLinkView();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onKeyDown={handleKeyDown}
|
||||
className="shadow-md rounded p-2 flex flex-col gap-3 bg-custom-background-90 border-custom-border-100 border-2 animate-in fade-in translate-y-1"
|
||||
style={{
|
||||
transition: "all 0.1s cubic-bezier(.55, .085, .68, .53)",
|
||||
}}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === "Enter" && viewProps.closeLinkView()}
|
||||
className="shadow-md rounded p-2 flex flex-col gap-3 bg-custom-background-90 border-custom-border-100 border-2"
|
||||
>
|
||||
<InputView label="URL" placeholder="Enter or paste URL" value={localUrl} onChange={setLocalUrl} autoFocus />
|
||||
<InputView label="Text" placeholder="Enter Text to display" value={localText} onChange={handleTextChange} />
|
||||
<InputView
|
||||
label={"URL"}
|
||||
placeholder={"Enter or paste URL"}
|
||||
defaultValue={localUrl}
|
||||
onChange={(e) => handleUpdateLink(e.target.value)}
|
||||
/>
|
||||
<InputView
|
||||
label={"Text"}
|
||||
placeholder={"Enter Text to display"}
|
||||
defaultValue={getText(from, to)}
|
||||
onChange={(e) => handleUpdateText(e.target.value)}
|
||||
/>
|
||||
<div className="mb-1 bg-custom-border-300 h-[1px] w-full gap-2" />
|
||||
<div className="flex text-sm text-custom-text-800 gap-2 items-center">
|
||||
<Link2Off size={14} className="inline-block" />
|
||||
<button onClick={removeLink} className="cursor-pointer hover:text-custom-text-400 transition-colors">
|
||||
<button onClick={() => removeLink()} className="cursor-pointer">
|
||||
Remove Link
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// components
|
||||
import { LinkViewProps } from "@/components/links";
|
||||
|
||||
export const LinkInputView = ({}: {
|
||||
viewProps: LinkViewProps;
|
||||
switchView: (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => void;
|
||||
}) => <p>LinkInputView</p>;
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Copy, GlobeIcon, Link2Off, PencilIcon } from "lucide-react";
|
||||
// components
|
||||
import { LinkViewProps, LinkViews } from "@/components/links";
|
||||
import { LinkViewProps } from "@/components/links";
|
||||
|
||||
export const LinkPreview = ({
|
||||
viewProps,
|
||||
switchView,
|
||||
}: {
|
||||
viewProps: LinkViewProps;
|
||||
switchView: (view: LinkViews) => void;
|
||||
switchView: (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => void;
|
||||
}) => {
|
||||
const { editor, from, to, url } = viewProps;
|
||||
|
||||
@@ -22,32 +22,20 @@ export const LinkPreview = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute left-0 top-0 max-w-max animate-in fade-in translate-y-1"
|
||||
style={{
|
||||
transition: "all 0.2s cubic-bezier(.55, .085, .68, .53)",
|
||||
}}
|
||||
>
|
||||
<div className="absolute left-0 top-0 max-w-max">
|
||||
<div className="shadow-md items-center rounded p-2 flex gap-3 bg-custom-background-90 border-custom-border-100 border-2 text-custom-text-300 text-xs">
|
||||
<GlobeIcon size={14} className="inline-block" />
|
||||
<p>{url?.length > 40 ? url.slice(0, 40) + "..." : url}</p>
|
||||
<p>{url.length > 40 ? url.slice(0, 40) + "..." : url}</p>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={copyLinkToClipboard} className="cursor-pointer hover:text-custom-text-100 transition-colors">
|
||||
<button onClick={copyLinkToClipboard} className="cursor-pointer">
|
||||
<Copy size={14} className="inline-block" />
|
||||
</button>
|
||||
{editor.isEditable && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => switchView("LinkEditView")}
|
||||
className="cursor-pointer hover:text-custom-text-100 transition-colors"
|
||||
>
|
||||
<PencilIcon size={14} className="inline-block" />
|
||||
</button>
|
||||
<button onClick={removeLink} className="cursor-pointer hover:text-custom-text-100 transition-colors">
|
||||
<Link2Off size={14} className="inline-block" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button onClick={() => switchView("LinkEditView")} className="cursor-pointer">
|
||||
<PencilIcon size={14} className="inline-block" />
|
||||
</button>
|
||||
<button onClick={removeLink} className="cursor-pointer">
|
||||
<Link2Off size={14} className="inline-block" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
import { Editor } from "@tiptap/react";
|
||||
// components
|
||||
import { LinkEditView, LinkPreview } from "@/components/links";
|
||||
|
||||
export type LinkViews = "LinkPreview" | "LinkEditView";
|
||||
import { LinkEditView, LinkInputView, LinkPreview } from "@/components/links";
|
||||
|
||||
export interface LinkViewProps {
|
||||
view?: LinkViews;
|
||||
view?: "LinkPreview" | "LinkEditView" | "LinkInputView";
|
||||
editor: Editor;
|
||||
from: number;
|
||||
to: number;
|
||||
url: string;
|
||||
text?: string;
|
||||
closeLinkView: () => void;
|
||||
}
|
||||
|
||||
export const LinkView = (props: LinkViewProps & { style: CSSProperties }) => {
|
||||
const [currentView, setCurrentView] = useState<LinkViews>(props.view ?? "LinkPreview");
|
||||
const [currentView, setCurrentView] = useState(props.view ?? "LinkInputView");
|
||||
const [prevFrom, setPrevFrom] = useState(props.from);
|
||||
|
||||
const switchView = (view: LinkViews) => {
|
||||
const switchView = (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => {
|
||||
setCurrentView(view);
|
||||
};
|
||||
|
||||
@@ -30,10 +27,16 @@ export const LinkView = (props: LinkViewProps & { style: CSSProperties }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentView === "LinkPreview" && <LinkPreview viewProps={props} switchView={switchView} />}
|
||||
{currentView === "LinkEditView" && <LinkEditView viewProps={props} switchView={switchView} />}
|
||||
</>
|
||||
);
|
||||
const renderView = () => {
|
||||
switch (currentView) {
|
||||
case "LinkPreview":
|
||||
return <LinkPreview viewProps={props} switchView={switchView} />;
|
||||
case "LinkEditView":
|
||||
return <LinkEditView viewProps={props} switchView={switchView} />;
|
||||
case "LinkInputView":
|
||||
return <LinkInputView viewProps={props} switchView={switchView} />;
|
||||
}
|
||||
};
|
||||
|
||||
return renderView();
|
||||
};
|
||||
|
||||
@@ -23,11 +23,11 @@ export const BubbleMenuLinkSelector: FC<Props> = (props) => {
|
||||
const handleLinkSubmit = useCallback(() => {
|
||||
const input = inputRef.current;
|
||||
if (!input) return;
|
||||
const url = input.value;
|
||||
let url = input.value;
|
||||
if (!url) return;
|
||||
const { isValid, url: validatedUrl } = isValidHttpUrl(url);
|
||||
if (isValid) {
|
||||
setLinkEditor(editor, validatedUrl);
|
||||
if (!url.startsWith("http")) url = `http://${url}`;
|
||||
if (isValidHttpUrl(url)) {
|
||||
setLinkEditor(editor, url);
|
||||
setIsOpen(false);
|
||||
setError(false);
|
||||
} else {
|
||||
|
||||
@@ -91,7 +91,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Edi
|
||||
empty ||
|
||||
!editor.isEditable ||
|
||||
editor.isActive("image") ||
|
||||
editor.isActive("imageComponent") ||
|
||||
isNodeSelection(selection) ||
|
||||
isCellSelection(selection) ||
|
||||
isSelecting
|
||||
@@ -103,12 +102,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Edi
|
||||
tippyOptions: {
|
||||
moveTransition: "transform 0.15s ease-out",
|
||||
duration: [300, 0],
|
||||
zIndex: 9,
|
||||
onShow: () => {
|
||||
props.editor.storage.link.isBubbleMenuOpen = true;
|
||||
},
|
||||
onHidden: () => {
|
||||
props.editor.storage.link.isBubbleMenuOpen = false;
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
|
||||
@@ -142,8 +142,8 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({
|
||||
icon: UnderlineIcon,
|
||||
});
|
||||
|
||||
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({
|
||||
key: "strikethrough",
|
||||
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strike"> => ({
|
||||
key: "strike",
|
||||
name: "Strikethrough",
|
||||
isActive: () => editor?.isActive("strike"),
|
||||
command: () => toggleStrike(editor),
|
||||
|
||||
@@ -87,7 +87,7 @@ export const TEXT_ALIGNMENT_ITEMS: ToolbarMenuItem<"text-align">[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strikethrough">[] = [
|
||||
const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strike">[] = [
|
||||
{
|
||||
itemKey: "bold",
|
||||
renderKey: "bold",
|
||||
@@ -113,7 +113,7 @@ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strik
|
||||
editors: ["lite", "document"],
|
||||
},
|
||||
{
|
||||
itemKey: "strikethrough",
|
||||
itemKey: "strike",
|
||||
renderKey: "strikethrough",
|
||||
name: "Strikethrough",
|
||||
icon: Strikethrough,
|
||||
|
||||
@@ -66,7 +66,7 @@ export const CoreEditorExtensionsWithoutProps = [
|
||||
autolink: true,
|
||||
linkOnPaste: true,
|
||||
protocols: ["http", "https"],
|
||||
validate: (url: string) => isValidHttpUrl(url).isValid,
|
||||
validate: (url: string) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user