Something went wrong.
diff --git a/apps/admin/package.json b/apps/admin/package.json
index 2d314b06123..8a492643a5e 100644
--- a/apps/admin/package.json
+++ b/apps/admin/package.json
@@ -1,6 +1,6 @@
{
"name": "admin",
- "version": "1.2.0",
+ "version": "1.3.0",
"private": true,
"description": "Admin UI for Plane",
"license": "AGPL-3.0",
@@ -49,7 +49,6 @@
"uuid": "catalog:"
},
"devDependencies": {
- "@dotenvx/dotenvx": "catalog:",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@react-router/dev": "catalog:",
@@ -57,6 +56,7 @@
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
+ "dotenv": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts
index c9d97157f41..f61d9b49eb5 100644
--- a/apps/admin/vite.config.ts
+++ b/apps/admin/vite.config.ts
@@ -1,5 +1,5 @@
import path from "node:path";
-import * as dotenv from "@dotenvx/dotenvx";
+import * as dotenv from "dotenv";
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
diff --git a/apps/api/package.json b/apps/api/package.json
index 6e62f2bd114..99f5de98794 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -1,7 +1,7 @@
{
"name": "plane-api",
- "version": "1.2.0",
- "license": "AGPL-3.0",
+ "version": "1.3.0",
"private": true,
- "description": "API server powering Plane's backend"
+ "description": "API server powering Plane's backend",
+ "license": "AGPL-3.0"
}
diff --git a/apps/api/plane/api/serializers/__init__.py b/apps/api/plane/api/serializers/__init__.py
index 44e527a2dc5..2ab639d5466 100644
--- a/apps/api/plane/api/serializers/__init__.py
+++ b/apps/api/plane/api/serializers/__init__.py
@@ -25,6 +25,10 @@
IssueCommentCreateSerializer,
IssueLinkCreateSerializer,
IssueLinkUpdateSerializer,
+ IssueRelationCreateSerializer,
+ IssueRelationResponseSerializer,
+ IssueRelationSerializer,
+ RelatedIssueSerializer,
)
from .state import StateLiteSerializer, StateSerializer
from .cycle import (
@@ -49,7 +53,7 @@
IntakeIssueCreateSerializer,
IntakeIssueUpdateSerializer,
)
-from .estimate import EstimatePointSerializer
+from .estimate import EstimateSerializer, EstimatePointSerializer
from .asset import (
UserAssetUploadSerializer,
AssetUpdateSerializer,
diff --git a/apps/api/plane/api/serializers/estimate.py b/apps/api/plane/api/serializers/estimate.py
index e2a7c5e1d32..978003240b0 100644
--- a/apps/api/plane/api/serializers/estimate.py
+++ b/apps/api/plane/api/serializers/estimate.py
@@ -2,20 +2,36 @@
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
+# Third party imports
+from rest_framework import serializers
+
# Module imports
-from plane.db.models import EstimatePoint
+from plane.db.models import Estimate, EstimatePoint
from .base import BaseSerializer
-class EstimatePointSerializer(BaseSerializer):
- """
- Serializer for project estimation points and story point values.
+class EstimateSerializer(BaseSerializer):
+ class Meta:
+ model = Estimate
+ fields = "__all__"
+ read_only_fields = ["workspace", "project", "deleted_at"]
- Handles numeric estimation data for work item sizing and sprint planning,
- providing standardized point values for project velocity calculations.
- """
+ def create(self, validated_data):
+ validated_data["workspace"] = self.context["workspace"]
+ validated_data["project"] = self.context["project"]
+ return super().create(validated_data)
+
+
+class EstimatePointSerializer(BaseSerializer):
+ def validate(self, data):
+ if not data:
+ raise serializers.ValidationError("Estimate points are required")
+ value = data.get("value")
+ if value and len(value) > 20:
+ raise serializers.ValidationError("Value can't be more than 20 characters")
+ return data
class Meta:
model = EstimatePoint
- fields = ["id", "value"]
- read_only_fields = fields
+ fields = "__all__"
+ read_only_fields = ["estimate", "workspace", "project"]
diff --git a/apps/api/plane/api/serializers/issue.py b/apps/api/plane/api/serializers/issue.py
index ba9ddae301d..6468ddbc84f 100644
--- a/apps/api/plane/api/serializers/issue.py
+++ b/apps/api/plane/api/serializers/issue.py
@@ -20,6 +20,7 @@
IssueComment,
IssueLabel,
IssueLink,
+ IssueRelation,
Label,
ProjectMember,
State,
@@ -479,6 +480,184 @@ class Meta:
]
+class IssueRelationResponseSerializer(serializers.Serializer):
+ """
+ Serializer for issue relations response showing grouped relation types.
+
+ Returns issue IDs organized by relation type for efficient client-side processing.
+ """
+
+ blocking = serializers.ListField(
+ child=serializers.UUIDField(),
+ help_text="List of issue IDs that are blocking this issue",
+ )
+ blocked_by = serializers.ListField(
+ child=serializers.UUIDField(),
+ help_text="List of issue IDs that this issue is blocked by",
+ )
+ duplicate = serializers.ListField(
+ child=serializers.UUIDField(),
+ help_text="List of issue IDs that are duplicates of this issue",
+ )
+ relates_to = serializers.ListField(
+ child=serializers.UUIDField(),
+ help_text="List of issue IDs that relate to this issue",
+ )
+ start_after = serializers.ListField(
+ child=serializers.UUIDField(),
+ help_text="List of issue IDs that start after this issue",
+ )
+ start_before = serializers.ListField(
+ child=serializers.UUIDField(),
+ help_text="List of issue IDs that start before this issue",
+ )
+ finish_after = serializers.ListField(
+ child=serializers.UUIDField(),
+ help_text="List of issue IDs that finish after this issue",
+ )
+ finish_before = serializers.ListField(
+ child=serializers.UUIDField(),
+ help_text="List of issue IDs that finish before this issue",
+ )
+
+
+class IssueRelationCreateSerializer(serializers.Serializer):
+ """
+ Serializer for creating issue relations.
+
+ Creates issue relations with the specified relation type and issues.
+ Validates relation types and ensures proper issue ID format.
+ """
+
+ RELATION_TYPE_CHOICES = [
+ ("blocking", "Blocking"),
+ ("blocked_by", "Blocked By"),
+ ("duplicate", "Duplicate"),
+ ("relates_to", "Relates To"),
+ ("start_before", "Start Before"),
+ ("start_after", "Start After"),
+ ("finish_before", "Finish Before"),
+ ("finish_after", "Finish After"),
+ ]
+
+ relation_type = serializers.ChoiceField(
+ choices=RELATION_TYPE_CHOICES,
+ required=True,
+ help_text="Type of relationship between work items",
+ )
+ issues = serializers.ListField(
+ child=serializers.UUIDField(),
+ required=True,
+ min_length=1,
+ help_text="Array of work item IDs to create relations with",
+ )
+
+ def validate_issues(self, value):
+ """Validate that issues list is not empty and contains valid UUIDs."""
+ if not value:
+ raise serializers.ValidationError("At least one issue ID is required.")
+ return value
+
+
+class IssueRelationRemoveSerializer(serializers.Serializer):
+ """
+ Serializer for removing issue relations.
+
+ Removes existing relationships between work items by specifying
+ the related issue ID.
+ """
+
+ related_issue = serializers.UUIDField(
+ required=True, help_text="ID of the related work item to remove relation with"
+ )
+
+
+class IssueRelationSerializer(BaseSerializer):
+ """
+ Serializer for issue relationships showing related issue details.
+
+ Provides comprehensive information about related issues including
+ project context, sequence ID, and relationship type.
+ """
+
+ id = serializers.UUIDField(source="related_issue.id", read_only=True)
+ project_id = serializers.UUIDField(source="related_issue.project_id", read_only=True)
+ sequence_id = serializers.IntegerField(source="related_issue.sequence_id", read_only=True)
+ name = serializers.CharField(source="related_issue.name", read_only=True)
+ relation_type = serializers.CharField(read_only=True)
+ state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
+ priority = serializers.CharField(source="related_issue.priority", read_only=True)
+
+ class Meta:
+ model = IssueRelation
+ fields = [
+ "id",
+ "project_id",
+ "sequence_id",
+ "relation_type",
+ "name",
+ "state_id",
+ "priority",
+ "created_by",
+ "created_at",
+ "updated_at",
+ "updated_by",
+ ]
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "created_at",
+ "updated_by",
+ "updated_at",
+ ]
+
+
+class RelatedIssueSerializer(BaseSerializer):
+ """
+ Serializer for reverse issue relationships showing issue details.
+
+ Provides comprehensive information about the source issue in a relationship
+ including project context, sequence ID, and relationship type.
+ """
+
+ id = serializers.UUIDField(source="issue.id", read_only=True)
+ project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True)
+ sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
+ name = serializers.CharField(source="issue.name", read_only=True)
+ type_id = serializers.UUIDField(source="issue.type.id", read_only=True)
+ relation_type = serializers.CharField(read_only=True)
+ is_epic = serializers.BooleanField(source="issue.type.is_epic", read_only=True)
+ state_id = serializers.UUIDField(source="issue.state.id", read_only=True)
+ priority = serializers.CharField(source="issue.priority", read_only=True)
+
+ class Meta:
+ model = IssueRelation
+ fields = [
+ "id",
+ "project_id",
+ "sequence_id",
+ "relation_type",
+ "name",
+ "type_id",
+ "is_epic",
+ "state_id",
+ "priority",
+ "created_by",
+ "created_at",
+ "updated_by",
+ "updated_at",
+ ]
+ read_only_fields = [
+ "workspace",
+ "project",
+ "created_by",
+ "created_at",
+ "updated_by",
+ "updated_at",
+ ]
+
+
class IssueAttachmentSerializer(BaseSerializer):
"""
Serializer for work item file attachments.
diff --git a/apps/api/plane/api/urls/estimate.py b/apps/api/plane/api/urls/estimate.py
new file mode 100644
index 00000000000..3fe5d3b7b43
--- /dev/null
+++ b/apps/api/plane/api/urls/estimate.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2023-present Plane Software, Inc. and contributors
+# SPDX-License-Identifier: AGPL-3.0-only
+# See the LICENSE file for details.
+
+from django.urls import path
+
+from plane.api.views.estimate import (
+ ProjectEstimateAPIEndpoint,
+ EstimatePointListCreateAPIEndpoint,
+ EstimatePointDetailAPIEndpoint,
+)
+
+urlpatterns = [
+ path(
+ "workspaces/
/projects//estimates/",
+ ProjectEstimateAPIEndpoint.as_view(http_method_names=["get", "post", "patch", "delete"]),
+ name="project-estimate",
+ ),
+ path(
+ "workspaces//projects//estimates//estimate-points/",
+ EstimatePointListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
+ name="estimate-point-list-create",
+ ),
+ path(
+ "workspaces//projects//estimates//estimate-points//",
+ EstimatePointDetailAPIEndpoint.as_view(http_method_names=["patch", "delete"]),
+ name="estimate-point-detail",
+ ),
+]
diff --git a/apps/api/plane/api/urls/work_item.py b/apps/api/plane/api/urls/work_item.py
index 48b1948dbe5..1a1704f2773 100644
--- a/apps/api/plane/api/urls/work_item.py
+++ b/apps/api/plane/api/urls/work_item.py
@@ -17,6 +17,7 @@
IssueAttachmentDetailAPIEndpoint,
WorkspaceIssueAPIEndpoint,
IssueSearchEndpoint,
+ IssueRelationListCreateAPIEndpoint,
)
# Deprecated url patterns
@@ -145,6 +146,11 @@
IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
name="work-item-attachment-detail",
),
+ path(
+ "workspaces//projects//work-items//relations/",
+ IssueRelationListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
+ name="work-item-relation-list",
+ ),
]
urlpatterns = old_url_patterns + new_url_patterns
diff --git a/apps/api/plane/api/views/__init__.py b/apps/api/plane/api/views/__init__.py
index 644d87edc86..e8549afb437 100644
--- a/apps/api/plane/api/views/__init__.py
+++ b/apps/api/plane/api/views/__init__.py
@@ -29,6 +29,7 @@
IssueAttachmentListCreateAPIEndpoint,
IssueAttachmentDetailAPIEndpoint,
IssueSearchEndpoint,
+ IssueRelationListCreateAPIEndpoint,
)
from .cycle import (
diff --git a/apps/api/plane/api/views/estimate.py b/apps/api/plane/api/views/estimate.py
new file mode 100644
index 00000000000..915cc8e5cc3
--- /dev/null
+++ b/apps/api/plane/api/views/estimate.py
@@ -0,0 +1,291 @@
+# Copyright (c) 2023-present Plane Software, Inc. and contributors
+# SPDX-License-Identifier: AGPL-3.0-only
+# See the LICENSE file for details.
+
+# Third party imports
+from rest_framework.response import Response
+from rest_framework import status
+from drf_spectacular.utils import OpenApiRequest, OpenApiResponse
+
+# Module imports
+from plane.app.permissions.project import ProjectEntityPermission
+from plane.api.views.base import BaseAPIView
+from plane.db.models import Estimate, EstimatePoint, Project, Workspace
+from plane.api.serializers import EstimateSerializer, EstimatePointSerializer
+from plane.utils.openapi.decorators import estimate_docs, estimate_point_docs
+from plane.utils.openapi import (
+ ESTIMATE_CREATE_EXAMPLE,
+ ESTIMATE_UPDATE_EXAMPLE,
+ ESTIMATE_POINT_CREATE_EXAMPLE,
+ ESTIMATE_POINT_UPDATE_EXAMPLE,
+ ESTIMATE_EXAMPLE,
+ ESTIMATE_POINT_EXAMPLE,
+ DELETED_RESPONSE,
+ WORKSPACE_SLUG_PARAMETER,
+ PROJECT_ID_PARAMETER,
+ ESTIMATE_ID_PARAMETER,
+)
+
+
+class ProjectEstimateAPIEndpoint(BaseAPIView):
+ permission_classes = [ProjectEntityPermission]
+ model = Estimate
+ serializer_class = EstimateSerializer
+
+ def get_queryset(self):
+ return self.model.objects.filter(workspace__slug=self.workspace_slug, project_id=self.project_id)
+
+ @estimate_docs(
+ operation_id="create_estimate",
+ summary="Create an estimate",
+ description="Create an estimate for a project",
+ request=OpenApiRequest(
+ request=EstimateSerializer,
+ examples=[ESTIMATE_CREATE_EXAMPLE],
+ ),
+ )
+ def post(self, request, slug, project_id):
+ project = Project.objects.filter(id=project_id, workspace__slug=slug).first()
+ if not project:
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Project not found"})
+
+ workspace = Workspace.objects.filter(slug=slug).first()
+ if not workspace:
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Workspace not found"})
+
+ project_estimate = self.get_queryset().first()
+ if project_estimate:
+ # return 409 if the project estimate already exists
+ return Response(
+ status=status.HTTP_409_CONFLICT,
+ data={"error": "An estimate already exists for this project", "id": str(project_estimate.id)},
+ )
+ # create the project estimate
+ serializer = self.serializer_class(data=request.data, context={"workspace": workspace, "project": project})
+ if not serializer.is_valid():
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+
+ @estimate_docs(
+ operation_id="get_estimate",
+ summary="Get an estimate",
+ description="Get an estimate for a project",
+ responses={
+ 200: OpenApiResponse(
+ description="Estimate",
+ response=EstimateSerializer,
+ examples=[ESTIMATE_EXAMPLE],
+ ),
+ },
+ )
+ def get(self, request, slug, project_id):
+ estimate = self.get_queryset().first()
+ if not estimate:
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
+ serializer = self.serializer_class(estimate)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ @estimate_docs(
+ operation_id="update_estimate",
+ summary="Update an estimate",
+ description="Update an estimate for a project",
+ request=OpenApiRequest(
+ request=EstimateSerializer,
+ examples=[ESTIMATE_UPDATE_EXAMPLE],
+ ),
+ responses={
+ 200: OpenApiResponse(
+ description="Estimate",
+ response=EstimateSerializer,
+ examples=[ESTIMATE_EXAMPLE],
+ ),
+ },
+ )
+ def patch(self, request, slug, project_id):
+ ALLOWED_FIELDS = ["name", "description"]
+ estimate = self.get_queryset().first()
+ if not estimate:
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
+ filtered_data = {k: v for k, v in request.data.items() if k in ALLOWED_FIELDS}
+ if not filtered_data:
+ serializer = self.serializer_class(estimate)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ serializer = self.serializer_class(estimate, data=filtered_data, partial=True)
+ if not serializer.is_valid():
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ @estimate_docs(
+ operation_id="delete_estimate",
+ summary="Delete an estimate",
+ description="Delete an estimate for a project",
+ responses={
+ 204: DELETED_RESPONSE,
+ },
+ )
+ def delete(self, request, slug, project_id):
+ estimate = self.get_queryset().first()
+ if not estimate:
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
+ estimate.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class EstimatePointListCreateAPIEndpoint(BaseAPIView):
+ """List and bulk create estimate points for an estimate."""
+
+ permission_classes = [ProjectEntityPermission]
+ model = EstimatePoint
+ serializer_class = EstimatePointSerializer
+
+ def get_queryset(self):
+ return self.model.objects.filter(
+ estimate_id=self.kwargs["estimate_id"],
+ workspace__slug=self.kwargs["slug"],
+ project_id=self.kwargs["project_id"],
+ ).select_related("estimate", "workspace", "project")
+
+ @estimate_point_docs(
+ operation_id="get_estimate_points",
+ summary="Get estimate points",
+ description="Get estimate points for an estimate",
+ parameters=[
+ WORKSPACE_SLUG_PARAMETER,
+ PROJECT_ID_PARAMETER,
+ ESTIMATE_ID_PARAMETER,
+ ],
+ responses={
+ 200: OpenApiResponse(
+ description="Estimate points",
+ response=EstimatePointSerializer(many=True),
+ examples=[ESTIMATE_POINT_EXAMPLE],
+ ),
+ },
+ )
+ def get(self, request, slug, project_id, estimate_id):
+ estimate = Estimate.objects.filter(
+ id=estimate_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ ).first()
+ if not estimate:
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
+ estimate_points = self.get_queryset()
+ serializer = self.serializer_class(estimate_points, many=True)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ @estimate_point_docs(
+ operation_id="create_estimate_points",
+ summary="Create estimate points",
+ description="Create estimate points for an estimate",
+ request=OpenApiRequest(
+ request=EstimatePointSerializer,
+ examples=[ESTIMATE_POINT_CREATE_EXAMPLE],
+ ),
+ responses={
+ 201: OpenApiResponse(
+ description="Estimate points",
+ response=EstimatePointSerializer(many=True),
+ examples=[ESTIMATE_POINT_EXAMPLE],
+ ),
+ },
+ )
+ def post(self, request, slug, project_id, estimate_id):
+ estimate = Estimate.objects.filter(
+ id=estimate_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ ).first()
+ if not estimate:
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
+
+ estimate_points_data = (
+ request.data if isinstance(request.data, list) else request.data.get("estimate_points", [])
+ )
+ if not estimate_points_data:
+ return Response(
+ status=status.HTTP_400_BAD_REQUEST,
+ data={"error": "Estimate points are required"},
+ )
+
+ serializer = self.serializer_class(data=estimate_points_data, many=True)
+ if not serializer.is_valid():
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ estimate_points = [
+ EstimatePoint(
+ estimate=estimate,
+ workspace=estimate.workspace,
+ project=estimate.project,
+ **item,
+ )
+ for item in serializer.validated_data
+ ]
+ created = EstimatePoint.objects.bulk_create(estimate_points)
+ return Response(
+ self.serializer_class(created, many=True).data,
+ status=status.HTTP_201_CREATED,
+ )
+
+
+class EstimatePointDetailAPIEndpoint(BaseAPIView):
+ """Update and delete a single estimate point."""
+
+ permission_classes = [ProjectEntityPermission]
+ model = EstimatePoint
+ serializer_class = EstimatePointSerializer
+
+ def get_queryset(self):
+ return self.model.objects.filter(
+ estimate_id=self.kwargs["estimate_id"],
+ workspace__slug=self.kwargs["slug"],
+ project_id=self.kwargs["project_id"],
+ )
+
+ @estimate_point_docs(
+ operation_id="update_estimate_point",
+ summary="Update an estimate point",
+ description="Update an estimate point for an estimate",
+ request=OpenApiRequest(
+ request=EstimatePointSerializer,
+ examples=[ESTIMATE_POINT_UPDATE_EXAMPLE],
+ ),
+ responses={
+ 200: OpenApiResponse(
+ description="Estimate point",
+ response=EstimatePointSerializer,
+ examples=[ESTIMATE_POINT_EXAMPLE],
+ ),
+ },
+ )
+ def patch(self, request, slug, project_id, estimate_id, estimate_point_id):
+ estimate_point = self.get_queryset().filter(id=estimate_point_id).first()
+ if not estimate_point:
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate point not found"})
+ ALLOWED_FIELDS = ["key", "value", "description"]
+ filtered_data = {k: v for k, v in request.data.items() if k in ALLOWED_FIELDS}
+ if not filtered_data:
+ return Response(self.serializer_class(estimate_point).data, status=status.HTTP_200_OK)
+ serializer = self.serializer_class(estimate_point, data=filtered_data, partial=True)
+ if not serializer.is_valid():
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ @estimate_point_docs(
+ operation_id="delete_estimate_point",
+ summary="Delete an estimate point",
+ description="Delete an estimate point for an estimate",
+ responses={
+ 204: DELETED_RESPONSE,
+ },
+ )
+ def delete(self, request, slug, project_id, estimate_id, estimate_point_id):
+ estimate_point = self.get_queryset().filter(id=estimate_point_id).first()
+ if not estimate_point:
+ return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate point not found"})
+ estimate_point.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apps/api/plane/api/views/issue.py b/apps/api/plane/api/views/issue.py
index b936cdcda00..97e8e7cee0a 100644
--- a/apps/api/plane/api/views/issue.py
+++ b/apps/api/plane/api/views/issue.py
@@ -23,7 +23,11 @@
Value,
When,
Subquery,
+ UUIDField,
)
+from django.db.models.functions import Coalesce
+from django.contrib.postgres.aggregates import ArrayAgg
+from django.contrib.postgres.fields import ArrayField
from django.utils import timezone
from django.conf import settings
@@ -45,6 +49,9 @@
IssueActivitySerializer,
IssueCommentSerializer,
IssueLinkSerializer,
+ IssueRelationCreateSerializer,
+ IssueRelationResponseSerializer,
+ IssueRelationSerializer,
IssueSerializer,
LabelSerializer,
IssueAttachmentUploadSerializer,
@@ -53,6 +60,7 @@
IssueLinkCreateSerializer,
IssueLinkUpdateSerializer,
LabelCreateUpdateSerializer,
+ RelatedIssueSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
@@ -66,6 +74,7 @@
FileAsset,
IssueComment,
IssueLink,
+ IssueRelation,
Label,
Project,
ProjectMember,
@@ -76,10 +85,12 @@
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
from .base import BaseAPIView
from plane.utils.host import base_host
+from plane.utils.issue_relation_mapper import get_actual_relation
from plane.bgtasks.webhook_task import model_activity
from plane.app.permissions import ROLE
from plane.utils.openapi import (
work_item_docs,
+ work_item_relation_docs,
label_docs,
issue_link_docs,
issue_comment_docs,
@@ -629,6 +640,16 @@ def put(self, request, slug, project_id):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
+ # Send the model activity for webhook dispatch
+ model_activity.delay(
+ model_name="issue",
+ model_id=str(issue.id),
+ requested_data=request.data,
+ current_instance=current_instance,
+ actor_id=request.user.id,
+ slug=slug,
+ origin=base_host(request=request, is_app=True),
+ )
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
# If the serializer is not valid, respond with 400 bad
@@ -677,6 +698,16 @@ def put(self, request, slug, project_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
+ # Send the model activity for webhook dispatch
+ 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)
else:
@@ -752,6 +783,16 @@ def patch(self, request, slug, project_id, pk):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
+ # Send the model activity for webhook dispatch
+ model_activity.delay(
+ model_name="issue",
+ model_id=str(pk),
+ requested_data=request.data,
+ current_instance=current_instance,
+ actor_id=request.user.id,
+ slug=slug,
+ origin=base_host(request=request, is_app=True),
+ )
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1089,9 +1130,9 @@ def get(self, request, slug, project_id, issue_id):
return self.paginate(
request=request,
queryset=(self.get_queryset()),
- on_results=lambda issue_links: IssueLinkSerializer(
- issue_links, many=True, fields=self.fields, expand=self.expand
- ).data,
+ on_results=lambda issue_links: (
+ IssueLinkSerializer(issue_links, many=True, fields=self.fields, expand=self.expand).data
+ ),
)
@issue_link_docs(
@@ -1196,9 +1237,9 @@ def get(self, request, slug, project_id, issue_id, pk):
return self.paginate(
request=request,
queryset=(self.get_queryset()),
- on_results=lambda issue_links: IssueLinkSerializer(
- issue_links, many=True, fields=self.fields, expand=self.expand
- ).data,
+ on_results=lambda issue_links: (
+ IssueLinkSerializer(issue_links, many=True, fields=self.fields, expand=self.expand).data
+ ),
)
issue_link = self.get_queryset().get(pk=pk)
serializer = IssueLinkSerializer(issue_link, fields=self.fields, expand=self.expand)
@@ -1347,9 +1388,9 @@ def get(self, request, slug, project_id, issue_id):
return self.paginate(
request=request,
queryset=(self.get_queryset()),
- on_results=lambda issue_comments: IssueCommentSerializer(
- issue_comments, many=True, fields=self.fields, expand=self.expand
- ).data,
+ on_results=lambda issue_comments: (
+ IssueCommentSerializer(issue_comments, many=True, fields=self.fields, expand=self.expand).data
+ ),
)
@issue_comment_docs(
@@ -1658,9 +1699,9 @@ def get(self, request, slug, project_id, issue_id):
return self.paginate(
request=request,
queryset=(issue_activities),
- on_results=lambda issue_activity: IssueActivitySerializer(
- issue_activity, many=True, fields=self.fields, expand=self.expand
- ).data,
+ on_results=lambda issue_activity: (
+ IssueActivitySerializer(issue_activity, many=True, fields=self.fields, expand=self.expand).data
+ ),
)
@@ -2220,3 +2261,224 @@ def get(self, request, slug):
)[: int(limit)]
return Response({"issues": issue_results}, status=status.HTTP_200_OK)
+
+
+class IssueRelationListCreateAPIEndpoint(BaseAPIView):
+ """Issue Relation List and Create Endpoint"""
+
+ serializer_class = IssueRelationSerializer
+ model = IssueRelation
+ permission_classes = [ProjectEntityPermission]
+ use_read_replica = True
+
+ @work_item_relation_docs(
+ operation_id="list_work_item_relations",
+ summary="List work item relations",
+ description="Retrieve all relationships for a work item including blocking, blocked_by, duplicate, relates_to, start_before, start_after, finish_before, and finish_after relations.", # noqa E501
+ parameters=[
+ ISSUE_ID_PARAMETER,
+ CURSOR_PARAMETER,
+ PER_PAGE_PARAMETER,
+ ORDER_BY_PARAMETER,
+ FIELDS_PARAMETER,
+ EXPAND_PARAMETER,
+ ],
+ responses={
+ 200: OpenApiResponse(
+ description="Work item relations grouped by relation type",
+ response=IssueRelationResponseSerializer,
+ examples=[
+ OpenApiExample(
+ name="Work Item Relations Response",
+ value={
+ "blocking": [
+ "550e8400-e29b-41d4-a716-446655440000",
+ "550e8400-e29b-41d4-a716-446655440001",
+ ],
+ "blocked_by": ["550e8400-e29b-41d4-a716-446655440002"],
+ "duplicate": [],
+ "relates_to": ["550e8400-e29b-41d4-a716-446655440003"],
+ "start_after": [],
+ "start_before": ["550e8400-e29b-41d4-a716-446655440004"],
+ "finish_after": [],
+ "finish_before": [],
+ },
+ )
+ ],
+ ),
+ 400: INVALID_REQUEST_RESPONSE,
+ 404: ISSUE_NOT_FOUND_RESPONSE,
+ },
+ )
+ def get(self, request, slug, project_id, issue_id):
+ """List work item relations
+
+ Retrieve all relationships for a work item organized by relation type.
+ Returns a structured response with relations grouped by type.
+ """
+ empty_uuid_array = Value([], output_field=ArrayField(UUIDField()))
+
+ def _agg_ids(field, **filter_kwargs):
+ return Coalesce(
+ ArrayAgg(field, filter=Q(**filter_kwargs), distinct=True),
+ empty_uuid_array,
+ )
+
+ issue_relation_qs = IssueRelation.objects.filter(
+ Q(issue_id=issue_id) | Q(related_issue_id=issue_id),
+ workspace__slug=slug,
+ )
+
+ relation_ids = issue_relation_qs.aggregate(
+ blocking_ids=_agg_ids("issue_id", relation_type="blocked_by", related_issue_id=issue_id),
+ blocked_by_ids=_agg_ids("related_issue_id", relation_type="blocked_by", issue_id=issue_id),
+ duplicate_ids=_agg_ids("related_issue_id", relation_type="duplicate", issue_id=issue_id),
+ duplicate_ids_related=_agg_ids("issue_id", relation_type="duplicate", related_issue_id=issue_id),
+ relates_to_ids=_agg_ids("related_issue_id", relation_type="relates_to", issue_id=issue_id),
+ relates_to_ids_related=_agg_ids("issue_id", relation_type="relates_to", related_issue_id=issue_id),
+ start_after_ids=_agg_ids("issue_id", relation_type="start_before", related_issue_id=issue_id),
+ start_before_ids=_agg_ids("related_issue_id", relation_type="start_before", issue_id=issue_id),
+ finish_after_ids=_agg_ids("issue_id", relation_type="finish_before", related_issue_id=issue_id),
+ finish_before_ids=_agg_ids("related_issue_id", relation_type="finish_before", issue_id=issue_id),
+ )
+
+ response_data = {
+ "blocking": relation_ids["blocking_ids"],
+ "blocked_by": relation_ids["blocked_by_ids"],
+ "duplicate": list(set(relation_ids["duplicate_ids"] + relation_ids["duplicate_ids_related"])),
+ "relates_to": list(set(relation_ids["relates_to_ids"] + relation_ids["relates_to_ids_related"])),
+ "start_after": relation_ids["start_after_ids"],
+ "start_before": relation_ids["start_before_ids"],
+ "finish_after": relation_ids["finish_after_ids"],
+ "finish_before": relation_ids["finish_before_ids"],
+ }
+
+ return Response(response_data, status=status.HTTP_200_OK)
+
+ @work_item_relation_docs(
+ operation_id="create_work_item_relation",
+ summary="Create work item relation",
+ description="Create relationships between work items. Supports various relation types including blocking, blocked_by, duplicate, relates_to, start_before, start_after, finish_before, and finish_after.", # noqa E501
+ parameters=[
+ ISSUE_ID_PARAMETER,
+ ],
+ request=OpenApiRequest(
+ request=IssueRelationCreateSerializer,
+ examples=[
+ OpenApiExample(
+ name="Create blocking relation",
+ value={
+ "relation_type": "blocking",
+ "issues": [
+ "550e8400-e29b-41d4-a716-446655440000",
+ "550e8400-e29b-41d4-a716-446655440001",
+ ],
+ },
+ )
+ ],
+ ),
+ responses={
+ 201: OpenApiResponse(
+ description="Work item relations created successfully",
+ response=IssueRelationSerializer(many=True),
+ examples=[
+ OpenApiExample(
+ name="Relations created",
+ value=[
+ {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "Fix authentication bug",
+ "sequence_id": 42,
+ "project_id": "550e8400-e29b-41d4-a716-446655440001",
+ "relation_type": "blocked_by",
+ "state_id": "550e8400-e29b-41d4-a716-446655440002",
+ "priority": "high",
+ "created_at": "2024-01-15T10:00:00Z",
+ "updated_at": "2024-01-15T10:00:00Z",
+ "created_by": "550e8400-e29b-41d4-a716-446655440004",
+ "updated_by": "550e8400-e29b-41d4-a716-446655440004",
+ }
+ ],
+ )
+ ],
+ ),
+ 400: INVALID_REQUEST_RESPONSE,
+ 404: ISSUE_NOT_FOUND_RESPONSE,
+ },
+ )
+ def post(self, request, slug, project_id, issue_id):
+ """Create work item relation
+
+ Create relationships between work items with specified relation type.
+ Automatically tracks relation creation activity.
+ """
+ # Validate request data using serializer
+ serializer = IssueRelationCreateSerializer(data=request.data)
+ if not serializer.is_valid():
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ relation_type = serializer.validated_data["relation_type"]
+ issues = serializer.validated_data["issues"]
+ project = Project.objects.get(pk=project_id, workspace__slug=slug)
+
+ actual_relation = get_actual_relation(relation_type)
+ is_reverse = relation_type in ["blocking", "start_after", "finish_after"]
+
+ IssueRelation.objects.bulk_create(
+ [
+ IssueRelation(
+ issue_id=(issue if is_reverse else issue_id),
+ related_issue_id=(issue_id if is_reverse else issue),
+ relation_type=actual_relation,
+ project_id=project_id,
+ workspace_id=project.workspace_id,
+ created_by=request.user,
+ updated_by=request.user,
+ )
+ for issue in issues
+ ],
+ batch_size=10,
+ ignore_conflicts=True,
+ )
+
+ issue_activity.delay(
+ type="issue_relation.activity.created",
+ requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
+ actor_id=str(request.user.id),
+ issue_id=str(issue_id),
+ project_id=str(project_id),
+ current_instance=None,
+ epoch=int(timezone.now().timestamp()),
+ notification=True,
+ origin=base_host(request=request, is_app=True),
+ )
+
+ # Re-fetch with select_related to avoid N+1 queries in serializers.
+ # bulk_create with ignore_conflicts=True may not return PKs,
+ # so query by the issue/related_issue pairs and relation type.
+ if is_reverse:
+ refetch_filter = Q(
+ issue_id__in=issues,
+ related_issue_id=issue_id,
+ relation_type=actual_relation,
+ )
+ else:
+ refetch_filter = Q(
+ issue_id=issue_id,
+ related_issue_id__in=issues,
+ relation_type=actual_relation,
+ )
+
+ refetched_relations = IssueRelation.objects.filter(
+ refetch_filter,
+ workspace__slug=slug,
+ ).select_related(
+ "issue__state",
+ "related_issue__state",
+ )
+
+ serializer_class = RelatedIssueSerializer if is_reverse else IssueRelationSerializer
+ return Response(
+ serializer_class(refetched_relations, many=True).data,
+ status=status.HTTP_201_CREATED,
+ )
diff --git a/apps/api/plane/app/permissions/base.py b/apps/api/plane/app/permissions/base.py
index 7b243cbb789..9c451ed86ab 100644
--- a/apps/api/plane/app/permissions/base.py
+++ b/apps/api/plane/app/permissions/base.py
@@ -22,6 +22,17 @@ def decorator(view_func):
def _wrapped_view(instance, request, *args, **kwargs):
# Check for creator if required
if creator and model:
+ # check if the user is part of the workspace or not
+ if not WorkspaceMember.objects.filter(
+ member=request.user,
+ workspace__slug=kwargs["slug"],
+ is_active=True,
+ ).exists():
+ return Response(
+ {"error": "You don't have the required permissions."},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+
obj = model.objects.filter(id=kwargs["pk"], created_by=request.user).exists()
if obj:
return view_func(instance, request, *args, **kwargs)
diff --git a/apps/api/plane/app/urls/api.py b/apps/api/plane/app/urls/api.py
index eedf18ccc3d..145cfdd652d 100644
--- a/apps/api/plane/app/urls/api.py
+++ b/apps/api/plane/app/urls/api.py
@@ -3,7 +3,7 @@
# See the LICENSE file for details.
from django.urls import path
-from plane.app.views import ApiTokenEndpoint, ServiceApiTokenEndpoint
+from plane.app.views import ApiTokenEndpoint
urlpatterns = [
# API Tokens
@@ -17,10 +17,5 @@
ApiTokenEndpoint.as_view(),
name="api-tokens-details",
),
- path(
- "workspaces//service-api-tokens/",
- ServiceApiTokenEndpoint.as_view(),
- name="service-api-tokens",
- ),
## End API Tokens
]
diff --git a/apps/api/plane/app/views/__init__.py b/apps/api/plane/app/views/__init__.py
index baa6661b9cc..84f7872ec85 100644
--- a/apps/api/plane/app/views/__init__.py
+++ b/apps/api/plane/app/views/__init__.py
@@ -165,7 +165,7 @@
from .module.archive import ModuleArchiveUnarchiveEndpoint
-from .api import ApiTokenEndpoint, ServiceApiTokenEndpoint
+from .api import ApiTokenEndpoint
from .page.base import (
PageViewSet,
diff --git a/apps/api/plane/app/views/analytic/base.py b/apps/api/plane/app/views/analytic/base.py
index 2f3f8b5737d..a05712c4ecf 100644
--- a/apps/api/plane/app/views/analytic/base.py
+++ b/apps/api/plane/app/views/analytic/base.py
@@ -29,7 +29,7 @@
Module,
)
-from plane.utils.analytics_plot import build_graph_plot
+from plane.utils.analytics_plot import build_graph_plot, VALID_ANALYTICS_FIELDS, VALID_YAXIS
from plane.utils.issue_filters import issue_filters
from plane.app.permissions import allow_permission, ROLE
@@ -41,32 +41,15 @@ def get(self, request, slug):
y_axis = request.GET.get("y_axis", False)
segment = request.GET.get("segment", False)
- valid_xaxis_segment = [
- "state_id",
- "state__group",
- "labels__id",
- "assignees__id",
- "estimate_point__value",
- "issue_cycle__cycle_id",
- "issue_module__module_id",
- "priority",
- "start_date",
- "target_date",
- "created_at",
- "completed_at",
- ]
-
- valid_yaxis = ["issue_count", "estimate"]
-
# Check for x-axis and y-axis as thery are required parameters
- if not x_axis or not y_axis or x_axis not in valid_xaxis_segment or y_axis not in valid_yaxis:
+ if not x_axis or not y_axis or x_axis not in VALID_ANALYTICS_FIELDS or y_axis not in VALID_YAXIS:
return Response(
{"error": "x-axis and y-axis dimensions are required and the values should be valid"},
status=status.HTTP_400_BAD_REQUEST,
)
# If segment is present it cannot be same as x-axis
- if segment and (segment not in valid_xaxis_segment or x_axis == segment):
+ if segment and (segment not in VALID_ANALYTICS_FIELDS or x_axis == segment):
return Response(
{"error": "Both segment and x axis cannot be same and segment should be valid"},
status=status.HTTP_400_BAD_REQUEST,
@@ -214,13 +197,20 @@ def get(self, request, slug, analytic_id):
x_axis = analytic_view.query_dict.get("x_axis", False)
y_axis = analytic_view.query_dict.get("y_axis", False)
- if not x_axis or not y_axis:
+ if not x_axis or not y_axis or x_axis not in VALID_ANALYTICS_FIELDS or y_axis not in VALID_YAXIS:
return Response(
- {"error": "x-axis and y-axis dimensions are required"},
+ {"error": "x-axis and y-axis dimensions are required and the values should be valid"},
status=status.HTTP_400_BAD_REQUEST,
)
segment = request.GET.get("segment", False)
+
+ if segment and (segment not in VALID_ANALYTICS_FIELDS or x_axis == segment):
+ return Response(
+ {"error": "Both segment and x axis cannot be same and segment should be valid"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
distribution = build_graph_plot(queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment)
total_issues = queryset.count()
return Response(
@@ -236,32 +226,15 @@ def post(self, request, slug):
y_axis = request.data.get("y_axis", False)
segment = request.data.get("segment", False)
- valid_xaxis_segment = [
- "state_id",
- "state__group",
- "labels__id",
- "assignees__id",
- "estimate_point",
- "issue_cycle__cycle_id",
- "issue_module__module_id",
- "priority",
- "start_date",
- "target_date",
- "created_at",
- "completed_at",
- ]
-
- valid_yaxis = ["issue_count", "estimate"]
-
# Check for x-axis and y-axis as thery are required parameters
- if not x_axis or not y_axis or x_axis not in valid_xaxis_segment or y_axis not in valid_yaxis:
+ if not x_axis or not y_axis or x_axis not in VALID_ANALYTICS_FIELDS or y_axis not in VALID_YAXIS:
return Response(
{"error": "x-axis and y-axis dimensions are required and the values should be valid"},
status=status.HTTP_400_BAD_REQUEST,
)
# If segment is present it cannot be same as x-axis
- if segment and (segment not in valid_xaxis_segment or x_axis == segment):
+ if segment and (segment not in VALID_ANALYTICS_FIELDS or x_axis == segment):
return Response(
{"error": "Both segment and x axis cannot be same and segment should be valid"},
status=status.HTTP_400_BAD_REQUEST,
diff --git a/apps/api/plane/app/views/api.py b/apps/api/plane/app/views/api.py
index f2abc1a2dec..f3163c33146 100644
--- a/apps/api/plane/app/views/api.py
+++ b/apps/api/plane/app/views/api.py
@@ -13,9 +13,8 @@
# Module import
from .base import BaseAPIView
-from plane.db.models import APIToken, Workspace
+from plane.db.models import APIToken
from plane.app.serializers import APITokenSerializer, APITokenReadSerializer
-from plane.app.permissions import WorkspaceEntityPermission
class ApiTokenEndpoint(BaseAPIView):
@@ -61,28 +60,3 @@ def patch(self, request: Request, pk: str) -> Response:
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-
-class ServiceApiTokenEndpoint(BaseAPIView):
- permission_classes = [WorkspaceEntityPermission]
-
- def post(self, request: Request, slug: str) -> Response:
- workspace = Workspace.objects.get(slug=slug)
-
- api_token = APIToken.objects.filter(workspace=workspace, is_service=True).first()
-
- if api_token:
- return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK)
- else:
- # Check the user type
- user_type = 1 if request.user.is_bot else 0
-
- api_token = APIToken.objects.create(
- label=str(uuid4().hex),
- description="Service Token",
- user=request.user,
- workspace=workspace,
- user_type=user_type,
- is_service=True,
- )
- return Response({"token": str(api_token.token)}, status=status.HTTP_201_CREATED)
diff --git a/apps/api/plane/app/views/issue/attachment.py b/apps/api/plane/app/views/issue/attachment.py
index fa03ae5f1c3..df027c413b1 100644
--- a/apps/api/plane/app/views/issue/attachment.py
+++ b/apps/api/plane/app/views/issue/attachment.py
@@ -64,7 +64,10 @@ def delete(self, request, slug, project_id, issue_id, pk):
pk=pk, workspace__slug=slug, project_id=project_id, issue_id=issue_id
).first()
if not issue_attachment:
- return Response(status=status.HTTP_404_NOT_FOUND)
+ return Response(
+ {"error": "Issue attachment not found."},
+ status=status.HTTP_404_NOT_FOUND,
+ )
issue_attachment.asset.delete(save=False)
issue_attachment.delete()
issue_activity.delay(
diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py
index 98a59b6481c..bb331802c84 100644
--- a/apps/api/plane/app/views/issue/base.py
+++ b/apps/api/plane/app/views/issue/base.py
@@ -1118,7 +1118,7 @@ def post(self, request, slug, project_id):
epoch = int(timezone.now().timestamp())
# Fetch all relevant issues in a single query
- issues = list(Issue.objects.filter(id__in=issue_ids))
+ issues = list(Issue.objects.filter(id__in=issue_ids, workspace__slug=slug, project_id=project_id))
issues_dict = {str(issue.id): issue for issue in issues}
issues_to_update = []
diff --git a/apps/api/plane/app/views/issue/sub_issue.py b/apps/api/plane/app/views/issue/sub_issue.py
index b52e07564f6..5194148dd7b 100644
--- a/apps/api/plane/app/views/issue/sub_issue.py
+++ b/apps/api/plane/app/views/issue/sub_issue.py
@@ -7,7 +7,7 @@
# Django imports
from django.utils import timezone
-from django.db.models import OuterRef, Func, F, Q, Value, UUIDField, Subquery
+from django.db.models import OuterRef, Func, F, Q, Value, UUIDField, Subquery, Count, IntegerField
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.contrib.postgres.aggregates import ArrayAgg
@@ -22,7 +22,7 @@
from .. import BaseAPIView
from plane.app.serializers import IssueSerializer
from plane.app.permissions import ProjectEntityPermission
-from plane.db.models import Issue, IssueLink, FileAsset, CycleIssue
+from plane.db.models import Issue, IssueLink, FileAsset, CycleIssue, IssueLabel, IssueAssignee, ModuleIssue
from plane.bgtasks.issue_activities_task import issue_activity
from plane.utils.timezone_converter import user_timezone_converter
from collections import defaultdict
@@ -37,70 +37,97 @@ class SubIssuesEndpoint(BaseAPIView):
def get(self, request, slug, project_id, issue_id):
sub_issues = (
Issue.issue_objects.filter(parent_id=issue_id, workspace__slug=slug)
- .select_related("workspace", "project", "state", "parent")
- .prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
)
)
.annotate(
- link_count=IssueLink.objects.filter(issue=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
+ link_count=Coalesce(
+ Subquery(
+ IssueLink.objects.filter(issue=OuterRef("id"))
+ .order_by()
+ .values("issue")
+ .annotate(count=Count("id"))
+ .values("count"),
+ output_field=IntegerField(),
+ ),
+ 0,
+ )
)
.annotate(
- attachment_count=FileAsset.objects.filter(
- issue_id=OuterRef("id"),
- entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
+ attachment_count=Coalesce(
+ Subquery(
+ FileAsset.objects.filter(
+ issue_id=OuterRef("id"),
+ entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
+ )
+ .order_by()
+ .values("issue_id")
+ .annotate(count=Count("id"))
+ .values("count"),
+ output_field=IntegerField(),
+ ),
+ 0,
)
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
)
.annotate(
- sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
+ sub_issues_count=Coalesce(
+ Subquery(
+ Issue.issue_objects.filter(parent=OuterRef("id"))
+ .order_by()
+ .values("parent")
+ .annotate(count=Count("id"))
+ .values("count"),
+ output_field=IntegerField(),
+ ),
+ 0,
+ )
)
.annotate(
label_ids=Coalesce(
- ArrayAgg(
- "labels__id",
- distinct=True,
- filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
+ Subquery(
+ IssueLabel.objects.filter(issue_id=OuterRef("id"), deleted_at__isnull=True)
+ .order_by()
+ .values("issue_id")
+ .annotate(arr=ArrayAgg("label_id", distinct=True))
+ .values("arr"),
+ output_field=ArrayField(UUIDField()),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
- ArrayAgg(
- "assignees__id",
- distinct=True,
- filter=Q(
- ~Q(assignees__id__isnull=True)
- & Q(assignees__member_project__is_active=True)
- & Q(issue_assignee__deleted_at__isnull=True)
- ),
+ Subquery(
+ IssueAssignee.objects.filter(
+ issue_id=OuterRef("id"),
+ assignee__member_project__is_active=True,
+ deleted_at__isnull=True,
+ )
+ .order_by()
+ .values("issue_id")
+ .annotate(arr=ArrayAgg("assignee_id", distinct=True))
+ .values("arr"),
+ output_field=ArrayField(UUIDField()),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
- ArrayAgg(
- "issue_module__module_id",
- distinct=True,
- filter=Q(
- ~Q(issue_module__module_id__isnull=True)
- & Q(issue_module__module__archived_at__isnull=True)
- & Q(issue_module__deleted_at__isnull=True)
- ),
+ Subquery(
+ ModuleIssue.objects.filter(
+ issue_id=OuterRef("id"),
+ module__archived_at__isnull=True,
+ deleted_at__isnull=True,
+ )
+ .order_by()
+ .values("issue_id")
+ .annotate(arr=ArrayAgg("module_id", distinct=True))
+ .values("arr"),
+ output_field=ArrayField(UUIDField()),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.annotate(state_group=F("state__group"))
- .order_by("-created_at")
)
# Ordering
@@ -110,38 +137,42 @@ def get(self, request, slug, project_id, issue_id):
if order_by_param:
sub_issues, order_by_param = order_issue_queryset(sub_issues, order_by_param)
+ sub_issues = list(
+ sub_issues.values(
+ "id",
+ "name",
+ "state_id",
+ "sort_order",
+ "completed_at",
+ "estimate_point",
+ "priority",
+ "start_date",
+ "target_date",
+ "sequence_id",
+ "project_id",
+ "parent_id",
+ "cycle_id",
+ "module_ids",
+ "label_ids",
+ "assignee_ids",
+ "sub_issues_count",
+ "created_at",
+ "updated_at",
+ "created_by",
+ "updated_by",
+ "attachment_count",
+ "link_count",
+ "is_draft",
+ "archived_at",
+ "state_group",
+ )
+ )
+
# create's a dict with state group name with their respective issue id's
result = defaultdict(list)
for sub_issue in sub_issues:
- result[sub_issue.state_group].append(str(sub_issue.id))
+ result[sub_issue["state_group"]].append(str(sub_issue["id"]))
- sub_issues = sub_issues.values(
- "id",
- "name",
- "state_id",
- "sort_order",
- "completed_at",
- "estimate_point",
- "priority",
- "start_date",
- "target_date",
- "sequence_id",
- "project_id",
- "parent_id",
- "cycle_id",
- "module_ids",
- "label_ids",
- "assignee_ids",
- "sub_issues_count",
- "created_at",
- "updated_at",
- "created_by",
- "updated_by",
- "attachment_count",
- "link_count",
- "is_draft",
- "archived_at",
- )
datetime_fields = ["created_at", "updated_at"]
sub_issues = user_timezone_converter(sub_issues, datetime_fields, request.user.user_timezone)
# Grouping
diff --git a/apps/api/plane/app/views/project/member.py b/apps/api/plane/app/views/project/member.py
index 1ad7639fb8b..7dfe7090013 100644
--- a/apps/api/plane/app/views/project/member.py
+++ b/apps/api/plane/app/views/project/member.py
@@ -226,21 +226,36 @@ def partial_update(self, request, slug, project_id, pk):
is_active=True,
)
- if workspace_role in [5] and int(request.data.get("role", project_member.role)) in [15, 20]:
- return Response(
- {"error": "You cannot add a user with role higher than the workspace role"},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ if "role" in request.data:
+ # Only Admins can modify roles
+ if requested_project_member.role < ROLE.ADMIN.value and not is_workspace_admin:
+ return Response(
+ {"error": "You do not have permission to update roles"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
- if (
- "role" in request.data
- and int(request.data.get("role", project_member.role)) > requested_project_member.role
- and not is_workspace_admin
- ):
- return Response(
- {"error": "You cannot update a role that is higher than your own role"},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ # Cannot modify a member whose role is equal to or higher than your own
+ if project_member.role >= requested_project_member.role and not is_workspace_admin:
+ return Response(
+ {"error": "You cannot update the role of a member with a role equal to or higher than your own"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+
+ new_role = int(request.data.get("role"))
+
+ # Cannot assign a role equal to or higher than your own
+ if new_role >= requested_project_member.role and not is_workspace_admin:
+ return Response(
+ {"error": "You cannot assign a role equal to or higher than your own"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+
+ # Cannot assign a role higher than the target's workspace role
+ if workspace_role in [5] and new_role in [15, 20]:
+ return Response(
+ {"error": "You cannot add a user with role higher than the workspace role"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True)
diff --git a/apps/api/plane/bgtasks/work_item_link_task.py b/apps/api/plane/bgtasks/work_item_link_task.py
index 442396c7f02..5cf0fbb1908 100644
--- a/apps/api/plane/bgtasks/work_item_link_task.py
+++ b/apps/api/plane/bgtasks/work_item_link_task.py
@@ -13,7 +13,7 @@
from urllib.parse import urlparse, urljoin
import base64
import ipaddress
-from typing import Dict, Any
+from typing import Dict, Any, Tuple
from typing import Optional
from plane.db.models import IssueLink
from plane.utils.exception_logger import log_exception
@@ -66,6 +66,52 @@ def validate_url_ip(url: str) -> None:
MAX_REDIRECTS = 5
+def safe_get(
+ url: str,
+ headers: Optional[Dict[str, str]] = None,
+ timeout: int = 1,
+) -> Tuple[requests.Response, str]:
+ """
+ Perform a GET request that validates every redirect hop against private IPs.
+ Prevents SSRF by ensuring no redirect lands on a private/internal address.
+
+ Args:
+ url: The URL to fetch
+ headers: Optional request headers
+ timeout: Request timeout in seconds
+
+ Returns:
+ A tuple of (final Response object, final URL after redirects)
+
+ Raises:
+ ValueError: If any URL in the redirect chain points to a private IP
+ requests.RequestException: On network errors
+ RuntimeError: If max redirects exceeded
+ """
+ validate_url_ip(url)
+
+ current_url = url
+ response = requests.get(
+ current_url, headers=headers, timeout=timeout, allow_redirects=False
+ )
+
+ redirect_count = 0
+ while response.is_redirect:
+ if redirect_count >= MAX_REDIRECTS:
+ raise RuntimeError(f"Too many redirects for URL: {url}")
+ redirect_url = response.headers.get("Location")
+ if not redirect_url:
+ break
+ current_url = urljoin(current_url, redirect_url)
+ validate_url_ip(current_url)
+ redirect_count += 1
+ response = requests.get(
+ current_url, headers=headers, timeout=timeout, allow_redirects=False
+ )
+
+ return response, current_url
+
+
def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
"""
Crawls a URL to extract the title and favicon.
@@ -86,26 +132,8 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
title = None
final_url = url
- validate_url_ip(final_url)
-
try:
- # Manually follow redirects to validate each URL before requesting
- redirect_count = 0
- response = requests.get(final_url, headers=headers, timeout=1, allow_redirects=False)
-
- while response.is_redirect and redirect_count < MAX_REDIRECTS:
- redirect_url = response.headers.get("Location")
- if not redirect_url:
- break
- # Resolve relative redirects against current URL
- final_url = urljoin(final_url, redirect_url)
- # Validate the redirect target BEFORE making the request
- validate_url_ip(final_url)
- redirect_count += 1
- response = requests.get(final_url, headers=headers, timeout=1, allow_redirects=False)
-
- if redirect_count >= MAX_REDIRECTS:
- logger.warning(f"Too many redirects for URL: {url}")
+ response, final_url = safe_get(url, headers=headers)
soup = BeautifulSoup(response.content, "html.parser")
title_tag = soup.find("title")
@@ -113,8 +141,10 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
except requests.RequestException as e:
logger.warning(f"Failed to fetch HTML for title: {str(e)}")
+ except (ValueError, RuntimeError) as e:
+ logger.warning(f"URL validation failed: {str(e)}")
- # Fetch and encode favicon using final URL (after redirects)
+ # Fetch and encode favicon using final URL (after redirects) for correct relative href resolution
favicon_base64 = fetch_and_encode_favicon(headers, soup, final_url)
# Prepare result
@@ -204,9 +234,7 @@ def fetch_and_encode_favicon(
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
}
- validate_url_ip(favicon_url)
-
- response = requests.get(favicon_url, headers=headers, timeout=1)
+ response, _ = safe_get(favicon_url, headers=headers)
# Get content type
content_type = response.headers.get("content-type", "image/x-icon")
diff --git a/apps/api/plane/db/migrations/0121_alter_estimate_type.py b/apps/api/plane/db/migrations/0121_alter_estimate_type.py
new file mode 100644
index 00000000000..73b75123f63
--- /dev/null
+++ b/apps/api/plane/db/migrations/0121_alter_estimate_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.28 on 2026-02-26 14:37
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('db', '0120_issueview_archived_at'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='estimate',
+ name='type',
+ field=models.CharField(choices=[('categories', 'Categories'), ('points', 'Points')], default='categories', max_length=255),
+ ),
+ ]
diff --git a/apps/api/plane/db/models/estimate.py b/apps/api/plane/db/models/estimate.py
index ded6f97bcf3..fb472a69bd4 100644
--- a/apps/api/plane/db/models/estimate.py
+++ b/apps/api/plane/db/models/estimate.py
@@ -10,11 +10,15 @@
# Module imports
from .project import ProjectBaseModel
+class EstimateType(models.TextChoices):
+ CATEGORIES = "categories", "Categories"
+ POINTS = "points", "Points"
+
class Estimate(ProjectBaseModel):
name = models.CharField(max_length=255)
description = models.TextField(verbose_name="Estimate Description", blank=True)
- type = models.CharField(max_length=255, default="categories")
+ type = models.CharField(max_length=255, choices=EstimateType.choices, default=EstimateType.CATEGORIES)
last_used = models.BooleanField(default=False)
def __str__(self):
diff --git a/apps/api/plane/license/api/views/configuration.py b/apps/api/plane/license/api/views/configuration.py
index bb9a9e00ee6..c5ad5f5788b 100644
--- a/apps/api/plane/license/api/views/configuration.py
+++ b/apps/api/plane/license/api/views/configuration.py
@@ -45,7 +45,8 @@ def patch(self, request):
bulk_configurations = []
for configuration in configurations:
- value = request.data.get(configuration.key, configuration.value)
+ raw_value = request.data.get(configuration.key, configuration.value)
+ value = "" if raw_value is None else str(raw_value).strip()
if configuration.is_encrypted:
configuration.value = encrypt_data(value)
else:
diff --git a/apps/api/plane/license/api/views/instance.py b/apps/api/plane/license/api/views/instance.py
index a0d52d4912f..29c2521abd8 100644
--- a/apps/api/plane/license/api/views/instance.py
+++ b/apps/api/plane/license/api/views/instance.py
@@ -63,8 +63,6 @@ def get(self, request):
POSTHOG_HOST,
UNSPLASH_ACCESS_KEY,
LLM_API_KEY,
- IS_INTERCOM_ENABLED,
- INTERCOM_APP_ID,
) = get_configuration_value(
[
{
@@ -124,15 +122,6 @@ def get(self, request):
"key": "LLM_API_KEY",
"default": os.environ.get("LLM_API_KEY", ""),
},
- # Intercom settings
- {
- "key": "IS_INTERCOM_ENABLED",
- "default": os.environ.get("IS_INTERCOM_ENABLED", "1"),
- },
- {
- "key": "INTERCOM_APP_ID",
- "default": os.environ.get("INTERCOM_APP_ID", ""),
- },
]
)
@@ -169,10 +158,6 @@ def get(self, request):
# is smtp configured
data["is_smtp_configured"] = bool(EMAIL_HOST)
- # Intercom settings
- data["is_intercom_enabled"] = IS_INTERCOM_ENABLED == "1"
- data["intercom_app_id"] = INTERCOM_APP_ID
-
# Base URL
data["admin_base_url"] = settings.ADMIN_BASE_URL
data["space_base_url"] = settings.SPACE_BASE_URL
diff --git a/apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py b/apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py
new file mode 100644
index 00000000000..2838260e890
--- /dev/null
+++ b/apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py
@@ -0,0 +1,126 @@
+# Copyright (c) 2023-present Plane Software, Inc. and contributors
+# SPDX-License-Identifier: AGPL-3.0-only
+# See the LICENSE file for details.
+
+import pytest
+from unittest.mock import patch, MagicMock
+from plane.bgtasks.work_item_link_task import safe_get, validate_url_ip
+
+
+def _make_response(status_code=200, headers=None, is_redirect=False, content=b""):
+ """Create a mock requests.Response."""
+ resp = MagicMock()
+ resp.status_code = status_code
+ resp.is_redirect = is_redirect
+ resp.headers = headers or {}
+ resp.content = content
+ return resp
+
+
+@pytest.mark.unit
+class TestValidateUrlIp:
+ """Test validate_url_ip blocks private/internal IPs."""
+
+ def test_rejects_private_ip(self):
+ with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
+ mock_dns.return_value = [(None, None, None, None, ("192.168.1.1", 0))]
+ with pytest.raises(ValueError, match="private/internal"):
+ validate_url_ip("http://example.com")
+
+ def test_rejects_loopback(self):
+ with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
+ mock_dns.return_value = [(None, None, None, None, ("127.0.0.1", 0))]
+ with pytest.raises(ValueError, match="private/internal"):
+ validate_url_ip("http://example.com")
+
+ def test_rejects_non_http_scheme(self):
+ with pytest.raises(ValueError, match="Only HTTP and HTTPS"):
+ validate_url_ip("file:///etc/passwd")
+
+ def test_allows_public_ip(self):
+ with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
+ mock_dns.return_value = [(None, None, None, None, ("93.184.216.34", 0))]
+ validate_url_ip("https://example.com") # Should not raise
+
+
+@pytest.mark.unit
+class TestSafeGet:
+ """Test safe_get follows redirects safely and blocks SSRF."""
+
+ @patch("plane.bgtasks.work_item_link_task.requests.get")
+ @patch("plane.bgtasks.work_item_link_task.validate_url_ip")
+ def test_returns_response_for_non_redirect(self, mock_validate, mock_get):
+ final_resp = _make_response(status_code=200, content=b"OK")
+ mock_get.return_value = final_resp
+
+ response, final_url = safe_get("https://example.com")
+
+ assert response is final_resp
+ assert final_url == "https://example.com"
+ mock_validate.assert_called_once_with("https://example.com")
+
+ @patch("plane.bgtasks.work_item_link_task.requests.get")
+ @patch("plane.bgtasks.work_item_link_task.validate_url_ip")
+ def test_follows_redirect_and_validates_each_hop(self, mock_validate, mock_get):
+ redirect_resp = _make_response(
+ status_code=301,
+ is_redirect=True,
+ headers={"Location": "https://other.com/page"},
+ )
+ final_resp = _make_response(status_code=200, content=b"OK")
+ mock_get.side_effect = [redirect_resp, final_resp]
+
+ response, final_url = safe_get("https://example.com")
+
+ assert response is final_resp
+ assert final_url == "https://other.com/page"
+ # Should validate both the initial URL and the redirect target
+ assert mock_validate.call_count == 2
+ mock_validate.assert_any_call("https://example.com")
+ mock_validate.assert_any_call("https://other.com/page")
+
+ @patch("plane.bgtasks.work_item_link_task.requests.get")
+ @patch("plane.bgtasks.work_item_link_task.validate_url_ip")
+ def test_blocks_redirect_to_private_ip(self, mock_validate, mock_get):
+ redirect_resp = _make_response(
+ status_code=302,
+ is_redirect=True,
+ headers={"Location": "http://192.168.1.1:8080"},
+ )
+ mock_get.return_value = redirect_resp
+ # First call (initial URL) succeeds, second call (redirect target) fails
+ mock_validate.side_effect = [None, ValueError("Access to private/internal networks is not allowed")]
+
+ with pytest.raises(ValueError, match="private/internal"):
+ safe_get("https://evil.com/redirect")
+
+ @patch("plane.bgtasks.work_item_link_task.requests.get")
+ @patch("plane.bgtasks.work_item_link_task.validate_url_ip")
+ def test_raises_on_too_many_redirects(self, mock_validate, mock_get):
+ redirect_resp = _make_response(
+ status_code=302,
+ is_redirect=True,
+ headers={"Location": "https://example.com/loop"},
+ )
+ mock_get.return_value = redirect_resp
+
+ with pytest.raises(RuntimeError, match="Too many redirects"):
+ safe_get("https://example.com/start")
+
+ @patch("plane.bgtasks.work_item_link_task.requests.get")
+ @patch("plane.bgtasks.work_item_link_task.validate_url_ip")
+ def test_succeeds_at_exact_max_redirects(self, mock_validate, mock_get):
+ """After exactly MAX_REDIRECTS hops, if the final response is 200, it should succeed."""
+ redirect_resp = _make_response(
+ status_code=302,
+ is_redirect=True,
+ headers={"Location": "https://example.com/next"},
+ )
+ final_resp = _make_response(status_code=200, content=b"OK")
+ # 5 redirects then a 200
+ mock_get.side_effect = [redirect_resp] * 5 + [final_resp]
+
+ response, final_url = safe_get("https://example.com/start")
+
+ assert response is final_resp
+ assert not response.is_redirect
diff --git a/apps/api/plane/utils/analytics_plot.py b/apps/api/plane/utils/analytics_plot.py
index acd86aca868..5a09f12fdb6 100644
--- a/apps/api/plane/utils/analytics_plot.py
+++ b/apps/api/plane/utils/analytics_plot.py
@@ -22,6 +22,23 @@
# Module imports
from plane.db.models import Issue, Project
+VALID_ANALYTICS_FIELDS = [
+ "state_id",
+ "state__group",
+ "labels__id",
+ "assignees__id",
+ "estimate_point__value",
+ "issue_cycle__cycle_id",
+ "issue_module__module_id",
+ "priority",
+ "start_date",
+ "target_date",
+ "created_at",
+ "completed_at",
+]
+
+VALID_YAXIS = ["issue_count", "estimate"]
+
def annotate_with_monthly_dimension(queryset, field_name, attribute):
# Get the year and the months
@@ -34,6 +51,8 @@ def annotate_with_monthly_dimension(queryset, field_name, attribute):
def extract_axis(queryset, x_axis):
+ if x_axis not in VALID_ANALYTICS_FIELDS:
+ raise ValueError(f"Invalid x_axis value: {x_axis}")
# Format the dimension when the axis is in date
if x_axis in ["created_at", "start_date", "target_date", "completed_at"]:
queryset = annotate_with_monthly_dimension(queryset, x_axis, "dimension")
@@ -52,6 +71,13 @@ def sort_data(data, temp_axis):
def build_graph_plot(queryset, x_axis, y_axis, segment=None):
+ if x_axis not in VALID_ANALYTICS_FIELDS:
+ raise ValueError(f"Invalid x_axis value: {x_axis}")
+ if y_axis not in VALID_YAXIS:
+ raise ValueError(f"Invalid y_axis value: {y_axis}")
+ if segment and segment not in VALID_ANALYTICS_FIELDS:
+ raise ValueError(f"Invalid segment value: {segment}")
+
# temp x_axis
temp_axis = x_axis
# Extract the x_axis and queryset
diff --git a/apps/api/plane/utils/instance_config_variables/core.py b/apps/api/plane/utils/instance_config_variables/core.py
index 274c6539af9..6eebf0b3adb 100644
--- a/apps/api/plane/utils/instance_config_variables/core.py
+++ b/apps/api/plane/utils/instance_config_variables/core.py
@@ -232,21 +232,6 @@
},
]
-intercom_config_variables = [
- {
- "key": "IS_INTERCOM_ENABLED",
- "value": os.environ.get("IS_INTERCOM_ENABLED", "1"),
- "category": "INTERCOM",
- "is_encrypted": False,
- },
- {
- "key": "INTERCOM_APP_ID",
- "value": os.environ.get("INTERCOM_APP_ID", ""),
- "category": "INTERCOM",
- "is_encrypted": False,
- },
-]
-
core_config_variables = [
*authentication_config_variables,
*workspace_management_config_variables,
@@ -257,5 +242,4 @@
*smtp_config_variables,
*llm_config_variables,
*unsplash_config_variables,
- *intercom_config_variables,
]
diff --git a/apps/api/plane/utils/openapi/__init__.py b/apps/api/plane/utils/openapi/__init__.py
index d54caf584eb..090d076ecd1 100644
--- a/apps/api/plane/utils/openapi/__init__.py
+++ b/apps/api/plane/utils/openapi/__init__.py
@@ -47,6 +47,7 @@
CYCLE_VIEW_PARAMETER,
FIELDS_PARAMETER,
EXPAND_PARAMETER,
+ ESTIMATE_ID_PARAMETER,
)
# Responses
@@ -126,6 +127,10 @@
STATE_UPDATE_EXAMPLE,
INTAKE_ISSUE_CREATE_EXAMPLE,
INTAKE_ISSUE_UPDATE_EXAMPLE,
+ ESTIMATE_CREATE_EXAMPLE,
+ ESTIMATE_UPDATE_EXAMPLE,
+ ESTIMATE_POINT_CREATE_EXAMPLE,
+ ESTIMATE_POINT_UPDATE_EXAMPLE,
# Response Examples
CYCLE_EXAMPLE,
TRANSFER_CYCLE_ISSUE_SUCCESS_EXAMPLE,
@@ -145,6 +150,8 @@
PROJECT_MEMBER_EXAMPLE,
CYCLE_ISSUE_EXAMPLE,
STICKY_EXAMPLE,
+ ESTIMATE_EXAMPLE,
+ ESTIMATE_POINT_EXAMPLE,
)
# Helper decorators
@@ -157,6 +164,7 @@
user_docs,
cycle_docs,
work_item_docs,
+ work_item_relation_docs,
label_docs,
issue_link_docs,
issue_comment_docs,
@@ -165,6 +173,8 @@
module_docs,
module_issue_docs,
state_docs,
+ estimate_docs,
+ estimate_point_docs,
)
# Schema processing hooks
@@ -206,6 +216,7 @@
"CYCLE_VIEW_PARAMETER",
"FIELDS_PARAMETER",
"EXPAND_PARAMETER",
+ "ESTIMATE_ID_PARAMETER",
# Responses
"UNAUTHORIZED_RESPONSE",
"FORBIDDEN_RESPONSE",
@@ -279,6 +290,10 @@
"STATE_UPDATE_EXAMPLE",
"INTAKE_ISSUE_CREATE_EXAMPLE",
"INTAKE_ISSUE_UPDATE_EXAMPLE",
+ "ESTIMATE_CREATE_EXAMPLE",
+ "ESTIMATE_UPDATE_EXAMPLE",
+ "ESTIMATE_POINT_CREATE_EXAMPLE",
+ "ESTIMATE_POINT_UPDATE_EXAMPLE",
# Response Examples
"CYCLE_EXAMPLE",
"TRANSFER_CYCLE_ISSUE_SUCCESS_EXAMPLE",
@@ -298,6 +313,8 @@
"PROJECT_MEMBER_EXAMPLE",
"CYCLE_ISSUE_EXAMPLE",
"STICKY_EXAMPLE",
+ "ESTIMATE_EXAMPLE",
+ "ESTIMATE_POINT_EXAMPLE",
# Decorators
"workspace_docs",
"project_docs",
@@ -307,6 +324,7 @@
"user_docs",
"cycle_docs",
"work_item_docs",
+ "work_item_relation_docs",
"label_docs",
"issue_link_docs",
"issue_comment_docs",
@@ -315,6 +333,8 @@
"module_docs",
"module_issue_docs",
"state_docs",
+ "estimate_docs",
+ "estimate_point_docs",
# Hooks
"preprocess_filter_api_v1_paths",
"generate_operation_summary",
diff --git a/apps/api/plane/utils/openapi/decorators.py b/apps/api/plane/utils/openapi/decorators.py
index 8b016f4c016..7ded9fb10b3 100644
--- a/apps/api/plane/utils/openapi/decorators.py
+++ b/apps/api/plane/utils/openapi/decorators.py
@@ -223,6 +223,21 @@ def issue_attachment_docs(**kwargs):
return extend_schema(**_merge_schema_options(defaults, kwargs))
+def work_item_relation_docs(**kwargs):
+ """Decorator for work item relation endpoints"""
+ defaults = {
+ "tags": ["Work Item Relations"],
+ "parameters": [WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
+ "responses": {
+ 401: UNAUTHORIZED_RESPONSE,
+ 403: FORBIDDEN_RESPONSE,
+ 404: NOT_FOUND_RESPONSE,
+ },
+ }
+
+ return extend_schema(**_merge_schema_options(defaults, kwargs))
+
+
def module_docs(**kwargs):
"""Decorator for module management endpoints"""
defaults = {
@@ -282,3 +297,29 @@ def sticky_docs(**kwargs):
}
return extend_schema(**_merge_schema_options(defaults, kwargs))
+
+def estimate_docs(**kwargs):
+ """Decorator for estimate-related endpoints"""
+ defaults = {
+ "tags": ["Estimates"],
+ "parameters": [WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
+ "responses": {
+ 401: UNAUTHORIZED_RESPONSE,
+ 403: FORBIDDEN_RESPONSE,
+ 404: NOT_FOUND_RESPONSE,
+ },
+ }
+ return extend_schema(**_merge_schema_options(defaults, kwargs))
+
+def estimate_point_docs(**kwargs):
+ """Decorator for estimate point-related endpoints"""
+ defaults = {
+ "tags": ["Estimate Points"],
+ "parameters": [WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
+ "responses": {
+ 401: UNAUTHORIZED_RESPONSE,
+ 403: FORBIDDEN_RESPONSE,
+ 404: NOT_FOUND_RESPONSE,
+ },
+ }
+ return extend_schema(**_merge_schema_options(defaults, kwargs))
\ No newline at end of file
diff --git a/apps/api/plane/utils/openapi/examples.py b/apps/api/plane/utils/openapi/examples.py
index 5a2188e6951..20aff18958a 100644
--- a/apps/api/plane/utils/openapi/examples.py
+++ b/apps/api/plane/utils/openapi/examples.py
@@ -686,6 +686,69 @@
},
)
+# Estimate Examples
+ESTIMATE_EXAMPLE = OpenApiExample(
+ name="Estimate",
+ value={
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "Estimate 1",
+ "description": "Estimate 1 description",
+ },
+ description="Example response for an estimate",
+)
+
+ESTIMATE_POINT_EXAMPLE = OpenApiExample(
+ name="EstimatePoint",
+ value={
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "estimate": "550e8400-e29b-41d4-a716-446655440001",
+ "key": 1,
+ "value": "1",
+ },
+ description="Example response for an estimate point",
+)
+ESTIMATE_CREATE_EXAMPLE = OpenApiExample(
+ name="EstimateCreateSerializer",
+ value={
+ "name": "Estimate 1",
+ "description": "Estimate 1 description",
+ },
+ description="Example request for creating an estimate",
+)
+ESTIMATE_UPDATE_EXAMPLE = OpenApiExample(
+ name="EstimateUpdateSerializer",
+ value={
+ "name": "Estimate 1",
+ "description": "Estimate 1 description",
+ },
+ description="Example request for updating an estimate",
+)
+
+# Estimate Point Examples
+ESTIMATE_POINT_CREATE_EXAMPLE = OpenApiExample(
+ name="EstimatePointCreateSerializer",
+ value=[
+ {
+ "value": "1",
+ "description": "Estimate Point 1 description",
+ },
+ {
+ "value": "2",
+ "description": "Estimate Point 2 description",
+ },
+ ],
+ description="Example request for creating an estimate point",
+)
+ESTIMATE_POINT_UPDATE_EXAMPLE = OpenApiExample(
+ name="EstimatePointUpdateSerializer",
+ value={
+ "value": "1",
+ "description": "Estimate Point 1 description",
+ },
+ description="Example request for updating an estimate point",
+)
+
+
# Sample data for different entity types
SAMPLE_ISSUE = {
"id": "550e8400-e29b-41d4-a716-446655440000",
@@ -801,6 +864,24 @@
"created_at": "2024-01-01T10:30:00Z",
}
+SAMPLE_ESTIMATE = {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "Estimate 1",
+ "description": "Estimate 1 description",
+ "type": "categories",
+ "last_used": False,
+ "created_at": "2024-01-01T10:30:00Z",
+}
+
+SAMPLE_ESTIMATE_POINT = {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "estimate": "550e8400-e29b-41d4-a716-446655440001",
+ "key": 1,
+ "value": "1",
+ "description": "Estimate Point 1 description",
+ "created_at": "2024-01-01T10:30:00Z",
+}
+
# Mapping of schema types to sample data
SCHEMA_EXAMPLES = {
"Issue": SAMPLE_ISSUE,
@@ -816,6 +897,8 @@
"Intake": SAMPLE_INTAKE,
"CycleIssue": SAMPLE_CYCLE_ISSUE,
"Sticky": SAMPLE_STICKY,
+ "Estimate": SAMPLE_ESTIMATE,
+ "EstimatePoint": SAMPLE_ESTIMATE_POINT,
}
diff --git a/apps/api/plane/utils/openapi/parameters.py b/apps/api/plane/utils/openapi/parameters.py
index d0ceba6c526..2812892ec91 100644
--- a/apps/api/plane/utils/openapi/parameters.py
+++ b/apps/api/plane/utils/openapi/parameters.py
@@ -495,3 +495,11 @@
),
],
)
+
+ESTIMATE_ID_PARAMETER = OpenApiParameter(
+ name="estimate_id",
+ description="Estimate ID",
+ required=True,
+ type=OpenApiTypes.UUID,
+ location=OpenApiParameter.PATH,
+)
diff --git a/apps/api/requirements/base.txt b/apps/api/requirements/base.txt
index 02c509ea14d..7778a71a353 100644
--- a/apps/api/requirements/base.txt
+++ b/apps/api/requirements/base.txt
@@ -1,7 +1,7 @@
# base requirements
# django
-Django==4.2.29
+Django==4.2.30
# rest framework
djangorestframework==3.15.2
# postgres
@@ -51,7 +51,7 @@ beautifulsoup4==4.12.3
# analytics
posthog==3.5.0
# crypto
-cryptography==46.0.5
+cryptography==46.0.7
# html validator
lxml==6.0.0
# s3
@@ -61,7 +61,7 @@ zxcvbn==4.4.28
# timezone
pytz==2024.1
# jwt
-PyJWT==2.8.0
+PyJWT==2.12.0
# OpenTelemetry
opentelemetry-api==1.28.1
opentelemetry-sdk==1.28.1
diff --git a/apps/api/requirements/test.txt b/apps/api/requirements/test.txt
index 7c242c806ab..bed38224a2f 100644
--- a/apps/api/requirements/test.txt
+++ b/apps/api/requirements/test.txt
@@ -1,6 +1,6 @@
-r base.txt
# test framework
-pytest==9.0.2
+pytest==9.0.3
pytest-django==4.5.2
pytest-cov==4.1.0
pytest-xdist==3.3.1
@@ -9,4 +9,4 @@ factory-boy==3.3.0
freezegun==1.2.2
coverage==7.2.7
httpx==0.24.1
-requests==2.32.4
\ No newline at end of file
+requests==2.33.0
\ No newline at end of file
diff --git a/apps/api/templates/emails/invitations/project_invitation.html b/apps/api/templates/emails/invitations/project_invitation.html
index 36aecd60d82..f18579c6459 100644
--- a/apps/api/templates/emails/invitations/project_invitation.html
+++ b/apps/api/templates/emails/invitations/project_invitation.html
@@ -261,7 +261,7 @@
|
- |
+ |
|
diff --git a/apps/api/templates/emails/notifications/issue-updates.html b/apps/api/templates/emails/notifications/issue-updates.html
index c6fe3b2786c..d82614ded61 100644
--- a/apps/api/templates/emails/notifications/issue-updates.html
+++ b/apps/api/templates/emails/notifications/issue-updates.html
@@ -233,7 +233,7 @@
|
|
diff --git a/apps/api/templates/emails/notifications/webhook-deactivate.html b/apps/api/templates/emails/notifications/webhook-deactivate.html
index 44aca67203a..c094eaa7636 100644
--- a/apps/api/templates/emails/notifications/webhook-deactivate.html
+++ b/apps/api/templates/emails/notifications/webhook-deactivate.html
@@ -155,7 +155,7 @@
|
- Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every message, tweet, and conversation and update our public roadmap.
+ Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every message, tweet, and conversation and update our public roadmap.
|
|
@@ -220,7 +220,7 @@
- |
+ |
|
diff --git a/apps/api/templates/emails/user/email_updated.html b/apps/api/templates/emails/user/email_updated.html
index cd13347ead2..cb1cad45966 100644
--- a/apps/api/templates/emails/user/email_updated.html
+++ b/apps/api/templates/emails/user/email_updated.html
@@ -831,7 +831,7 @@
"
>
,
,
;
}
diff --git a/apps/space/hooks/use-editor-flagging.ts b/apps/space/hooks/use-editor-flagging.ts
index f796845170d..88e82f76979 100644
--- a/apps/space/hooks/use-editor-flagging.ts
+++ b/apps/space/hooks/use-editor-flagging.ts
@@ -25,7 +25,7 @@ export type TEditorFlaggingHookReturnType = {
/**
* @description extensions disabled in various editors
*/
-export const useEditorFlagging = (anchor: string): TEditorFlaggingHookReturnType => ({
+export const useEditorFlagging = (_anchor: string): TEditorFlaggingHookReturnType => ({
document: {
disabled: [],
flagged: [],
diff --git a/apps/space/package.json b/apps/space/package.json
index ac3f7acae58..4500152f7d1 100644
--- a/apps/space/package.json
+++ b/apps/space/package.json
@@ -1,6 +1,6 @@
{
"name": "space",
- "version": "1.2.0",
+ "version": "1.3.0",
"private": true,
"license": "AGPL-3.0",
"type": "module",
@@ -53,7 +53,6 @@
"uuid": "catalog:"
},
"devDependencies": {
- "@dotenvx/dotenvx": "catalog:",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@react-router/dev": "catalog:",
@@ -62,6 +61,7 @@
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
+ "dotenv": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/apps/space/store/cycle.store.ts b/apps/space/store/cycle.store.ts
index 41ef8d0d436..13cb6950a5b 100644
--- a/apps/space/store/cycle.store.ts
+++ b/apps/space/store/cycle.store.ts
@@ -9,7 +9,7 @@ import { action, makeObservable, observable, runInAction } from "mobx";
import { SitesCycleService } from "@plane/services";
import type { TPublicCycle } from "@/types/cycle";
// store
-import type { CoreRootStore } from "./root.store";
+import type { RootStore } from "./root.store";
export interface ICycleStore {
// observables
@@ -23,9 +23,9 @@ export interface ICycleStore {
export class CycleStore implements ICycleStore {
cycles: TPublicCycle[] | undefined = undefined;
cycleService: SitesCycleService;
- rootStore: CoreRootStore;
+ rootStore: RootStore;
- constructor(_rootStore: CoreRootStore) {
+ constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
cycles: observable,
diff --git a/apps/space/store/helpers/base-issues.store.ts b/apps/space/store/helpers/base-issues.store.ts
index b1c342308e8..e0fdb564c60 100644
--- a/apps/space/store/helpers/base-issues.store.ts
+++ b/apps/space/store/helpers/base-issues.store.ts
@@ -23,7 +23,7 @@ import type {
} from "@plane/types";
// types
import type { IIssue, TIssuesResponse } from "@/types/issue";
-import type { CoreRootStore } from "../root.store";
+import type { RootStore } from "../root.store";
// constants
// helpers
@@ -81,7 +81,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
// root store
rootIssueStore;
- constructor(_rootStore: CoreRootStore) {
+ constructor(_rootStore: RootStore) {
makeObservable(this, {
// observable
loader: observable,
diff --git a/apps/space/store/instance.store.ts b/apps/space/store/instance.store.ts
index 3672c5f0081..37dc9318eac 100644
--- a/apps/space/store/instance.store.ts
+++ b/apps/space/store/instance.store.ts
@@ -10,7 +10,7 @@ import { observable, action, makeObservable, runInAction } from "mobx";
import { InstanceService } from "@plane/services";
import type { IInstance, IInstanceConfig } from "@plane/types";
// store
-import type { CoreRootStore } from "@/store/root.store";
+import type { RootStore } from "@/store/root.store";
type TError = {
status: string;
@@ -40,7 +40,7 @@ export class InstanceStore implements IInstanceStore {
// services
instanceService;
- constructor(private store: CoreRootStore) {
+ constructor(private store: RootStore) {
makeObservable(this, {
// observable
isLoading: observable.ref,
diff --git a/apps/space/store/issue-detail.store.ts b/apps/space/store/issue-detail.store.ts
index 0840e70c00a..6cbb6c867b3 100644
--- a/apps/space/store/issue-detail.store.ts
+++ b/apps/space/store/issue-detail.store.ts
@@ -13,7 +13,7 @@ import { SitesFileService, SitesIssueService } from "@plane/services";
import type { TFileSignedURLResponse, TIssuePublicComment } from "@plane/types";
import { EFileAssetType } from "@plane/types";
// store
-import type { CoreRootStore } from "@/store/root.store";
+import type { RootStore } from "@/store/root.store";
// types
import type { IIssue, IPeekMode, IVote } from "@/types/issue";
@@ -60,12 +60,12 @@ export class IssueDetailStore implements IIssueDetailStore {
[key: string]: IIssue;
} = {};
// root store
- rootStore: CoreRootStore;
+ rootStore: RootStore;
// services
issueService: SitesIssueService;
fileService: SitesFileService;
- constructor(_rootStore: CoreRootStore) {
+ constructor(_rootStore: RootStore) {
makeObservable(this, {
loader: observable.ref,
error: observable.ref,
diff --git a/apps/space/store/issue-filters.store.ts b/apps/space/store/issue-filters.store.ts
index 1632a0b3ea2..e2cb514dead 100644
--- a/apps/space/store/issue-filters.store.ts
+++ b/apps/space/store/issue-filters.store.ts
@@ -11,7 +11,7 @@ import { computedFn } from "mobx-utils";
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@plane/constants";
import type { IssuePaginationOptions, TIssueParams } from "@plane/types";
// store
-import type { CoreRootStore } from "@/store/root.store";
+import type { RootStore } from "@/store/root.store";
// types
import type {
TIssueLayoutOptions,
@@ -60,7 +60,7 @@ export class IssueFilterStore implements IIssueFilterStore {
};
filters: { [anchor: string]: TIssueFilters } | undefined = undefined;
- constructor(private store: CoreRootStore) {
+ constructor(private store: RootStore) {
makeObservable(this, {
// observables
layoutOptions: observable,
diff --git a/apps/space/store/issue.store.ts b/apps/space/store/issue.store.ts
index 27ed2ad8951..93556bc8068 100644
--- a/apps/space/store/issue.store.ts
+++ b/apps/space/store/issue.store.ts
@@ -9,7 +9,7 @@ import { action, makeObservable, runInAction } from "mobx";
import { SitesIssueService } from "@plane/services";
import type { IssuePaginationOptions, TLoader } from "@plane/types";
// store
-import type { CoreRootStore } from "@/store/root.store";
+import type { RootStore } from "@/store/root.store";
// types
import { BaseIssuesStore } from "./helpers/base-issues.store";
import type { IBaseIssuesStore } from "./helpers/base-issues.store";
@@ -28,11 +28,11 @@ export interface IIssueStore extends IBaseIssuesStore {
export class IssueStore extends BaseIssuesStore implements IIssueStore {
// root store
- rootStore: CoreRootStore;
+ rootStore: RootStore;
// services
issueService: SitesIssueService;
- constructor(_rootStore: CoreRootStore) {
+ constructor(_rootStore: RootStore) {
super(_rootStore);
makeObservable(this, {
// actions
diff --git a/apps/space/store/label.store.ts b/apps/space/store/label.store.ts
index b720816cf13..b1c229f0ae9 100644
--- a/apps/space/store/label.store.ts
+++ b/apps/space/store/label.store.ts
@@ -10,7 +10,7 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
import { SitesLabelService } from "@plane/services";
import type { IIssueLabel } from "@plane/types";
// store
-import type { CoreRootStore } from "./root.store";
+import type { RootStore } from "./root.store";
export interface IIssueLabelStore {
// observables
@@ -25,9 +25,9 @@ export interface IIssueLabelStore {
export class LabelStore implements IIssueLabelStore {
labelMap: Record = {};
labelService: SitesLabelService;
- rootStore: CoreRootStore;
+ rootStore: RootStore;
- constructor(_rootStore: CoreRootStore) {
+ constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
labelMap: observable,
diff --git a/apps/space/store/members.store.ts b/apps/space/store/members.store.ts
index 5c429b0037e..a06d0055049 100644
--- a/apps/space/store/members.store.ts
+++ b/apps/space/store/members.store.ts
@@ -9,7 +9,7 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
// plane imports
import { SitesMemberService } from "@plane/services";
import type { TPublicMember } from "@/types/member";
-import type { CoreRootStore } from "./root.store";
+import type { RootStore } from "./root.store";
export interface IIssueMemberStore {
// observables
@@ -24,9 +24,9 @@ export interface IIssueMemberStore {
export class MemberStore implements IIssueMemberStore {
memberMap: Record = {};
memberService: SitesMemberService;
- rootStore: CoreRootStore;
+ rootStore: RootStore;
- constructor(_rootStore: CoreRootStore) {
+ constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
memberMap: observable,
diff --git a/apps/space/store/module.store.ts b/apps/space/store/module.store.ts
index 1f2dd2d54a6..c539846efc0 100644
--- a/apps/space/store/module.store.ts
+++ b/apps/space/store/module.store.ts
@@ -11,7 +11,7 @@ import { SitesModuleService } from "@plane/services";
// types
import type { TPublicModule } from "@/types/modules";
// root store
-import type { CoreRootStore } from "./root.store";
+import type { RootStore } from "./root.store";
export interface IIssueModuleStore {
// observables
@@ -26,9 +26,9 @@ export interface IIssueModuleStore {
export class ModuleStore implements IIssueModuleStore {
moduleMap: Record = {};
moduleService: SitesModuleService;
- rootStore: CoreRootStore;
+ rootStore: RootStore;
- constructor(_rootStore: CoreRootStore) {
+ constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
moduleMap: observable,
diff --git a/apps/space/store/profile.store.ts b/apps/space/store/profile.store.ts
index 0dee248dda5..d62d1d37a38 100644
--- a/apps/space/store/profile.store.ts
+++ b/apps/space/store/profile.store.ts
@@ -11,7 +11,7 @@ import { UserService } from "@plane/services";
import type { TUserProfile } from "@plane/types";
import { EStartOfTheWeek } from "@plane/types";
// store
-import type { CoreRootStore } from "@/store/root.store";
+import type { RootStore } from "@/store/root.store";
type TError = {
status: string;
@@ -64,7 +64,7 @@ export class ProfileStore implements IProfileStore {
// services
userService: UserService;
- constructor(public store: CoreRootStore) {
+ constructor(public store: RootStore) {
makeObservable(this, {
// observables
isLoading: observable.ref,
diff --git a/apps/space/store/publish/publish.store.ts b/apps/space/store/publish/publish.store.ts
index b59ff2be230..973ea86e589 100644
--- a/apps/space/store/publish/publish.store.ts
+++ b/apps/space/store/publish/publish.store.ts
@@ -14,7 +14,7 @@ import type {
TProjectPublishViewProps,
} from "@plane/types";
// store
-import type { CoreRootStore } from "../root.store";
+import type { RootStore } from "../root.store";
export interface IPublishStore extends TProjectPublishSettings {
// computed
@@ -45,7 +45,7 @@ export class PublishStore implements IPublishStore {
workspace_detail: IWorkspaceLite | undefined;
constructor(
- private store: CoreRootStore,
+ private store: RootStore,
publishSettings: TProjectPublishSettings
) {
this.anchor = publishSettings.anchor;
diff --git a/apps/space/store/publish/publish_list.store.ts b/apps/space/store/publish/publish_list.store.ts
index 29a09dca17e..eeebe97e70a 100644
--- a/apps/space/store/publish/publish_list.store.ts
+++ b/apps/space/store/publish/publish_list.store.ts
@@ -11,7 +11,7 @@ import { SitesProjectPublishService } from "@plane/services";
import type { TProjectPublishSettings } from "@plane/types";
// store
import { PublishStore } from "@/store/publish/publish.store";
-import type { CoreRootStore } from "@/store/root.store";
+import type { RootStore } from "@/store/root.store";
export interface IPublishListStore {
// observables
@@ -26,7 +26,7 @@ export class PublishListStore implements IPublishListStore {
// service
publishService;
- constructor(private rootStore: CoreRootStore) {
+ constructor(private rootStore: RootStore) {
makeObservable(this, {
// observables
publishMap: observable,
diff --git a/apps/space/store/state.store.ts b/apps/space/store/state.store.ts
index f900c5105f7..3ecf78d1592 100644
--- a/apps/space/store/state.store.ts
+++ b/apps/space/store/state.store.ts
@@ -12,7 +12,7 @@ import type { IState } from "@plane/types";
// helpers
import { sortStates } from "@/helpers/state.helper";
// store
-import type { CoreRootStore } from "./root.store";
+import type { RootStore } from "./root.store";
export interface IStateStore {
// observables
@@ -28,9 +28,9 @@ export interface IStateStore {
export class StateStore implements IStateStore {
states: IState[] | undefined = undefined;
stateService: SitesStateService;
- rootStore: CoreRootStore;
+ rootStore: RootStore;
- constructor(_rootStore: CoreRootStore) {
+ constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
states: observable,
diff --git a/apps/space/store/user.store.ts b/apps/space/store/user.store.ts
index fac64281ead..f2a3c7dbc20 100644
--- a/apps/space/store/user.store.ts
+++ b/apps/space/store/user.store.ts
@@ -14,7 +14,7 @@ import type { ActorDetail, IUser } from "@plane/types";
import type { IProfileStore } from "@/store/profile.store";
import { ProfileStore } from "@/store/profile.store";
// store
-import type { CoreRootStore } from "@/store/root.store";
+import type { RootStore } from "@/store/root.store";
type TUserErrorStatus = {
status: string;
@@ -50,7 +50,7 @@ export class UserStore implements IUserStore {
// service
userService: UserService;
- constructor(private store: CoreRootStore) {
+ constructor(private store: RootStore) {
// stores
this.profile = new ProfileStore(store);
// service
diff --git a/apps/space/vite.config.ts b/apps/space/vite.config.ts
index 5368af0761c..1767562cec3 100644
--- a/apps/space/vite.config.ts
+++ b/apps/space/vite.config.ts
@@ -1,5 +1,5 @@
import path from "node:path";
-import * as dotenv from "@dotenvx/dotenvx";
+import * as dotenv from "dotenv";
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
diff --git a/apps/web/Dockerfile.web b/apps/web/Dockerfile.web
index ab1a4046296..38af19e74ba 100644
--- a/apps/web/Dockerfile.web
+++ b/apps/web/Dockerfile.web
@@ -14,7 +14,7 @@ RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
-ARG TURBO_VERSION=2.8.12
+ARG TURBO_VERSION=2.9.4
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
COPY . .
diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/_sidebar.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/_sidebar.tsx
index bbbea873523..173b1fb0e09 100644
--- a/apps/web/app/(all)/[workspaceSlug]/(projects)/_sidebar.tsx
+++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/_sidebar.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
// plane imports
diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx
index 62c1c162154..296b780c91e 100644
--- a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx
+++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx
@@ -5,7 +5,6 @@
*/
// ui
-import type { FC } from "react";
import { observer } from "mobx-react";
import { useParams, useRouter } from "next/navigation";
import { PanelRight } from "lucide-react";
@@ -14,7 +13,6 @@ import { useTranslation } from "@plane/i18n";
import { YourWorkIcon, ChevronDownIcon } from "@plane/propel/icons";
import type { IUserProfileProjectSegregation } from "@plane/types";
import { Breadcrumbs, Header, CustomMenu } from "@plane/ui";
-import { cn } from "@plane/utils";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { ProfileIssuesFilter } from "@/components/profile/profile-issues-filter";
diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx
index 26c4734c734..c529c4efe9f 100644
--- a/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx
+++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { isEmpty } from "lodash-es";
import { observer } from "mobx-react";
// plane helpers
diff --git a/apps/web/app/(all)/invitations/page.tsx b/apps/web/app/(all)/invitations/page.tsx
index 6921f5b841a..dc1e45cc2d7 100644
--- a/apps/web/app/(all)/invitations/page.tsx
+++ b/apps/web/app/(all)/invitations/page.tsx
@@ -81,7 +81,6 @@ function UserInvitationsPage() {
.then(() => {
mutate(USER_WORKSPACES_LIST);
const firstInviteId = invitationsRespond[0];
- const invitation = invitations?.find((i) => i.id === firstInviteId);
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
updateUserProfile({ last_workspace_id: redirectWorkspace?.id })
.then(() => {
diff --git a/apps/web/app/assets/auth/gradient-bg-logo.webp b/apps/web/app/assets/auth/gradient-bg-logo.webp
new file mode 100644
index 00000000000..47202e562cb
Binary files /dev/null and b/apps/web/app/assets/auth/gradient-bg-logo.webp differ
diff --git a/apps/web/app/assets/auth/gradient-logo.webp b/apps/web/app/assets/auth/gradient-logo.webp
new file mode 100644
index 00000000000..674434ceef7
Binary files /dev/null and b/apps/web/app/assets/auth/gradient-logo.webp differ
diff --git a/apps/web/app/provider.tsx b/apps/web/app/provider.tsx
index a8a5d27188e..016b66c0e4b 100644
--- a/apps/web/app/provider.tsx
+++ b/apps/web/app/provider.tsx
@@ -13,8 +13,6 @@ import { TranslationProvider } from "@plane/i18n";
import { Toast } from "@plane/propel/toast";
// helpers
import { resolveGeneralTheme } from "@plane/utils";
-// polyfills
-import "@/lib/polyfills";
// mobx store provider
import { StoreProvider } from "@/lib/store-context";
@@ -31,10 +29,6 @@ const InstanceWrapper = lazy(function InstanceWrapper() {
return import("@/lib/wrappers/instance-wrapper");
});
-const ChatSupportModal = lazy(function ChatSupportModal() {
- return import("@/components/global/chat-support-modal");
-});
-
export interface IAppProvider {
children: React.ReactNode;
}
@@ -53,7 +47,6 @@ export function AppProvider(props: IAppProvider) {
-
{children}
diff --git a/apps/web/ce/components/analytics/use-analytics-tabs.tsx b/apps/web/ce/components/analytics/use-analytics-tabs.tsx
index 03877975127..d022d74f0ff 100644
--- a/apps/web/ce/components/analytics/use-analytics-tabs.tsx
+++ b/apps/web/ce/components/analytics/use-analytics-tabs.tsx
@@ -8,7 +8,7 @@ import { useMemo } from "react";
import { useTranslation } from "@plane/i18n";
import { getAnalyticsTabs } from "./tabs";
-export const useAnalyticsTabs = (workspaceSlug: string) => {
+export const useAnalyticsTabs = (_workspaceSlug: string) => {
const { t } = useTranslation();
const analyticsTabs = useMemo(() => getAnalyticsTabs(t), [t]);
diff --git a/apps/web/ce/components/automations/root.tsx b/apps/web/ce/components/automations/root.tsx
index f36f36d3ce1..1baeeb8b0b2 100644
--- a/apps/web/ce/components/automations/root.tsx
+++ b/apps/web/ce/components/automations/root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
export type TCustomAutomationsRootProps = {
diff --git a/apps/web/ce/components/command-palette/modals/work-item-level.tsx b/apps/web/ce/components/command-palette/modals/work-item-level.tsx
index cb9d8dc228c..fc83af463a3 100644
--- a/apps/web/ce/components/command-palette/modals/work-item-level.tsx
+++ b/apps/web/ce/components/command-palette/modals/work-item-level.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
diff --git a/apps/web/ce/components/common/subscription/subscription-pill.tsx b/apps/web/ce/components/common/subscription/subscription-pill.tsx
index f286b870a35..89efebe83be 100644
--- a/apps/web/ce/components/common/subscription/subscription-pill.tsx
+++ b/apps/web/ce/components/common/subscription/subscription-pill.tsx
@@ -10,6 +10,6 @@ type TProps = {
workspace?: IWorkspace;
};
-export function SubscriptionPill(props: TProps) {
+export function SubscriptionPill(_props: TProps) {
return <>>;
}
diff --git a/apps/web/ce/components/cycles/active-cycle/root.tsx b/apps/web/ce/components/cycles/active-cycle/root.tsx
index 7c15122e1e1..ab413b8b0a4 100644
--- a/apps/web/ce/components/cycles/active-cycle/root.tsx
+++ b/apps/web/ce/components/cycles/active-cycle/root.tsx
@@ -46,7 +46,7 @@ type ActiveCyclesComponentProps = {
const ActiveCyclesComponent = observer(function ActiveCyclesComponent({
cycleId,
activeCycle,
- activeCycleResolvedPath,
+ activeCycleResolvedPath: _activeCycleResolvedPath,
workspaceSlug,
projectId,
handleFiltersUpdate,
diff --git a/apps/web/ce/components/cycles/additional-actions.tsx b/apps/web/ce/components/cycles/additional-actions.tsx
index 843e9e51e9e..45bc7b5cd58 100644
--- a/apps/web/ce/components/cycles/additional-actions.tsx
+++ b/apps/web/ce/components/cycles/additional-actions.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
type Props = {
cycleId: string;
diff --git a/apps/web/ce/components/cycles/analytics-sidebar/root.tsx b/apps/web/ce/components/cycles/analytics-sidebar/root.tsx
index ce0865c4d4e..17501e01d8a 100644
--- a/apps/web/ce/components/cycles/analytics-sidebar/root.tsx
+++ b/apps/web/ce/components/cycles/analytics-sidebar/root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
// components
import { SidebarChart } from "./base";
diff --git a/apps/web/ce/components/de-dupe/de-dupe-button.tsx b/apps/web/ce/components/de-dupe/de-dupe-button.tsx
index 2060f155661..cfeeeb82ad7 100644
--- a/apps/web/ce/components/de-dupe/de-dupe-button.tsx
+++ b/apps/web/ce/components/de-dupe/de-dupe-button.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
// local components
@@ -15,7 +14,6 @@ type TDeDupeButtonRoot = {
label: string;
};
-export function DeDupeButtonRoot(props: TDeDupeButtonRoot) {
- const { workspaceSlug, isDuplicateModalOpen, label, handleOnClick } = props;
+export function DeDupeButtonRoot(_props: TDeDupeButtonRoot) {
return <>>;
}
diff --git a/apps/web/ce/components/de-dupe/duplicate-modal/root.tsx b/apps/web/ce/components/de-dupe/duplicate-modal/root.tsx
index 60e3ec924c4..4afbdeca089 100644
--- a/apps/web/ce/components/de-dupe/duplicate-modal/root.tsx
+++ b/apps/web/ce/components/de-dupe/duplicate-modal/root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
// types
import type { TDeDupeIssue } from "@plane/types";
@@ -14,7 +13,6 @@ type TDuplicateModalRootProps = {
handleDuplicateIssueModal: (value: boolean) => void;
};
-export function DuplicateModalRoot(props: TDuplicateModalRootProps) {
- const { workspaceSlug, issues, handleDuplicateIssueModal } = props;
+export function DuplicateModalRoot(_props: TDuplicateModalRootProps) {
return <>>;
}
diff --git a/apps/web/ce/components/de-dupe/duplicate-popover/root.tsx b/apps/web/ce/components/de-dupe/duplicate-popover/root.tsx
index 51dfff897f2..146d6b4bc88 100644
--- a/apps/web/ce/components/de-dupe/duplicate-popover/root.tsx
+++ b/apps/web/ce/components/de-dupe/duplicate-popover/root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
import { observer } from "mobx-react";
// types
diff --git a/apps/web/ce/components/de-dupe/issue-block/button-label.tsx b/apps/web/ce/components/de-dupe/issue-block/button-label.tsx
index 461187bd099..ae89c7797fb 100644
--- a/apps/web/ce/components/de-dupe/issue-block/button-label.tsx
+++ b/apps/web/ce/components/de-dupe/issue-block/button-label.tsx
@@ -4,14 +4,11 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
-
type TDeDupeIssueButtonLabelProps = {
isOpen: boolean;
buttonLabel: string;
};
-export function DeDupeIssueButtonLabel(props: TDeDupeIssueButtonLabelProps) {
- const { isOpen, buttonLabel } = props;
+export function DeDupeIssueButtonLabel(_props: TDeDupeIssueButtonLabelProps) {
return <>>;
}
diff --git a/apps/web/ce/components/epics/epic-modal/modal.tsx b/apps/web/ce/components/epics/epic-modal/modal.tsx
index 14509d580d8..90d7c9fa34b 100644
--- a/apps/web/ce/components/epics/epic-modal/modal.tsx
+++ b/apps/web/ce/components/epics/epic-modal/modal.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
import type { TIssue } from "@plane/types";
@@ -22,6 +21,6 @@ export interface EpicModalProps {
isProjectSelectionDisabled?: boolean;
}
-export function CreateUpdateEpicModal(props: EpicModalProps) {
+export function CreateUpdateEpicModal(_props: EpicModalProps) {
return <>>;
}
diff --git a/apps/web/ce/components/estimates/inputs/time-input.tsx b/apps/web/ce/components/estimates/inputs/time-input.tsx
index f210f0083ac..6eadf418380 100644
--- a/apps/web/ce/components/estimates/inputs/time-input.tsx
+++ b/apps/web/ce/components/estimates/inputs/time-input.tsx
@@ -4,8 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
-
export type TEstimateTimeInputProps = {
value?: number;
handleEstimateInputValue: (value: string) => void;
diff --git a/apps/web/ce/components/estimates/points/delete.tsx b/apps/web/ce/components/estimates/points/delete.tsx
index f5969a9392a..c64ee12c1ae 100644
--- a/apps/web/ce/components/estimates/points/delete.tsx
+++ b/apps/web/ce/components/estimates/points/delete.tsx
@@ -4,8 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
-
import type { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@plane/types";
export type TEstimatePointDelete = {
diff --git a/apps/web/ce/components/estimates/update/modal.tsx b/apps/web/ce/components/estimates/update/modal.tsx
index cfb67209771..e0600f32d39 100644
--- a/apps/web/ce/components/estimates/update/modal.tsx
+++ b/apps/web/ce/components/estimates/update/modal.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
type TUpdateEstimateModal = {
diff --git a/apps/web/ce/components/gantt-chart/blocks/blocks-list.tsx b/apps/web/ce/components/gantt-chart/blocks/blocks-list.tsx
index 2c252ded637..fcb9d0936bd 100644
--- a/apps/web/ce/components/gantt-chart/blocks/blocks-list.tsx
+++ b/apps/web/ce/components/gantt-chart/blocks/blocks-list.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
//
import type { IBlockUpdateDependencyData } from "@plane/types";
import { GanttChartBlock } from "@/components/gantt-chart/blocks/block";
diff --git a/apps/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx b/apps/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx
index 252e3317605..34a8aac7d4a 100644
--- a/apps/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx
+++ b/apps/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx
@@ -12,6 +12,6 @@ type LeftDependencyDraggableProps = {
ganttContainerRef: RefObject;
};
-export function LeftDependencyDraggable(props: LeftDependencyDraggableProps) {
+export function LeftDependencyDraggable(_props: LeftDependencyDraggableProps) {
return <>>;
}
diff --git a/apps/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx b/apps/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx
index 059702dd00c..d6badd067db 100644
--- a/apps/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx
+++ b/apps/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx
@@ -11,6 +11,6 @@ type RightDependencyDraggableProps = {
block: IGanttBlock;
ganttContainerRef: RefObject;
};
-export function RightDependencyDraggable(props: RightDependencyDraggableProps) {
+export function RightDependencyDraggable(_props: RightDependencyDraggableProps) {
return <>>;
}
diff --git a/apps/web/ce/components/gantt-chart/dependency/dependency-paths.tsx b/apps/web/ce/components/gantt-chart/dependency/dependency-paths.tsx
index 92c531cfef0..6332a71cb25 100644
--- a/apps/web/ce/components/gantt-chart/dependency/dependency-paths.tsx
+++ b/apps/web/ce/components/gantt-chart/dependency/dependency-paths.tsx
@@ -4,12 +4,9 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
-
type Props = {
isEpic?: boolean;
};
-export function TimelineDependencyPaths(props: Props) {
- const { isEpic = false } = props;
+export function TimelineDependencyPaths(_props: Props) {
return <>>;
}
diff --git a/apps/web/ce/components/inbox/source-pill.tsx b/apps/web/ce/components/inbox/source-pill.tsx
index a9f23030883..6d7763f8ba9 100644
--- a/apps/web/ce/components/inbox/source-pill.tsx
+++ b/apps/web/ce/components/inbox/source-pill.tsx
@@ -10,6 +10,6 @@ export type TInboxSourcePill = {
source: EInboxIssueSource;
};
-export function InboxSourcePill(props: TInboxSourcePill) {
+export function InboxSourcePill(_props: TInboxSourcePill) {
return <>>;
}
diff --git a/apps/web/ce/components/issues/filters/issue-types.tsx b/apps/web/ce/components/issues/filters/issue-types.tsx
index 8c9bc6f0603..db7b06f77f0 100644
--- a/apps/web/ce/components/issues/filters/issue-types.tsx
+++ b/apps/web/ce/components/issues/filters/issue-types.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type React from "react";
import { observer } from "mobx-react";
type Props = {
diff --git a/apps/web/ce/components/issues/filters/team-project.tsx b/apps/web/ce/components/issues/filters/team-project.tsx
index e231da3d15b..f838f4c9481 100644
--- a/apps/web/ce/components/issues/filters/team-project.tsx
+++ b/apps/web/ce/components/issues/filters/team-project.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type React from "react";
import { observer } from "mobx-react";
type Props = {
diff --git a/apps/web/ce/components/issues/issue-detail-widgets/action-buttons.tsx b/apps/web/ce/components/issues/issue-detail-widgets/action-buttons.tsx
index d1e200e2224..4c17d1be123 100644
--- a/apps/web/ce/components/issues/issue-detail-widgets/action-buttons.tsx
+++ b/apps/web/ce/components/issues/issue-detail-widgets/action-buttons.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
// plane types
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
diff --git a/apps/web/ce/components/issues/issue-detail-widgets/collapsibles.tsx b/apps/web/ce/components/issues/issue-detail-widgets/collapsibles.tsx
index 02cf4611998..626107cdb2b 100644
--- a/apps/web/ce/components/issues/issue-detail-widgets/collapsibles.tsx
+++ b/apps/web/ce/components/issues/issue-detail-widgets/collapsibles.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
// plane types
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
diff --git a/apps/web/ce/components/issues/issue-detail-widgets/modals.tsx b/apps/web/ce/components/issues/issue-detail-widgets/modals.tsx
index 8be9058da3d..9356b772706 100644
--- a/apps/web/ce/components/issues/issue-detail-widgets/modals.tsx
+++ b/apps/web/ce/components/issues/issue-detail-widgets/modals.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
// plane types
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
diff --git a/apps/web/ce/components/issues/issue-details/additional-activity-root.tsx b/apps/web/ce/components/issues/issue-details/additional-activity-root.tsx
index 345811e553d..95804501abb 100644
--- a/apps/web/ce/components/issues/issue-details/additional-activity-root.tsx
+++ b/apps/web/ce/components/issues/issue-details/additional-activity-root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
export type TAdditionalActivityRoot = {
diff --git a/apps/web/ce/components/issues/issue-details/additional-properties.tsx b/apps/web/ce/components/issues/issue-details/additional-properties.tsx
index 7f04dde779a..a169a8863a5 100644
--- a/apps/web/ce/components/issues/issue-details/additional-properties.tsx
+++ b/apps/web/ce/components/issues/issue-details/additional-properties.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
// plane imports
diff --git a/apps/web/ce/components/issues/issue-details/issue-creator.tsx b/apps/web/ce/components/issues/issue-details/issue-creator.tsx
index 2c25079ad04..56e8e74938d 100644
--- a/apps/web/ce/components/issues/issue-details/issue-creator.tsx
+++ b/apps/web/ce/components/issues/issue-details/issue-creator.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import Link from "next/link";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
diff --git a/apps/web/ce/components/issues/issue-details/issue-properties-activity/root.tsx b/apps/web/ce/components/issues/issue-details/issue-properties-activity/root.tsx
index db97243b13d..c6a50531ee7 100644
--- a/apps/web/ce/components/issues/issue-details/issue-properties-activity/root.tsx
+++ b/apps/web/ce/components/issues/issue-details/issue-properties-activity/root.tsx
@@ -4,8 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
-
type TIssueAdditionalPropertiesActivity = {
activityId: string;
ends: "top" | "bottom" | undefined;
diff --git a/apps/web/ce/components/issues/issue-details/issue-type-activity.tsx b/apps/web/ce/components/issues/issue-details/issue-type-activity.tsx
index d85155aaf1b..08b499f823b 100644
--- a/apps/web/ce/components/issues/issue-details/issue-type-activity.tsx
+++ b/apps/web/ce/components/issues/issue-details/issue-type-activity.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
export type TIssueTypeActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
diff --git a/apps/web/ce/components/issues/issue-details/parent-select-root.tsx b/apps/web/ce/components/issues/issue-details/parent-select-root.tsx
index 2a1d2b51d09..301b31a9e22 100644
--- a/apps/web/ce/components/issues/issue-details/parent-select-root.tsx
+++ b/apps/web/ce/components/issues/issue-details/parent-select-root.tsx
@@ -47,7 +47,7 @@ export const IssueParentSelectRoot = observer(function IssueParentSelectRoot(pro
await issueOperations.fetch(workspaceSlug, projectId, issueId, false);
if (_issueId) await fetchSubIssues(workspaceSlug, projectId, _issueId);
toggleParentIssueModal(null);
- } catch (error) {
+ } catch (_error) {
console.error("something went wrong while fetching the issue");
}
};
@@ -63,7 +63,7 @@ export const IssueParentSelectRoot = observer(function IssueParentSelectRoot(pro
await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
await fetchSubIssues(workspaceSlug, projectId, parentIssueId);
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
- } catch (error) {
+ } catch (_error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("common.error.label"),
diff --git a/apps/web/ce/components/issues/issue-layouts/additional-properties.tsx b/apps/web/ce/components/issues/issue-layouts/additional-properties.tsx
index 37a5505d447..35c62fed4ac 100644
--- a/apps/web/ce/components/issues/issue-layouts/additional-properties.tsx
+++ b/apps/web/ce/components/issues/issue-layouts/additional-properties.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
import type { IIssueDisplayProperties, TIssue } from "@plane/types";
@@ -13,6 +12,6 @@ export type TWorkItemLayoutAdditionalProperties = {
issue: TIssue;
};
-export function WorkItemLayoutAdditionalProperties(props: TWorkItemLayoutAdditionalProperties) {
+export function WorkItemLayoutAdditionalProperties(_props: TWorkItemLayoutAdditionalProperties) {
return <>>;
}
diff --git a/apps/web/ce/components/issues/issue-layouts/issue-stats.tsx b/apps/web/ce/components/issues/issue-layouts/issue-stats.tsx
index 4ff1eed5552..fc6fbc0a024 100644
--- a/apps/web/ce/components/issues/issue-layouts/issue-stats.tsx
+++ b/apps/web/ce/components/issues/issue-layouts/issue-stats.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
type Props = {
@@ -15,6 +14,6 @@ type Props = {
showLabel?: boolean;
};
-export function IssueStats(props: Props) {
+export function IssueStats(_props: Props) {
return <>>;
}
diff --git a/apps/web/ce/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal.tsx b/apps/web/ce/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal.tsx
index 4c09712dc4f..b8b142f7408 100644
--- a/apps/web/ce/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal.tsx
+++ b/apps/web/ce/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal.tsx
@@ -4,8 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
-
type TDuplicateWorkItemModalProps = {
workItemId: string;
onClose: () => void;
diff --git a/apps/web/ce/components/issues/issue-modal/modal-additional-properties.tsx b/apps/web/ce/components/issues/issue-modal/modal-additional-properties.tsx
index 4daec9f8b6c..0255407ae68 100644
--- a/apps/web/ce/components/issues/issue-modal/modal-additional-properties.tsx
+++ b/apps/web/ce/components/issues/issue-modal/modal-additional-properties.tsx
@@ -4,8 +4,6 @@
* See the LICENSE file for details.
*/
-import type React from "react";
-
export type TWorkItemModalAdditionalPropertiesProps = {
isDraft?: boolean;
projectId: string | null;
diff --git a/apps/web/ce/components/issues/worklog/activity/filter-root.tsx b/apps/web/ce/components/issues/worklog/activity/filter-root.tsx
index 2c40285fe00..623980ffe00 100644
--- a/apps/web/ce/components/issues/worklog/activity/filter-root.tsx
+++ b/apps/web/ce/components/issues/worklog/activity/filter-root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
// plane imports
import type { TActivityFilters, TActivityFilterOption } from "@plane/constants";
import { ACTIVITY_FILTER_TYPE_OPTIONS } from "@plane/constants";
diff --git a/apps/web/ce/components/issues/worklog/activity/root.tsx b/apps/web/ce/components/issues/worklog/activity/root.tsx
index d885232588d..3b3d33729b2 100644
--- a/apps/web/ce/components/issues/worklog/activity/root.tsx
+++ b/apps/web/ce/components/issues/worklog/activity/root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import type { TIssueActivityComment } from "@plane/types";
type TIssueActivityWorklog = {
diff --git a/apps/web/ce/components/issues/worklog/activity/worklog-create-button.tsx b/apps/web/ce/components/issues/worklog/activity/worklog-create-button.tsx
index f9e13ccdf10..5ded8d02bb2 100644
--- a/apps/web/ce/components/issues/worklog/activity/worklog-create-button.tsx
+++ b/apps/web/ce/components/issues/worklog/activity/worklog-create-button.tsx
@@ -4,8 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
-
type TIssueActivityWorklogCreateButton = {
workspaceSlug: string;
projectId: string;
diff --git a/apps/web/ce/components/issues/worklog/property/root.tsx b/apps/web/ce/components/issues/worklog/property/root.tsx
index 151be1460ed..7eff7d6a8a0 100644
--- a/apps/web/ce/components/issues/worklog/property/root.tsx
+++ b/apps/web/ce/components/issues/worklog/property/root.tsx
@@ -4,8 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
-
type TIssueWorklogProperty = {
workspaceSlug: string;
projectId: string;
diff --git a/apps/web/ce/components/onboarding/tour/root.tsx b/apps/web/ce/components/onboarding/tour/root.tsx
index 46100a8c74b..6f879e3922e 100644
--- a/apps/web/ce/components/onboarding/tour/root.tsx
+++ b/apps/web/ce/components/onboarding/tour/root.tsx
@@ -7,7 +7,6 @@
import { useState } from "react";
import { observer } from "mobx-react";
// plane imports
-import { PRODUCT_TOUR_TRACKER_ELEMENTS } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { CloseIcon, PlaneLockup } from "@plane/propel/icons";
// assets
diff --git a/apps/web/ce/components/pages/modals/modals.tsx b/apps/web/ce/components/pages/modals/modals.tsx
index 0b36ce33f2e..84952fbf400 100644
--- a/apps/web/ce/components/pages/modals/modals.tsx
+++ b/apps/web/ce/components/pages/modals/modals.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type React from "react";
import { observer } from "mobx-react";
// components
import type { EPageStoreType } from "@/plane-web/hooks/store";
@@ -16,6 +15,6 @@ export type TPageModalsProps = {
storeType: EPageStoreType;
};
-export const PageModals = observer(function PageModals(props: TPageModalsProps) {
+export const PageModals = observer(function PageModals(_props: TPageModalsProps) {
return null;
});
diff --git a/apps/web/ce/components/sidebar/project-navigation-root.tsx b/apps/web/ce/components/sidebar/project-navigation-root.tsx
index 51463851e30..fd5249c01fc 100644
--- a/apps/web/ce/components/sidebar/project-navigation-root.tsx
+++ b/apps/web/ce/components/sidebar/project-navigation-root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
// components
import { ProjectNavigation } from "@/components/workspace/sidebar/project-navigation";
diff --git a/apps/web/ce/components/views/helper.tsx b/apps/web/ce/components/views/helper.tsx
index e725632ec00..dda72e6ed04 100644
--- a/apps/web/ce/components/views/helper.tsx
+++ b/apps/web/ce/components/views/helper.tsx
@@ -13,11 +13,11 @@ export type TLayoutSelectionProps = {
workspaceSlug: string;
};
-export function GlobalViewLayoutSelection(props: TLayoutSelectionProps) {
+export function GlobalViewLayoutSelection(_props: TLayoutSelectionProps) {
return <>>;
}
-export function WorkspaceAdditionalLayouts(props: TWorkspaceLayoutProps) {
+export function WorkspaceAdditionalLayouts(_props: TWorkspaceLayoutProps) {
return <>>;
}
diff --git a/apps/web/ce/components/workspace-notifications/notification-card/root.tsx b/apps/web/ce/components/workspace-notifications/notification-card/root.tsx
index d6e9afafb2d..2dcc706bf85 100644
--- a/apps/web/ce/components/workspace-notifications/notification-card/root.tsx
+++ b/apps/web/ce/components/workspace-notifications/notification-card/root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
// plane imports
import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants";
diff --git a/apps/web/ce/components/workspace/sidebar/sidebar-item.tsx b/apps/web/ce/components/workspace/sidebar/sidebar-item.tsx
index a1fb30bfc4e..c6fbc8f9fec 100644
--- a/apps/web/ce/components/workspace/sidebar/sidebar-item.tsx
+++ b/apps/web/ce/components/workspace/sidebar/sidebar-item.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import type { IWorkspaceSidebarNavigationItem } from "@plane/constants";
import { SidebarItemBase } from "@/components/workspace/sidebar/sidebar-item";
diff --git a/apps/web/ce/components/workspace/upgrade-badge.tsx b/apps/web/ce/components/workspace/upgrade-badge.tsx
index 039ac280af2..5bddd913d92 100644
--- a/apps/web/ce/components/workspace/upgrade-badge.tsx
+++ b/apps/web/ce/components/workspace/upgrade-badge.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
// helpers
import { useTranslation } from "@plane/i18n";
import { cn } from "@plane/utils";
diff --git a/apps/web/ce/hooks/use-debounced-duplicate-issues.tsx b/apps/web/ce/hooks/use-debounced-duplicate-issues.tsx
index f84f4d2be79..32459909792 100644
--- a/apps/web/ce/hooks/use-debounced-duplicate-issues.tsx
+++ b/apps/web/ce/hooks/use-debounced-duplicate-issues.tsx
@@ -7,10 +7,10 @@
import type { TDeDupeIssue } from "@plane/types";
export const useDebouncedDuplicateIssues = (
- workspaceSlug: string | undefined,
- workspaceId: string | undefined,
- projectId: string | undefined,
- formData: { name: string | undefined; description_html?: string | undefined; issueId?: string | undefined }
+ _workspaceSlug: string | undefined,
+ _workspaceId: string | undefined,
+ _projectId: string | undefined,
+ _formData: { name: string | undefined; description_html?: string | undefined; issueId?: string | undefined }
) => {
const duplicateIssues: TDeDupeIssue[] = [];
diff --git a/apps/web/ce/hooks/use-issue-properties.tsx b/apps/web/ce/hooks/use-issue-properties.tsx
index 4eff976d404..310fd725e4c 100644
--- a/apps/web/ce/hooks/use-issue-properties.tsx
+++ b/apps/web/ce/hooks/use-issue-properties.tsx
@@ -10,7 +10,7 @@ export const useWorkItemProperties = (
projectId: string | null | undefined,
workspaceSlug: string | null | undefined,
workItemId: string | null | undefined,
- issueServiceType: TIssueServiceType
+ _issueServiceType: TIssueServiceType
) => {
if (!projectId || !workspaceSlug || !workItemId) return;
};
diff --git a/apps/web/ce/store/issue/helpers/base-issue-store.ts b/apps/web/ce/store/issue/helpers/base-issue-store.ts
index 59d6d7b6a0a..236235a8256 100644
--- a/apps/web/ce/store/issue/helpers/base-issue-store.ts
+++ b/apps/web/ce/store/issue/helpers/base-issue-store.ts
@@ -7,4 +7,4 @@
import type { TIssue } from "@plane/types";
import { getIssueIds } from "@/store/issue/helpers/base-issues-utils";
-export const workItemSortWithOrderByExtended = (array: TIssue[], key?: string) => getIssueIds(array);
+export const workItemSortWithOrderByExtended = (array: TIssue[], _key?: string) => getIssueIds(array);
diff --git a/apps/web/ce/store/issue/helpers/base-issue.store.ts b/apps/web/ce/store/issue/helpers/base-issue.store.ts
index 59d6d7b6a0a..236235a8256 100644
--- a/apps/web/ce/store/issue/helpers/base-issue.store.ts
+++ b/apps/web/ce/store/issue/helpers/base-issue.store.ts
@@ -7,4 +7,4 @@
import type { TIssue } from "@plane/types";
import { getIssueIds } from "@/store/issue/helpers/base-issues-utils";
-export const workItemSortWithOrderByExtended = (array: TIssue[], key?: string) => getIssueIds(array);
+export const workItemSortWithOrderByExtended = (array: TIssue[], _key?: string) => getIssueIds(array);
diff --git a/apps/web/ce/store/timeline/base-timeline.store.ts b/apps/web/ce/store/timeline/base-timeline.store.ts
index b1c70f99407..b97b2ff7e14 100644
--- a/apps/web/ce/store/timeline/base-timeline.store.ts
+++ b/apps/web/ce/store/timeline/base-timeline.store.ts
@@ -342,5 +342,5 @@ export class BaseTimeLineStore implements IBaseTimelineStore {
});
// Dummy method to return if the current Block's dependency is being dragged
- getIsCurrentDependencyDragging = computedFn((blockId: string) => false);
+ getIsCurrentDependencyDragging = computedFn((_blockId: string) => false);
}
diff --git a/apps/web/core/components/account/auth-forms/email.tsx b/apps/web/core/components/account/auth-forms/email.tsx
index 8abddb42efa..2138753aaf6 100644
--- a/apps/web/core/components/account/auth-forms/email.tsx
+++ b/apps/web/core/components/account/auth-forms/email.tsx
@@ -4,7 +4,7 @@
* See the LICENSE file for details.
*/
-import type { FC, FormEvent } from "react";
+import type { FormEvent } from "react";
import { useMemo, useRef, useState } from "react";
import { observer } from "mobx-react";
// icons
diff --git a/apps/web/core/components/account/auth-forms/password.tsx b/apps/web/core/components/account/auth-forms/password.tsx
index 6fed9e88893..8ebe3c827d3 100644
--- a/apps/web/core/components/account/auth-forms/password.tsx
+++ b/apps/web/core/components/account/auth-forms/password.tsx
@@ -10,7 +10,7 @@ import Link from "next/link";
// icons
import { Eye, EyeOff, Info, XCircle } from "lucide-react";
// plane imports
-import { API_BASE_URL, E_PASSWORD_STRENGTH, AUTH_TRACKER_EVENTS, AUTH_TRACKER_ELEMENTS } from "@plane/constants";
+import { API_BASE_URL, E_PASSWORD_STRENGTH, AUTH_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { CloseIcon } from "@plane/propel/icons";
diff --git a/apps/web/core/components/account/auth-forms/unique-code.tsx b/apps/web/core/components/account/auth-forms/unique-code.tsx
index aa65175b49d..9c4b8a6c189 100644
--- a/apps/web/core/components/account/auth-forms/unique-code.tsx
+++ b/apps/web/core/components/account/auth-forms/unique-code.tsx
@@ -41,7 +41,7 @@ const defaultValues: TUniqueCodeFormValues = {
};
export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
- const { mode, email, handleEmailClear, generateEmailUniqueCode, isExistingEmail, nextPath } = props;
+ const { mode, email, handleEmailClear, generateEmailUniqueCode, nextPath } = props;
// derived values
const defaultResetTimerValue = 5;
// states
diff --git a/apps/web/core/components/analytics/analytics-section-wrapper.tsx b/apps/web/core/components/analytics/analytics-section-wrapper.tsx
index 9a3af1171b8..d46ce74b46b 100644
--- a/apps/web/core/components/analytics/analytics-section-wrapper.tsx
+++ b/apps/web/core/components/analytics/analytics-section-wrapper.tsx
@@ -16,7 +16,7 @@ type Props = {
};
function AnalyticsSectionWrapper(props: Props) {
- const { title, children, className, subtitle, actions, headerClassName } = props;
+ const { title, children, className, actions, headerClassName } = props;
return (
diff --git a/apps/web/core/components/analytics/insight-table/data-table.tsx b/apps/web/core/components/analytics/insight-table/data-table.tsx
index 9c24d1aa572..a0f5213a693 100644
--- a/apps/web/core/components/analytics/insight-table/data-table.tsx
+++ b/apps/web/core/components/analytics/insight-table/data-table.tsx
@@ -38,10 +38,10 @@ interface DataTableProps {
}
export function DataTable({ columns, data, searchPlaceholder, actions }: DataTableProps) {
- const [rowSelection, setRowSelection] = React.useState({});
+ const [rowSelection, _setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState({});
const [columnFilters, setColumnFilters] = React.useState([]);
- const [sorting, setSorting] = React.useState([]);
+ const [sorting, _setSorting] = React.useState([]);
const { t } = useTranslation();
const inputRef = React.useRef(null);
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
diff --git a/apps/web/core/components/analytics/overview/active-project-item.tsx b/apps/web/core/components/analytics/overview/active-project-item.tsx
index e71ae52d530..0af7dc87fda 100644
--- a/apps/web/core/components/analytics/overview/active-project-item.tsx
+++ b/apps/web/core/components/analytics/overview/active-project-item.tsx
@@ -23,7 +23,7 @@ type Props = {
function CompletionPercentage({ percentage }: { percentage: number }) {
const percentageColor =
- percentage > 50 ? "bg-success-primary text-success-primary" : "bg-danger-primary text-danger-primary";
+ percentage > 50 ? "bg-success-subtle text-success-primary" : "bg-danger-subtle text-danger-primary";
return (
{percentage}%
diff --git a/apps/web/core/components/analytics/work-items/priority-chart.tsx b/apps/web/core/components/analytics/work-items/priority-chart.tsx
index f5ba27d6105..942285cab20 100644
--- a/apps/web/core/components/analytics/work-items/priority-chart.tsx
+++ b/apps/web/core/components/analytics/work-items/priority-chart.tsx
@@ -31,6 +31,7 @@ import { ChartLoader } from "../loaders";
import { generateBarColor } from "./utils";
declare module "@tanstack/react-table" {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
interface ColumnMeta {
export: {
key: string;
diff --git a/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx b/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx
index d63b84da8e1..52f3abd66ca 100644
--- a/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx
+++ b/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx
@@ -29,6 +29,7 @@ import { InsightTable } from "../insight-table";
const analyticsService = new AnalyticsService();
declare module "@tanstack/react-table" {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
interface ColumnMeta {
export: {
key: string;
diff --git a/apps/web/core/components/archives/archive-tabs-list.tsx b/apps/web/core/components/archives/archive-tabs-list.tsx
index 96396b9a930..ea21842013e 100644
--- a/apps/web/core/components/archives/archive-tabs-list.tsx
+++ b/apps/web/core/components/archives/archive-tabs-list.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
diff --git a/apps/web/core/components/comments/comment-create.tsx b/apps/web/core/components/comments/comment-create.tsx
index f727fc78962..d230a5be93e 100644
--- a/apps/web/core/components/comments/comment-create.tsx
+++ b/apps/web/core/components/comments/comment-create.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { useForm, Controller } from "react-hook-form";
diff --git a/apps/web/core/components/comments/comment-reaction.tsx b/apps/web/core/components/comments/comment-reaction.tsx
index e9d1c931b09..709de31e81b 100644
--- a/apps/web/core/components/comments/comment-reaction.tsx
+++ b/apps/web/core/components/comments/comment-reaction.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { useMemo, useState } from "react";
import { observer } from "mobx-react";
// plane imports
@@ -12,7 +11,6 @@ import { stringToEmoji } from "@plane/propel/emoji-icon-picker";
import { EmojiReactionGroup, EmojiReactionPicker } from "@plane/propel/emoji-reaction";
import type { EmojiReactionType } from "@plane/propel/emoji-reaction";
import type { TCommentsOperations, TIssueComment } from "@plane/types";
-import { cn } from "@plane/utils";
// helpers
// local imports
diff --git a/apps/web/core/components/common/activity/activity-item.tsx b/apps/web/core/components/common/activity/activity-item.tsx
index c4b3e363aad..0cc6a01f5ff 100644
--- a/apps/web/core/components/common/activity/activity-item.tsx
+++ b/apps/web/core/components/common/activity/activity-item.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
import type { TProjectActivity } from "@/plane-web/types";
@@ -18,7 +17,7 @@ type TActivityItem = {
};
export const ActivityItem = observer(function ActivityItem(props: TActivityItem) {
- const { activity, showProject = true, ends } = props;
+ const { activity, ends } = props;
if (!activity) return null;
diff --git a/apps/web/core/components/common/activity/user.tsx b/apps/web/core/components/common/activity/user.tsx
index 9254ad3eb8e..1ba3d03b750 100644
--- a/apps/web/core/components/common/activity/user.tsx
+++ b/apps/web/core/components/common/activity/user.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// types
diff --git a/apps/web/core/components/common/count-chip.tsx b/apps/web/core/components/common/count-chip.tsx
index 113dc4264cd..8b920415195 100644
--- a/apps/web/core/components/common/count-chip.tsx
+++ b/apps/web/core/components/common/count-chip.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
//
import { cn } from "@plane/utils";
diff --git a/apps/web/core/components/common/pro-icon.tsx b/apps/web/core/components/common/pro-icon.tsx
index 30e7ea97191..474ddd83676 100644
--- a/apps/web/core/components/common/pro-icon.tsx
+++ b/apps/web/core/components/common/pro-icon.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { Crown } from "lucide-react";
// helpers
import { cn } from "@plane/utils";
diff --git a/apps/web/core/components/core/list/list-root.tsx b/apps/web/core/components/core/list/list-root.tsx
index a965478597a..d81f2daeec2 100644
--- a/apps/web/core/components/core/list/list-root.tsx
+++ b/apps/web/core/components/core/list/list-root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
import { Row, ERowVariant } from "@plane/ui";
diff --git a/apps/web/core/components/core/theme/theme-switch.tsx b/apps/web/core/components/core/theme/theme-switch.tsx
index 3560e997840..b9de9bc5dc5 100644
--- a/apps/web/core/components/core/theme/theme-switch.tsx
+++ b/apps/web/core/components/core/theme/theme-switch.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
// plane imports
import type { I_THEME_OPTION } from "@plane/constants";
import { THEME_OPTIONS } from "@plane/constants";
diff --git a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx
index 678fe682d50..f6ea00ba6c6 100644
--- a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx
+++ b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { Fragment, useCallback, useRef, useState } from "react";
import { isEmpty } from "lodash-es";
import { observer } from "mobx-react";
diff --git a/apps/web/core/components/cycles/active-cycle/productivity.tsx b/apps/web/core/components/cycles/active-cycle/productivity.tsx
index 21720abc5c3..c3ebdfcbe8f 100644
--- a/apps/web/core/components/cycles/active-cycle/productivity.tsx
+++ b/apps/web/core/components/cycles/active-cycle/productivity.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { Fragment } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
diff --git a/apps/web/core/components/cycles/active-cycle/progress.tsx b/apps/web/core/components/cycles/active-cycle/progress.tsx
index d781920b6c3..4688e9dc5bc 100644
--- a/apps/web/core/components/cycles/active-cycle/progress.tsx
+++ b/apps/web/core/components/cycles/active-cycle/progress.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
import { useTheme } from "next-themes";
// plane imports
diff --git a/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx b/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx
index 4e4e579b875..738856d20ea 100644
--- a/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx
+++ b/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { useMemo } from "react";
import { isEmpty } from "lodash-es";
import { observer } from "mobx-react";
diff --git a/apps/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx b/apps/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx
index 65137162603..9c8ba8ac2ab 100644
--- a/apps/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx
+++ b/apps/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
import { isEmpty } from "lodash-es";
import { observer } from "mobx-react";
diff --git a/apps/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx b/apps/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx
index e2a825ccc24..05cdb8ebbb5 100644
--- a/apps/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx
+++ b/apps/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx
@@ -76,7 +76,7 @@ export const CycleSidebarHeader = observer(function CycleSidebarHeader(props: Pr
try {
const res = await cycleService.cycleDateCheck(workspaceSlug, projectId, payload);
return res.status;
- } catch (err) {
+ } catch (_err) {
return false;
}
};
diff --git a/apps/web/core/components/cycles/archived-cycles/view.tsx b/apps/web/core/components/cycles/archived-cycles/view.tsx
index 48a1f6a097a..2153fb0c969 100644
--- a/apps/web/core/components/cycles/archived-cycles/view.tsx
+++ b/apps/web/core/components/cycles/archived-cycles/view.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
// assets
import AllFiltersImage from "@/app/assets/empty-state/cycle/all-filters.svg?url";
diff --git a/apps/web/core/components/cycles/cycles-view.tsx b/apps/web/core/components/cycles/cycles-view.tsx
index 223c3a959f4..d0762125fee 100644
--- a/apps/web/core/components/cycles/cycles-view.tsx
+++ b/apps/web/core/components/cycles/cycles-view.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
// components
import { useTranslation } from "@plane/i18n";
diff --git a/apps/web/core/components/cycles/list/cycle-list-group-header.tsx b/apps/web/core/components/cycles/list/cycle-list-group-header.tsx
index 903e1086be9..9b3578639af 100644
--- a/apps/web/core/components/cycles/list/cycle-list-group-header.tsx
+++ b/apps/web/core/components/cycles/list/cycle-list-group-header.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
// types
import { CycleGroupIcon, ChevronDownIcon } from "@plane/propel/icons";
diff --git a/apps/web/core/components/cycles/list/cycle-list-project-group-header.tsx b/apps/web/core/components/cycles/list/cycle-list-project-group-header.tsx
index 9e6f5c134fa..3d836dac0b4 100644
--- a/apps/web/core/components/cycles/list/cycle-list-project-group-header.tsx
+++ b/apps/web/core/components/cycles/list/cycle-list-project-group-header.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
import { observer } from "mobx-react";
import { Logo } from "@plane/propel/emoji-icon-picker";
diff --git a/apps/web/core/components/cycles/list/root.tsx b/apps/web/core/components/cycles/list/root.tsx
index 0b2a4ed044f..07351decfbb 100644
--- a/apps/web/core/components/cycles/list/root.tsx
+++ b/apps/web/core/components/cycles/list/root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import React from "react";
import { observer } from "mobx-react";
import { Disclosure } from "@headlessui/react";
diff --git a/apps/web/core/components/cycles/modal.tsx b/apps/web/core/components/cycles/modal.tsx
index 2128bc3461e..582489ced59 100644
--- a/apps/web/core/components/cycles/modal.tsx
+++ b/apps/web/core/components/cycles/modal.tsx
@@ -50,7 +50,7 @@ export function CycleCreateUpdateModal(props: CycleModalProps) {
const selectedProjectId = payload.project_id ?? projectId.toString();
await createCycle(workspaceSlug, selectedProjectId, payload)
- .then((res) => {
+ .then((_res) => {
// mutate when the current cycle creation is active
if (payload.start_date && payload.end_date) {
const currentDate = new Date();
@@ -81,7 +81,7 @@ export function CycleCreateUpdateModal(props: CycleModalProps) {
const selectedProjectId = payload.project_id ?? projectId.toString();
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
- .then((res) => {
+ .then((_res) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
diff --git a/apps/web/core/components/empty-state/section-empty-state-root.tsx b/apps/web/core/components/empty-state/section-empty-state-root.tsx
index cc4acdffb48..3d0a05aa3d2 100644
--- a/apps/web/core/components/empty-state/section-empty-state-root.tsx
+++ b/apps/web/core/components/empty-state/section-empty-state-root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { cn } from "@plane/utils";
type Props = {
diff --git a/apps/web/core/components/estimates/create/modal.tsx b/apps/web/core/components/estimates/create/modal.tsx
index 02bbaced9a2..518f6665095 100644
--- a/apps/web/core/components/estimates/create/modal.tsx
+++ b/apps/web/core/components/estimates/create/modal.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react";
// plane imports
diff --git a/apps/web/core/components/estimates/delete/modal.tsx b/apps/web/core/components/estimates/delete/modal.tsx
index 8181fc88814..e6b39fcdec2 100644
--- a/apps/web/core/components/estimates/delete/modal.tsx
+++ b/apps/web/core/components/estimates/delete/modal.tsx
@@ -48,7 +48,7 @@ export const DeleteEstimateModal = observer(function DeleteEstimateModal(props:
message: "Estimate has been removed from your project.",
});
handleClose();
- } catch (error) {
+ } catch (_error) {
setButtonLoader(false);
setToast({
type: TOAST_TYPE.ERROR,
diff --git a/apps/web/core/components/estimates/estimate-disable-switch.tsx b/apps/web/core/components/estimates/estimate-disable-switch.tsx
index 7553ce0757d..13bf7ce663a 100644
--- a/apps/web/core/components/estimates/estimate-disable-switch.tsx
+++ b/apps/web/core/components/estimates/estimate-disable-switch.tsx
@@ -44,7 +44,7 @@ export const EstimateDisableSwitch = observer(function EstimateDisableSwitch(pro
? t("project_settings.estimates.toasts.disabled.success.message")
: t("project_settings.estimates.toasts.enabled.success.message"),
});
- } catch (err) {
+ } catch (_err) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("project_settings.estimates.toasts.disabled.error.title"),
diff --git a/apps/web/core/components/estimates/estimate-list.tsx b/apps/web/core/components/estimates/estimate-list.tsx
index 0a428e8f711..6e919c55715 100644
--- a/apps/web/core/components/estimates/estimate-list.tsx
+++ b/apps/web/core/components/estimates/estimate-list.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
// local imports
import { EstimateListItem } from "./estimate-list-item";
diff --git a/apps/web/core/components/estimates/estimate-search.tsx b/apps/web/core/components/estimates/estimate-search.tsx
index 38a6439f18d..0b05f6716c2 100644
--- a/apps/web/core/components/estimates/estimate-search.tsx
+++ b/apps/web/core/components/estimates/estimate-search.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { observer } from "mobx-react";
export const EstimateSearch = observer(function EstimateSearch() {
diff --git a/apps/web/core/components/estimates/inputs/number-input.tsx b/apps/web/core/components/estimates/inputs/number-input.tsx
index a93626df1cb..3e4d30b73d6 100644
--- a/apps/web/core/components/estimates/inputs/number-input.tsx
+++ b/apps/web/core/components/estimates/inputs/number-input.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { useTranslation } from "@plane/i18n";
type TEstimateNumberInputProps = {
value?: number;
diff --git a/apps/web/core/components/estimates/inputs/root.tsx b/apps/web/core/components/estimates/inputs/root.tsx
index 1c38f41cfeb..86b743ff7b2 100644
--- a/apps/web/core/components/estimates/inputs/root.tsx
+++ b/apps/web/core/components/estimates/inputs/root.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
// plane imports
import type { TEstimateSystemKeys } from "@plane/types";
import { EEstimateSystem } from "@plane/types";
diff --git a/apps/web/core/components/estimates/inputs/text-input.tsx b/apps/web/core/components/estimates/inputs/text-input.tsx
index 03dd0b50d4a..50fe55da86e 100644
--- a/apps/web/core/components/estimates/inputs/text-input.tsx
+++ b/apps/web/core/components/estimates/inputs/text-input.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { useTranslation } from "@plane/i18n";
type TEstimateTextInputProps = {
value?: string;
diff --git a/apps/web/core/components/estimates/loader-screen.tsx b/apps/web/core/components/estimates/loader-screen.tsx
index 79f3b03947b..d858df1d971 100644
--- a/apps/web/core/components/estimates/loader-screen.tsx
+++ b/apps/web/core/components/estimates/loader-screen.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import type { FC } from "react";
import { Loader } from "@plane/ui";
export function EstimateLoaderScreen() {
diff --git a/apps/web/core/components/estimates/points/create-root.tsx b/apps/web/core/components/estimates/points/create-root.tsx
index b309b2890c7..ad3c57e9fd7 100644
--- a/apps/web/core/components/estimates/points/create-root.tsx
+++ b/apps/web/core/components/estimates/points/create-root.tsx
@@ -4,7 +4,7 @@
* See the LICENSE file for details.
*/
-import type { Dispatch, FC, SetStateAction } from "react";
+import type { Dispatch, SetStateAction } from "react";
import { useCallback, useState } from "react";
import { observer } from "mobx-react";
// plane imports
diff --git a/apps/web/core/components/estimates/points/create.tsx b/apps/web/core/components/estimates/points/create.tsx
index a4fd55ad183..50aced9cd56 100644
--- a/apps/web/core/components/estimates/points/create.tsx
+++ b/apps/web/core/components/estimates/points/create.tsx
@@ -166,12 +166,6 @@ export const EstimatePointCreate = observer(function EstimatePointCreate(props:
handleEstimatePointError(estimateInputValue, t("project_settings.estimates.validation.empty"));
};
- // derived values
- const inputProps = {
- type: "text",
- maxlength: MAX_ESTIMATE_POINT_INPUT_LENGTH,
- };
-
return (
|