diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 6e1e5e057..d1e1917a4 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -784,6 +784,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): def post(self, request, slug, project_id, cycle_id): new_cycle_id = request.data.get("new_cycle_id", False) + plot_type = request.GET.get("plot_type", "issues") if not new_cycle_id: return Response( @@ -865,6 +866,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): queryset=old_cycle.first(), slug=slug, project_id=project_id, + plot_type=plot_type, cycle_id=cycle_id, ) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 019ab704e..408e14fed 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -22,7 +22,7 @@ from plane.db.models import ( IssueProperty, Module, Project, - ProjectDeployBoard, + DeployBoard, ProjectMember, State, Workspace, @@ -99,7 +99,7 @@ class ProjectAPIEndpoint(BaseAPIView): ) .annotate( is_deployed=Exists( - ProjectDeployBoard.objects.filter( + DeployBoard.objects.filter( project_id=OuterRef("pk"), workspace__slug=self.kwargs.get("slug"), ) diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index bdcdf6c0d..d8364f931 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -30,7 +30,7 @@ from .project import ( ProjectIdentifierSerializer, ProjectLiteSerializer, ProjectMemberLiteSerializer, - ProjectDeployBoardSerializer, + DeployBoardSerializer, ProjectMemberAdminSerializer, ProjectPublicMemberSerializer, ProjectMemberRoleSerializer, diff --git a/apiserver/plane/app/serializers/estimate.py b/apiserver/plane/app/serializers/estimate.py index d28f38c75..e73b5ceef 100644 --- a/apiserver/plane/app/serializers/estimate.py +++ b/apiserver/plane/app/serializers/estimate.py @@ -11,10 +11,6 @@ from rest_framework import serializers class EstimateSerializer(BaseSerializer): - workspace_detail = WorkspaceLiteSerializer( - read_only=True, source="workspace" - ) - project_detail = ProjectLiteSerializer(read_only=True, source="project") class Meta: model = Estimate @@ -48,10 +44,6 @@ class EstimatePointSerializer(BaseSerializer): class EstimateReadSerializer(BaseSerializer): points = EstimatePointSerializer(read_only=True, many=True) - workspace_detail = WorkspaceLiteSerializer( - read_only=True, source="workspace" - ) - project_detail = ProjectLiteSerializer(read_only=True, source="project") class Meta: model = Estimate diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index 28d28d7db..7762d3b80 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -177,6 +177,8 @@ class ModuleSerializer(DynamicBaseSerializer): started_issues = serializers.IntegerField(read_only=True) unstarted_issues = serializers.IntegerField(read_only=True) backlog_issues = serializers.IntegerField(read_only=True) + total_estimate_points = serializers.IntegerField(read_only=True) + completed_estimate_points = serializers.IntegerField(read_only=True) class Meta: model = Module @@ -201,6 +203,8 @@ class ModuleSerializer(DynamicBaseSerializer): "external_id", "logo_props", # computed fields + "total_estimate_points", + "completed_estimate_points", "is_favorite", "total_issues", "cancelled_issues", diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index 96d92f340..1bbc580c1 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -13,7 +13,7 @@ from plane.db.models import ( ProjectMember, ProjectMemberInvite, ProjectIdentifier, - ProjectDeployBoard, + DeployBoard, ProjectPublicMember, ) @@ -114,7 +114,7 @@ class ProjectListSerializer(DynamicBaseSerializer): is_member = serializers.BooleanField(read_only=True) sort_order = serializers.FloatField(read_only=True) member_role = serializers.IntegerField(read_only=True) - is_deployed = serializers.BooleanField(read_only=True) + anchor = serializers.CharField(read_only=True) members = serializers.SerializerMethodField() def get_members(self, obj): @@ -148,7 +148,7 @@ class ProjectDetailSerializer(BaseSerializer): is_member = serializers.BooleanField(read_only=True) sort_order = serializers.FloatField(read_only=True) member_role = serializers.IntegerField(read_only=True) - is_deployed = serializers.BooleanField(read_only=True) + anchor = serializers.CharField(read_only=True) class Meta: model = Project @@ -206,14 +206,14 @@ class ProjectMemberLiteSerializer(BaseSerializer): read_only_fields = fields -class ProjectDeployBoardSerializer(BaseSerializer): +class DeployBoardSerializer(BaseSerializer): project_details = ProjectLiteSerializer(read_only=True, source="project") workspace_detail = WorkspaceLiteSerializer( read_only=True, source="workspace" ) class Meta: - model = ProjectDeployBoard + model = DeployBoard fields = "__all__" read_only_fields = [ "workspace", diff --git a/apiserver/plane/app/urls/estimate.py b/apiserver/plane/app/urls/estimate.py index d8571ff0c..7db94aa46 100644 --- a/apiserver/plane/app/urls/estimate.py +++ b/apiserver/plane/app/urls/estimate.py @@ -4,6 +4,7 @@ from django.urls import path from plane.app.views import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, + EstimatePointEndpoint, ) @@ -34,4 +35,23 @@ urlpatterns = [ ), name="bulk-create-estimate-points", ), + path( + "workspaces//projects//estimates//estimate-points/", + EstimatePointEndpoint.as_view( + { + "post": "create", + } + ), + name="estimate-points", + ), + path( + "workspaces//projects//estimates//estimate-points//", + EstimatePointEndpoint.as_view( + { + "patch": "partial_update", + "delete": "destroy", + } + ), + name="estimate-points", + ), ] diff --git a/apiserver/plane/app/urls/project.py b/apiserver/plane/app/urls/project.py index 7ea636df8..0807c7616 100644 --- a/apiserver/plane/app/urls/project.py +++ b/apiserver/plane/app/urls/project.py @@ -2,6 +2,7 @@ from django.urls import path from plane.app.views import ( ProjectViewSet, + DeployBoardViewSet, ProjectInvitationsViewset, ProjectMemberViewSet, ProjectMemberUserEndpoint, @@ -12,7 +13,6 @@ from plane.app.views import ( ProjectFavoritesViewSet, UserProjectInvitationsViewset, ProjectPublicCoverImagesEndpoint, - ProjectDeployBoardViewSet, UserProjectRolesEndpoint, ProjectArchiveUnarchiveEndpoint, ) @@ -157,7 +157,7 @@ urlpatterns = [ ), path( "workspaces//projects//project-deploy-boards/", - ProjectDeployBoardViewSet.as_view( + DeployBoardViewSet.as_view( { "get": "list", "post": "create", @@ -167,7 +167,7 @@ urlpatterns = [ ), path( "workspaces//projects//project-deploy-boards//", - ProjectDeployBoardViewSet.as_view( + DeployBoardViewSet.as_view( { "get": "retrieve", "patch": "partial_update", diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 0c489593d..8da0268b9 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -4,7 +4,7 @@ from .project.base import ( ProjectUserViewsEndpoint, ProjectFavoritesViewSet, ProjectPublicCoverImagesEndpoint, - ProjectDeployBoardViewSet, + DeployBoardViewSet, ProjectArchiveUnarchiveEndpoint, ) @@ -190,6 +190,7 @@ from .external.base import ( from .estimate.base import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, + EstimatePointEndpoint, ) from .inbox.base import InboxViewSet, InboxIssueViewSet diff --git a/apiserver/plane/app/views/analytic/base.py b/apiserver/plane/app/views/analytic/base.py index 256d3cae5..3d27641e3 100644 --- a/apiserver/plane/app/views/analytic/base.py +++ b/apiserver/plane/app/views/analytic/base.py @@ -33,7 +33,7 @@ class AnalyticsEndpoint(BaseAPIView): "state__group", "labels__id", "assignees__id", - "estimate_point", + "estimate_point__value", "issue_cycle__cycle_id", "issue_module__module_id", "priority", @@ -381,9 +381,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView): ) open_estimate_sum = open_issues_queryset.aggregate( - sum=Sum("estimate_point") + sum=Sum("point") )["sum"] - total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))[ + total_estimate_sum = base_issues.aggregate(sum=Sum("point"))[ "sum" ] diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py index 5e1241b08..f99c2ec97 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -177,6 +177,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView): ) def get(self, request, slug, project_id, pk=None): + plot_type = request.GET.get("plot_type", "issues") if pk is None: queryset = ( self.get_queryset() @@ -375,6 +376,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView): queryset=queryset, slug=slug, project_id=project_id, + plot_type=plot_type, cycle_id=pk, ) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 5982daf7f..91012748f 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -17,8 +17,11 @@ from django.db.models import ( UUIDField, Value, When, + Subquery, + Sum, + IntegerField, ) -from django.db.models.functions import Coalesce +from django.db.models.functions import Coalesce, Cast from django.utils import timezone from django.core.serializers.json import DjangoJSONEncoder @@ -73,6 +76,89 @@ class CycleViewSet(BaseViewSet): project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) + backlog_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="backlog", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + backlog_estimate_point=Sum( + Cast("estimate_point__value", IntegerField()) + ) + ) + .values("backlog_estimate_point")[:1] + ) + unstarted_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="unstarted", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + unstarted_estimate_point=Sum( + Cast("estimate_point__value", IntegerField()) + ) + ) + .values("unstarted_estimate_point")[:1] + ) + started_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="started", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + started_estimate_point=Sum( + Cast("estimate_point__value", IntegerField()) + ) + ) + .values("started_estimate_point")[:1] + ) + cancelled_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="cancelled", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + cancelled_estimate_point=Sum( + Cast("estimate_point__value", IntegerField()) + ) + ) + .values("cancelled_estimate_point")[:1] + ) + completed_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="completed", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + completed_estimate_points=Sum( + Cast("estimate_point__value", IntegerField()) + ) + ) + .values("completed_estimate_points")[:1] + ) + total_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + total_estimate_points=Sum( + Cast("estimate_point__value", IntegerField()) + ) + ) + .values("total_estimate_points")[:1] + ) return self.filter_queryset( super() .get_queryset() @@ -197,12 +283,49 @@ class CycleViewSet(BaseViewSet): Value([], output_field=ArrayField(UUIDField())), ) ) + .annotate( + backlog_estimate_points=Coalesce( + Subquery(backlog_estimate_point), + Value(0, output_field=IntegerField()), + ), + ) + .annotate( + unstarted_estimate_points=Coalesce( + Subquery(unstarted_estimate_point), + Value(0, output_field=IntegerField()), + ), + ) + .annotate( + started_estimate_points=Coalesce( + Subquery(started_estimate_point), + Value(0, output_field=IntegerField()), + ), + ) + .annotate( + cancelled_estimate_points=Coalesce( + Subquery(cancelled_estimate_point), + Value(0, output_field=IntegerField()), + ), + ) + .annotate( + completed_estimate_points=Coalesce( + Subquery(completed_estimate_point), + Value(0, output_field=IntegerField()), + ), + ) + .annotate( + total_estimate_points=Coalesce( + Subquery(total_estimate_point), + Value(0, output_field=IntegerField()), + ), + ) .order_by("-is_favorite", "name") .distinct() ) def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) + plot_type = request.GET.get("plot_type", "issues") cycle_view = request.GET.get("cycle_view", "all") # Update the order by @@ -233,6 +356,12 @@ class CycleViewSet(BaseViewSet): "progress_snapshot", "logo_props", # meta fields + "backlog_estimate_points", + "unstarted_estimate_points", + "started_estimate_points", + "cancelled_estimate_points", + "completed_estimate_points", + "total_estimate_points", "is_favorite", "total_issues", "cancelled_issues", @@ -335,6 +464,7 @@ class CycleViewSet(BaseViewSet): queryset=queryset.first(), slug=slug, project_id=project_id, + plot_type=plot_type, cycle_id=data[0]["id"], ) ) @@ -359,6 +489,8 @@ class CycleViewSet(BaseViewSet): "progress_snapshot", "logo_props", # meta fields + "completed_estimate_points", + "total_estimate_points", "is_favorite", "total_issues", "cancelled_issues", @@ -527,6 +659,7 @@ class CycleViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk): + plot_type = request.GET.get("plot_type", "issues") queryset = ( self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk) ) @@ -682,6 +815,7 @@ class CycleViewSet(BaseViewSet): queryset=queryset, slug=slug, project_id=project_id, + plot_type=plot_type, cycle_id=pk, ) @@ -798,6 +932,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): def post(self, request, slug, project_id, cycle_id): new_cycle_id = request.data.get("new_cycle_id", False) + plot_type = request.GET.get("plot_type", "issues") if not new_cycle_id: return Response( @@ -879,6 +1014,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): queryset=old_cycle.first(), slug=slug, project_id=project_id, + plot_type=plot_type, cycle_id=cycle_id, ) diff --git a/apiserver/plane/app/views/estimate/base.py b/apiserver/plane/app/views/estimate/base.py index 7ac3035a9..2bd9e3dfe 100644 --- a/apiserver/plane/app/views/estimate/base.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -1,3 +1,6 @@ +import random +import string + # Third party imports from rest_framework.response import Response from rest_framework import status @@ -5,7 +8,7 @@ from rest_framework import status # Module imports from ..base import BaseViewSet, BaseAPIView from plane.app.permissions import ProjectEntityPermission -from plane.db.models import Project, Estimate, EstimatePoint +from plane.db.models import Project, Estimate, EstimatePoint, Issue from plane.app.serializers import ( EstimateSerializer, EstimatePointSerializer, @@ -13,6 +16,12 @@ from plane.app.serializers import ( ) from plane.utils.cache import invalidate_cache + +def generate_random_name(length=10): + letters = string.ascii_lowercase + return "".join(random.choice(letters) for i in range(length)) + + class ProjectEstimatePointEndpoint(BaseAPIView): permission_classes = [ ProjectEntityPermission, @@ -49,13 +58,17 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer = EstimateReadSerializer(estimates, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) + @invalidate_cache( + path="/api/workspaces/:slug/estimates/", url_params=True, user=False + ) def create(self, request, slug, project_id): - if not request.data.get("estimate", False): - return Response( - {"error": "Estimate is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + estimate = request.data.get('estimate') + estimate_name = estimate.get("name", generate_random_name()) + estimate_type = estimate.get("type", 'categories') + last_used = estimate.get("last_used", False) + estimate = Estimate.objects.create( + name=estimate_name, project_id=project_id, last_used=last_used, type=estimate_type + ) estimate_points = request.data.get("estimate_points", []) @@ -67,14 +80,6 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - estimate_serializer = EstimateSerializer( - data=request.data.get("estimate") - ) - if not estimate_serializer.is_valid(): - return Response( - estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - estimate = estimate_serializer.save(project_id=project_id) estimate_points = EstimatePoint.objects.bulk_create( [ EstimatePoint( @@ -93,17 +98,8 @@ class BulkEstimatePointEndpoint(BaseViewSet): ignore_conflicts=True, ) - estimate_point_serializer = EstimatePointSerializer( - estimate_points, many=True - ) - - return Response( - { - "estimate": estimate_serializer.data, - "estimate_points": estimate_point_serializer.data, - }, - status=status.HTTP_200_OK, - ) + serializer = EstimateReadSerializer(estimate) + return Response(serializer.data, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, estimate_id): estimate = Estimate.objects.get( @@ -115,13 +111,10 @@ class BulkEstimatePointEndpoint(BaseViewSet): status=status.HTTP_200_OK, ) - @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) + @invalidate_cache( + path="/api/workspaces/:slug/estimates/", url_params=True, user=False + ) def partial_update(self, request, slug, project_id, estimate_id): - if not request.data.get("estimate", False): - return Response( - {"error": "Estimate is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) if not len(request.data.get("estimate_points", [])): return Response( @@ -131,15 +124,10 @@ class BulkEstimatePointEndpoint(BaseViewSet): estimate = Estimate.objects.get(pk=estimate_id) - estimate_serializer = EstimateSerializer( - estimate, data=request.data.get("estimate"), partial=True - ) - if not estimate_serializer.is_valid(): - return Response( - estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - - estimate = estimate_serializer.save() + if request.data.get("estimate"): + estimate.name = request.data.get("estimate").get("name", estimate.name) + estimate.type = request.data.get("estimate").get("type", estimate.type) + estimate.save() estimate_points_data = request.data.get("estimate_points", []) @@ -165,29 +153,113 @@ class BulkEstimatePointEndpoint(BaseViewSet): estimate_point.value = estimate_point_data[0].get( "value", estimate_point.value ) + estimate_point.key = estimate_point_data[0].get( + "key", estimate_point.key + ) updated_estimate_points.append(estimate_point) EstimatePoint.objects.bulk_update( updated_estimate_points, - ["value"], + ["key", "value"], batch_size=10, ) - estimate_point_serializer = EstimatePointSerializer( - estimate_points, many=True - ) + estimate_serializer = EstimateReadSerializer(estimate) return Response( - { - "estimate": estimate_serializer.data, - "estimate_points": estimate_point_serializer.data, - }, + estimate_serializer.data, status=status.HTTP_200_OK, ) - @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) + @invalidate_cache( + path="/api/workspaces/:slug/estimates/", url_params=True, user=False + ) def destroy(self, request, slug, project_id, estimate_id): estimate = Estimate.objects.get( pk=estimate_id, workspace__slug=slug, project_id=project_id ) estimate.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + +class EstimatePointEndpoint(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + + def create(self, request, slug, project_id, estimate_id): + # TODO: add a key validation if the same key already exists + if not request.data.get("key") or not request.data.get("value"): + return Response( + {"error": "Key and value are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + key = request.data.get("key", 0) + value = request.data.get("value", "") + estimate_point = EstimatePoint.objects.create( + estimate_id=estimate_id, + project_id=project_id, + key=key, + value=value, + ) + serializer = EstimatePointSerializer(estimate_point).data + return Response(serializer, status=status.HTTP_200_OK) + + def partial_update(self, request, slug, project_id, estimate_id, estimate_point_id): + # TODO: add a key validation if the same key already exists + estimate_point = EstimatePoint.objects.get( + pk=estimate_point_id, + estimate_id=estimate_id, + project_id=project_id, + workspace__slug=slug, + ) + serializer = EstimatePointSerializer( + estimate_point, data=request.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) + + def destroy( + self, request, slug, project_id, estimate_id, estimate_point_id + ): + new_estimate_id = request.GET.get("new_estimate_id", None) + estimate_points = EstimatePoint.objects.filter( + estimate_id=estimate_id, + project_id=project_id, + workspace__slug=slug, + ) + # update all the issues with the new estimate + if new_estimate_id: + _ = Issue.objects.filter( + project_id=project_id, + workspace__slug=slug, + estimate_id=estimate_point_id, + ).update(estimate_id=new_estimate_id) + + # delete the estimate point + old_estimate_point = EstimatePoint.objects.filter( + pk=estimate_point_id + ).first() + + # rearrange the estimate points + updated_estimate_points = [] + for estimate_point in estimate_points: + if estimate_point.key > old_estimate_point.key: + estimate_point.key -= 1 + updated_estimate_points.append(estimate_point) + + EstimatePoint.objects.bulk_update( + updated_estimate_points, + ["key"], + batch_size=10, + ) + + old_estimate_point.delete() + + return Response( + EstimatePointSerializer(updated_estimate_points, many=True).data, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/app/views/module/archive.py b/apiserver/plane/app/views/module/archive.py index 2cac5f366..d474f26c6 100644 --- a/apiserver/plane/app/views/module/archive.py +++ b/apiserver/plane/app/views/module/archive.py @@ -165,6 +165,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView): ) def get(self, request, slug, project_id, pk=None): + plot_type = request.GET.get("plot_type", "issues") if pk is None: queryset = self.get_queryset() modules = queryset.values( # Required fields @@ -323,6 +324,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView): queryset=modules, slug=slug, project_id=project_id, + plot_type=plot_type, module_id=pk, ) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 56267554d..5b631be46 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -16,8 +16,9 @@ from django.db.models import ( Subquery, UUIDField, Value, + Sum, ) -from django.db.models.functions import Coalesce +from django.db.models.functions import Coalesce, Cast from django.core.serializers.json import DjangoJSONEncoder from django.utils import timezone @@ -128,6 +129,34 @@ class ModuleViewSet(BaseViewSet): .annotate(cnt=Count("pk")) .values("cnt") ) + completed_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="completed", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate( + completed_estimate_points=Sum( + Cast("estimate_point__value", IntegerField()) + ) + ) + .values("completed_estimate_points")[:1] + ) + + total_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate( + total_estimate_points=Sum( + Cast("estimate_point__value", IntegerField()) + ) + ) + .values("total_estimate_points")[:1] + ) return ( super() .get_queryset() @@ -182,6 +211,18 @@ class ModuleViewSet(BaseViewSet): Value(0, output_field=IntegerField()), ) ) + .annotate( + completed_estimate_points=Coalesce( + Subquery(completed_estimate_point), + Value(0, output_field=IntegerField()), + ), + ) + .annotate( + total_estimate_points=Coalesce( + Subquery(total_estimate_point), + Value(0, output_field=IntegerField()), + ), + ) .annotate( member_ids=Coalesce( ArrayAgg( @@ -233,6 +274,8 @@ class ModuleViewSet(BaseViewSet): "total_issues", "started_issues", "unstarted_issues", + "completed_estimate_points", + "total_estimate_points", "backlog_issues", "created_at", "updated_at", @@ -284,6 +327,8 @@ class ModuleViewSet(BaseViewSet): "external_id", "logo_props", # computed fields + "completed_estimate_points", + "total_estimate_points", "total_issues", "is_favorite", "cancelled_issues", @@ -301,6 +346,7 @@ class ModuleViewSet(BaseViewSet): return Response(modules, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk): + plot_type = request.GET.get("plot_type", "burndown") queryset = ( self.get_queryset() .filter(archived_at__isnull=True) @@ -423,6 +469,7 @@ class ModuleViewSet(BaseViewSet): queryset=modules, slug=slug, project_id=project_id, + plot_type=plot_type, module_id=pk, ) @@ -469,6 +516,8 @@ class ModuleViewSet(BaseViewSet): "external_id", "logo_props", # computed fields + "completed_estimate_points", + "total_estimate_points", "is_favorite", "cancelled_issues", "completed_issues", diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 39db11871..7e3326e02 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -28,7 +28,7 @@ from plane.app.views.base import BaseViewSet, BaseAPIView from plane.app.serializers import ( ProjectSerializer, ProjectListSerializer, - ProjectDeployBoardSerializer, + DeployBoardSerializer, ) from plane.app.permissions import ( @@ -46,7 +46,7 @@ from plane.db.models import ( Module, Cycle, Inbox, - ProjectDeployBoard, + DeployBoard, IssueProperty, Issue, ) @@ -137,12 +137,11 @@ class ProjectViewSet(BaseViewSet): ).values("role") ) .annotate( - is_deployed=Exists( - ProjectDeployBoard.objects.filter( - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ) - ) + anchor=DeployBoard.objects.filter( + entity_name="project", + entity_identifier=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ).values("anchor") ) .annotate(sort_order=Subquery(sort_order)) .prefetch_related( @@ -639,29 +638,28 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): return Response(files, status=status.HTTP_200_OK) -class ProjectDeployBoardViewSet(BaseViewSet): +class DeployBoardViewSet(BaseViewSet): permission_classes = [ ProjectMemberPermission, ] - serializer_class = ProjectDeployBoardSerializer - model = ProjectDeployBoard + serializer_class = DeployBoardSerializer + model = DeployBoard - def get_queryset(self): - return ( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - .select_related("project") - ) + def list(self, request, slug, project_id): + project_deploy_board = DeployBoard.objects.filter( + entity_name="project", + entity_identifier=project_id, + workspace__slug=slug, + ).first() + + serializer = DeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, slug, project_id): - comments = request.data.get("comments", False) - reactions = request.data.get("reactions", False) + comments = request.data.get("is_comments_enabled", False) + reactions = request.data.get("is_reactions_enabled", False) inbox = request.data.get("inbox", None) - votes = request.data.get("votes", False) + votes = request.data.get("is_votes_enabled", False) views = request.data.get( "views", { @@ -673,17 +671,18 @@ class ProjectDeployBoardViewSet(BaseViewSet): }, ) - project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( - anchor=f"{slug}/{project_id}", + project_deploy_board, _ = DeployBoard.objects.get_or_create( + entity_name="project", + entity_identifier=project_id, project_id=project_id, ) - project_deploy_board.comments = comments - project_deploy_board.reactions = reactions project_deploy_board.inbox = inbox - project_deploy_board.votes = votes - project_deploy_board.views = views + project_deploy_board.view_props = views + project_deploy_board.is_votes_enabled = votes + project_deploy_board.is_comments_enabled = comments + project_deploy_board.is_reactions_enabled = reactions project_deploy_board.save() - serializer = ProjectDeployBoardSerializer(project_deploy_board) + serializer = DeployBoardSerializer(project_deploy_board) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 007b3e48c..67cda14af 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -28,6 +28,7 @@ from plane.db.models import ( Project, State, User, + EstimatePoint, ) from plane.settings.redis import redis_instance from plane.utils.exception_logger import log_exception @@ -448,21 +449,37 @@ def track_estimate_points( if current_instance.get("estimate_point") != requested_data.get( "estimate_point" ): + old_estimate = ( + EstimatePoint.objects.filter( + pk=current_instance.get("estimate_point") + ).first() + if current_instance.get("estimate_point") is not None + else None + ) + new_estimate = ( + EstimatePoint.objects.filter( + pk=requested_data.get("estimate_point") + ).first() + if requested_data.get("estimate_point") is not None + else None + ) issue_activities.append( IssueActivity( issue_id=issue_id, actor_id=actor_id, verb="updated", - old_value=( + old_identifier=( current_instance.get("estimate_point") if current_instance.get("estimate_point") is not None - else "" + else None ), - new_value=( + new_identifier=( requested_data.get("estimate_point") if requested_data.get("estimate_point") is not None - else "" + else None ), + old_value=old_estimate.value if old_estimate else None, + new_value=new_estimate.value if new_estimate else None, field="estimate_point", project_id=project_id, workspace_id=workspace_id, diff --git a/apiserver/plane/db/migrations/0067_issue_estimate.py b/apiserver/plane/db/migrations/0067_issue_estimate.py new file mode 100644 index 000000000..b341f9864 --- /dev/null +++ b/apiserver/plane/db/migrations/0067_issue_estimate.py @@ -0,0 +1,260 @@ +# # Generated by Django 4.2.7 on 2024-05-24 09:47 +# Python imports +import uuid +from uuid import uuid4 +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models +import plane.db.models.deploy_board + + +def issue_estimate_point(apps, schema_editor): + Issue = apps.get_model("db", "Issue") + Project = apps.get_model("db", "Project") + EstimatePoint = apps.get_model("db", "EstimatePoint") + IssueActivity = apps.get_model("db", "IssueActivity") + updated_estimate_point = [] + updated_issue_activity = [] + + # loop through all the projects + for project in Project.objects.filter(estimate__isnull=False): + estimate_points = EstimatePoint.objects.filter( + estimate=project.estimate, project=project + ) + + for issue_activity in IssueActivity.objects.filter( + field="estimate_point", project=project + ): + if issue_activity.new_value: + new_identifier = estimate_points.filter( + key=issue_activity.new_value + ).first().id + issue_activity.new_identifier = new_identifier + new_value = estimate_points.filter( + key=issue_activity.new_value + ).first().value + issue_activity.new_value = new_value + + if issue_activity.old_value: + old_identifier = estimate_points.filter( + key=issue_activity.old_value + ).first().id + issue_activity.old_identifier = old_identifier + old_value = estimate_points.filter( + key=issue_activity.old_value + ).first().value + issue_activity.old_value = old_value + updated_issue_activity.append(issue_activity) + + for issue in Issue.objects.filter( + point__isnull=False, project=project + ): + # get the estimate id for the corresponding estimate point in the issue + estimate = estimate_points.filter(key=issue.point).first() + issue.estimate_point = estimate + updated_estimate_point.append(issue) + + Issue.objects.bulk_update( + updated_estimate_point, ["estimate_point"], batch_size=1000 + ) + IssueActivity.objects.bulk_update( + updated_issue_activity, + ["new_value", "old_value", "new_identifier", "old_identifier"], + batch_size=1000, + ) + + +def last_used_estimate(apps, schema_editor): + Project = apps.get_model("db", "Project") + Estimate = apps.get_model("db", "Estimate") + + # Get all estimate ids used in projects + estimate_ids = Project.objects.filter(estimate__isnull=False).values_list( + "estimate", flat=True + ) + + # Update all matching estimates + Estimate.objects.filter(id__in=estimate_ids).update(last_used=True) + + +def populate_deploy_board(apps, schema_editor): + DeployBoard = apps.get_model("db", "DeployBoard") + ProjectDeployBoard = apps.get_model("db", "ProjectDeployBoard") + + DeployBoard.objects.bulk_create( + [ + DeployBoard( + entity_identifier=deploy_board.project_id, + project_id=deploy_board.project_id, + entity_name="project", + anchor=uuid4().hex, + is_comments_enabled=deploy_board.comments, + is_reactions_enabled=deploy_board.reactions, + inbox=deploy_board.inbox, + is_votes_enabled=deploy_board.votes, + view_props=deploy_board.views, + workspace_id=deploy_board.workspace_id, + created_at=deploy_board.created_at, + updated_at=deploy_board.updated_at, + created_by_id=deploy_board.created_by_id, + updated_by_id=deploy_board.updated_by_id, + ) + for deploy_board in ProjectDeployBoard.objects.all() + ], + batch_size=100, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0066_account_id_token_cycle_logo_props_module_logo_props"), + ] + + operations = [ + migrations.CreateModel( + name="DeployBoard", + fields=[ + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("entity_identifier", models.UUIDField(null=True)), + ( + "entity_name", + models.CharField( + choices=[ + ("project", "Project"), + ("issue", "Issue"), + ("module", "Module"), + ("cycle", "Task"), + ("page", "Page"), + ("view", "View"), + ], + max_length=30, + ), + ), + ( + "anchor", + models.CharField( + db_index=True, + default=plane.db.models.deploy_board.get_anchor, + max_length=255, + unique=True, + ), + ), + ("is_comments_enabled", models.BooleanField(default=False)), + ("is_reactions_enabled", models.BooleanField(default=False)), + ("is_votes_enabled", models.BooleanField(default=False)), + ("view_props", models.JSONField(default=dict)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "inbox", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="board_inbox", + to="db.inbox", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + ], + options={ + "verbose_name": "Deploy Board", + "verbose_name_plural": "Deploy Boards", + "db_table": "deploy_boards", + "ordering": ("-created_at",), + "unique_together": {("entity_name", "entity_identifier")}, + }, + ), + migrations.AddField( + model_name="estimate", + name="last_used", + field=models.BooleanField(default=False), + ), + # Rename the existing field + migrations.RenameField( + model_name="issue", + old_name="estimate_point", + new_name="point", + ), + # Add a new field with the original name as a foreign key + migrations.AddField( + model_name="issue", + name="estimate_point", + field=models.ForeignKey( + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_estimates", + to="db.EstimatePoint", + blank=True, + null=True, + ), + ), + migrations.AlterField( + model_name="estimate", + name="type", + field=models.CharField(default="categories", max_length=255), + ), + migrations.AlterField( + model_name="estimatepoint", + name="value", + field=models.CharField(max_length=255), + ), + migrations.RunPython(issue_estimate_point), + migrations.RunPython(last_used_estimate), + migrations.RunPython(populate_deploy_board), + ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index b11ce7aa3..51b0e70e5 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -4,6 +4,7 @@ from .asset import FileAsset from .base import BaseModel from .cycle import Cycle, CycleFavorite, CycleIssue, CycleUserProperties from .dashboard import Dashboard, DashboardWidget, Widget +from .deploy_board import DeployBoard from .estimate import Estimate, EstimatePoint from .exporter import ExporterHistory from .importer import Importer @@ -53,13 +54,13 @@ from .page import Page, PageFavorite, PageLabel, PageLog from .project import ( Project, ProjectBaseModel, - ProjectDeployBoard, ProjectFavorite, ProjectIdentifier, ProjectMember, ProjectMemberInvite, ProjectPublicMember, ) +from .deploy_board import DeployBoard from .session import Session from .social_connection import SocialLoginConnection from .state import State diff --git a/apiserver/plane/db/models/deploy_board.py b/apiserver/plane/db/models/deploy_board.py new file mode 100644 index 000000000..41ffbc7c1 --- /dev/null +++ b/apiserver/plane/db/models/deploy_board.py @@ -0,0 +1,53 @@ +# Python imports +from uuid import uuid4 + +# Django imports +from django.db import models + +# Module imports +from .workspace import WorkspaceBaseModel + + +def get_anchor(): + return uuid4().hex + + +class DeployBoard(WorkspaceBaseModel): + TYPE_CHOICES = ( + ("project", "Project"), + ("issue", "Issue"), + ("module", "Module"), + ("cycle", "Task"), + ("page", "Page"), + ("view", "View"), + ) + + entity_identifier = models.UUIDField(null=True) + entity_name = models.CharField( + max_length=30, + choices=TYPE_CHOICES, + ) + anchor = models.CharField( + max_length=255, default=get_anchor, unique=True, db_index=True + ) + is_comments_enabled = models.BooleanField(default=False) + is_reactions_enabled = models.BooleanField(default=False) + inbox = models.ForeignKey( + "db.Inbox", + related_name="board_inbox", + on_delete=models.SET_NULL, + null=True, + ) + is_votes_enabled = models.BooleanField(default=False) + view_props = models.JSONField(default=dict) + + def __str__(self): + """Return name of the deploy board""" + return f"{self.entity_identifier} <{self.entity_name}>" + + class Meta: + unique_together = ["entity_name", "entity_identifier"] + verbose_name = "Deploy Board" + verbose_name_plural = "Deploy Boards" + db_table = "deploy_boards" + ordering = ("-created_at",) diff --git a/apiserver/plane/db/models/estimate.py b/apiserver/plane/db/models/estimate.py index 6ff1186c3..0713d774f 100644 --- a/apiserver/plane/db/models/estimate.py +++ b/apiserver/plane/db/models/estimate.py @@ -11,7 +11,8 @@ class Estimate(ProjectBaseModel): description = models.TextField( verbose_name="Estimate Description", blank=True ) - type = models.CharField(max_length=255, default="Categories") + type = models.CharField(max_length=255, default="categories") + last_used = models.BooleanField(default=False) def __str__(self): """Return name of the estimate""" @@ -35,7 +36,7 @@ class EstimatePoint(ProjectBaseModel): default=0, validators=[MinValueValidator(0), MaxValueValidator(12)] ) description = models.TextField(blank=True) - value = models.CharField(max_length=20) + value = models.CharField(max_length=255) def __str__(self): """Return name of the estimate""" diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 527597ddc..2b07bd77b 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -119,11 +119,18 @@ class Issue(ProjectBaseModel): blank=True, related_name="state_issue", ) - estimate_point = models.IntegerField( + point = models.IntegerField( validators=[MinValueValidator(0), MaxValueValidator(12)], null=True, blank=True, ) + estimate_point = models.ForeignKey( + "db.EstimatePoint", + on_delete=models.SET_NULL, + related_name="issue_estimates", + null=True, + blank=True, + ) name = models.CharField(max_length=255, verbose_name="Issue Name") description = models.JSONField(blank=True, default=dict) description_html = models.TextField(blank=True, default="

") diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 49fca1323..ba8dbf580 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -260,6 +260,8 @@ def get_default_views(): } +# DEPRECATED TODO: +# used to get the old anchors for the project deploy boards class ProjectDeployBoard(ProjectBaseModel): anchor = models.CharField( max_length=255, default=get_anchor, unique=True, db_index=True diff --git a/apiserver/plane/space/urls/inbox.py b/apiserver/plane/space/urls/inbox.py index 60de040e2..20ebb3437 100644 --- a/apiserver/plane/space/urls/inbox.py +++ b/apiserver/plane/space/urls/inbox.py @@ -10,7 +10,7 @@ from plane.space.views import ( urlpatterns = [ path( - "workspaces//project-boards//inboxes//inbox-issues/", + "anchor//inboxes//inbox-issues/", InboxIssuePublicViewSet.as_view( { "get": "list", @@ -20,7 +20,7 @@ urlpatterns = [ name="inbox-issue", ), path( - "workspaces//project-boards//inboxes//inbox-issues//", + "anchor//inboxes//inbox-issues//", InboxIssuePublicViewSet.as_view( { "get": "retrieve", @@ -31,7 +31,7 @@ urlpatterns = [ name="inbox-issue", ), path( - "workspaces//project-boards//issues//votes/", + "anchor//issues//votes/", IssueVotePublicViewSet.as_view( { "get": "list", diff --git a/apiserver/plane/space/urls/issue.py b/apiserver/plane/space/urls/issue.py index 099eace5d..61c19ba01 100644 --- a/apiserver/plane/space/urls/issue.py +++ b/apiserver/plane/space/urls/issue.py @@ -10,12 +10,12 @@ from plane.space.views import ( urlpatterns = [ path( - "workspaces//project-boards//issues//", + "anchor//issues//", IssueRetrievePublicEndpoint.as_view(), name="workspace-project-boards", ), path( - "workspaces//project-boards//issues//comments/", + "anchor//issues//comments/", IssueCommentPublicViewSet.as_view( { "get": "list", @@ -25,7 +25,7 @@ urlpatterns = [ name="issue-comments-project-board", ), path( - "workspaces//project-boards//issues//comments//", + "anchor//issues//comments//", IssueCommentPublicViewSet.as_view( { "get": "retrieve", @@ -36,7 +36,7 @@ urlpatterns = [ name="issue-comments-project-board", ), path( - "workspaces//project-boards//issues//reactions/", + "anchor//issues//reactions/", IssueReactionPublicViewSet.as_view( { "get": "list", @@ -46,7 +46,7 @@ urlpatterns = [ name="issue-reactions-project-board", ), path( - "workspaces//project-boards//issues//reactions//", + "anchor//issues//reactions//", IssueReactionPublicViewSet.as_view( { "delete": "destroy", @@ -55,7 +55,7 @@ urlpatterns = [ name="issue-reactions-project-board", ), path( - "workspaces//project-boards//comments//reactions/", + "anchor//comments//reactions/", CommentReactionPublicViewSet.as_view( { "get": "list", @@ -65,7 +65,7 @@ urlpatterns = [ name="comment-reactions-project-board", ), path( - "workspaces//project-boards//comments//reactions//", + "anchor//comments//reactions//", CommentReactionPublicViewSet.as_view( { "delete": "destroy", diff --git a/apiserver/plane/space/urls/project.py b/apiserver/plane/space/urls/project.py index dc97b43a7..3294b01f6 100644 --- a/apiserver/plane/space/urls/project.py +++ b/apiserver/plane/space/urls/project.py @@ -4,17 +4,23 @@ from django.urls import path from plane.space.views import ( ProjectDeployBoardPublicSettingsEndpoint, ProjectIssuesPublicEndpoint, + WorkspaceProjectAnchorEndpoint, ) urlpatterns = [ path( - "workspaces//project-boards//settings/", + "anchor//settings/", ProjectDeployBoardPublicSettingsEndpoint.as_view(), name="project-deploy-board-settings", ), path( - "workspaces//project-boards//issues/", + "anchor//issues/", ProjectIssuesPublicEndpoint.as_view(), name="project-deploy-board", ), + path( + "workspaces//projects//anchor/", + WorkspaceProjectAnchorEndpoint.as_view(), + name="project-deploy-board", + ), ] diff --git a/apiserver/plane/space/views/__init__.py b/apiserver/plane/space/views/__init__.py index 5130e04d5..eced7d1b4 100644 --- a/apiserver/plane/space/views/__init__.py +++ b/apiserver/plane/space/views/__init__.py @@ -1,6 +1,7 @@ from .project import ( ProjectDeployBoardPublicSettingsEndpoint, WorkspaceProjectDeployBoardEndpoint, + WorkspaceProjectAnchorEndpoint, ) from .issue import ( diff --git a/apiserver/plane/space/views/inbox.py b/apiserver/plane/space/views/inbox.py index 9f681c160..b89c77672 100644 --- a/apiserver/plane/space/views/inbox.py +++ b/apiserver/plane/space/views/inbox.py @@ -18,7 +18,7 @@ from plane.db.models import ( State, IssueLink, IssueAttachment, - ProjectDeployBoard, + DeployBoard, ) from plane.app.serializers import ( IssueSerializer, @@ -39,7 +39,7 @@ class InboxIssuePublicViewSet(BaseViewSet): ] def get_queryset(self): - project_deploy_board = ProjectDeployBoard.objects.get( + project_deploy_board = DeployBoard.objects.get( workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"), ) @@ -58,9 +58,9 @@ class InboxIssuePublicViewSet(BaseViewSet): ) return InboxIssue.objects.none() - def list(self, request, slug, project_id, inbox_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def list(self, request, anchor, inbox_id): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) if project_deploy_board.inbox is None: return Response( @@ -72,8 +72,8 @@ class InboxIssuePublicViewSet(BaseViewSet): issues = ( Issue.objects.filter( issue_inbox__inbox_id=inbox_id, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, ) .filter(**filters) .annotate(bridge_id=F("issue_inbox__id")) @@ -117,9 +117,9 @@ class InboxIssuePublicViewSet(BaseViewSet): status=status.HTTP_200_OK, ) - def create(self, request, slug, project_id, inbox_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def create(self, request, anchor, inbox_id): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) if project_deploy_board.inbox is None: return Response( @@ -151,7 +151,7 @@ class InboxIssuePublicViewSet(BaseViewSet): name="Triage", group="backlog", description="Default state for managing all Inbox Issues", - project_id=project_id, + project_id=project_deploy_board.project_id, color="#ff7700", ) @@ -163,7 +163,7 @@ class InboxIssuePublicViewSet(BaseViewSet): "description_html", "

" ), priority=request.data.get("issue", {}).get("priority", "low"), - project_id=project_id, + project_id=project_deploy_board.project_id, state=state, ) @@ -173,14 +173,14 @@ class InboxIssuePublicViewSet(BaseViewSet): requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), actor_id=str(request.user.id), issue_id=str(issue.id), - project_id=str(project_id), + project_id=str(project_deploy_board.project_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) # create an inbox issue InboxIssue.objects.create( inbox_id=inbox_id, - project_id=project_id, + project_id=project_deploy_board.project_id, issue=issue, source=request.data.get("source", "in-app"), ) @@ -188,9 +188,9 @@ class InboxIssuePublicViewSet(BaseViewSet): serializer = IssueStateInboxSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) - def partial_update(self, request, slug, project_id, inbox_id, pk): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def partial_update(self, request, anchor, inbox_id, pk): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) if project_deploy_board.inbox is None: return Response( @@ -200,8 +200,8 @@ class InboxIssuePublicViewSet(BaseViewSet): inbox_issue = InboxIssue.objects.get( pk=pk, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, inbox_id=inbox_id, ) # Get the project member @@ -216,8 +216,8 @@ class InboxIssuePublicViewSet(BaseViewSet): issue = Issue.objects.get( pk=inbox_issue.issue_id, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, ) # viewers and guests since only viewers and guests issue_data = { @@ -242,7 +242,7 @@ class InboxIssuePublicViewSet(BaseViewSet): requested_data=requested_data, actor_id=str(request.user.id), issue_id=str(issue.id), - project_id=str(project_id), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( IssueSerializer(current_instance).data, cls=DjangoJSONEncoder, @@ -255,9 +255,9 @@ class InboxIssuePublicViewSet(BaseViewSet): issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - def retrieve(self, request, slug, project_id, inbox_id, pk): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def retrieve(self, request, anchor, inbox_id, pk): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) if project_deploy_board.inbox is None: return Response( @@ -267,21 +267,21 @@ class InboxIssuePublicViewSet(BaseViewSet): inbox_issue = InboxIssue.objects.get( pk=pk, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, inbox_id=inbox_id, ) issue = Issue.objects.get( pk=inbox_issue.issue_id, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, ) serializer = IssueStateInboxSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) - def destroy(self, request, slug, project_id, inbox_id, pk): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def destroy(self, request, anchor, inbox_id, pk): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) if project_deploy_board.inbox is None: return Response( @@ -291,8 +291,8 @@ class InboxIssuePublicViewSet(BaseViewSet): inbox_issue = InboxIssue.objects.get( pk=pk, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, inbox_id=inbox_id, ) diff --git a/apiserver/plane/space/views/issue.py b/apiserver/plane/space/views/issue.py index 8c4d6e150..71ba0f6a7 100644 --- a/apiserver/plane/space/views/issue.py +++ b/apiserver/plane/space/views/issue.py @@ -44,7 +44,7 @@ from plane.db.models import ( ProjectMember, IssueReaction, CommentReaction, - ProjectDeployBoard, + DeployBoard, IssueVote, ProjectPublicMember, ) @@ -76,15 +76,15 @@ class IssueCommentPublicViewSet(BaseViewSet): def get_queryset(self): try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), + project_deploy_board = DeployBoard.objects.get( + anchor=self.kwargs.get("anchor"), + entity_name="project", ) - if project_deploy_board.comments: + if project_deploy_board.is_comments_enabled: return self.filter_queryset( super() .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) + .filter(workspace_id=project_deploy_board.workspace_id) .filter(issue_id=self.kwargs.get("issue_id")) .filter(access="EXTERNAL") .select_related("project") @@ -93,8 +93,8 @@ class IssueCommentPublicViewSet(BaseViewSet): .annotate( is_member=Exists( ProjectMember.objects.filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, member_id=self.request.user.id, is_active=True, ) @@ -103,15 +103,15 @@ class IssueCommentPublicViewSet(BaseViewSet): .distinct() ).order_by("created_at") return IssueComment.objects.none() - except ProjectDeployBoard.DoesNotExist: + except DeployBoard.DoesNotExist: return IssueComment.objects.none() - def create(self, request, slug, project_id, issue_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def create(self, request, anchor, issue_id): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) - if not project_deploy_board.comments: + if not project_deploy_board.is_comments_enabled: return Response( {"error": "Comments are not enabled for this project"}, status=status.HTTP_400_BAD_REQUEST, @@ -120,7 +120,7 @@ class IssueCommentPublicViewSet(BaseViewSet): serializer = IssueCommentSerializer(data=request.data) if serializer.is_valid(): serializer.save( - project_id=project_id, + project_id=project_deploy_board.project_id, issue_id=issue_id, actor=request.user, access="EXTERNAL", @@ -132,37 +132,35 @@ class IssueCommentPublicViewSet(BaseViewSet): ), actor_id=str(request.user.id), issue_id=str(issue_id), - project_id=str(project_id), + project_id=str(project_deploy_board.project_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) if not ProjectMember.objects.filter( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, is_active=True, ).exists(): # Add the user for workspace tracking _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def partial_update(self, request, slug, project_id, issue_id, pk): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def partial_update(self, request, anchor, issue_id, pk): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) - if not project_deploy_board.comments: + if not project_deploy_board.is_comments_enabled: return Response( {"error": "Comments are not enabled for this project"}, status=status.HTTP_400_BAD_REQUEST, ) - comment = IssueComment.objects.get( - workspace__slug=slug, pk=pk, actor=request.user - ) + comment = IssueComment.objects.get(pk=pk, actor=request.user) serializer = IssueCommentSerializer( comment, data=request.data, partial=True ) @@ -173,7 +171,7 @@ class IssueCommentPublicViewSet(BaseViewSet): requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), actor_id=str(request.user.id), issue_id=str(issue_id), - project_id=str(project_id), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( IssueCommentSerializer(comment).data, cls=DjangoJSONEncoder, @@ -183,20 +181,18 @@ class IssueCommentPublicViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, slug, project_id, issue_id, pk): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def destroy(self, request, anchor, issue_id, pk): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) - if not project_deploy_board.comments: + if not project_deploy_board.is_comments_enabled: return Response( {"error": "Comments are not enabled for this project"}, status=status.HTTP_400_BAD_REQUEST, ) comment = IssueComment.objects.get( - workspace__slug=slug, pk=pk, - project_id=project_id, actor=request.user, ) issue_activity.delay( @@ -204,7 +200,7 @@ class IssueCommentPublicViewSet(BaseViewSet): requested_data=json.dumps({"comment_id": str(pk)}), actor_id=str(request.user.id), issue_id=str(issue_id), - project_id=str(project_id), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( IssueCommentSerializer(comment).data, cls=DjangoJSONEncoder, @@ -221,11 +217,11 @@ class IssueReactionPublicViewSet(BaseViewSet): def get_queryset(self): try: - project_deploy_board = ProjectDeployBoard.objects.get( + project_deploy_board = DeployBoard.objects.get( workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"), ) - if project_deploy_board.reactions: + if project_deploy_board.is_reactions_enabled: return ( super() .get_queryset() @@ -236,15 +232,15 @@ class IssueReactionPublicViewSet(BaseViewSet): .distinct() ) return IssueReaction.objects.none() - except ProjectDeployBoard.DoesNotExist: + except DeployBoard.DoesNotExist: return IssueReaction.objects.none() - def create(self, request, slug, project_id, issue_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def create(self, request, anchor, issue_id): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) - if not project_deploy_board.reactions: + if not project_deploy_board.is_reactions_enabled: return Response( {"error": "Reactions are not enabled for this project board"}, status=status.HTTP_400_BAD_REQUEST, @@ -253,16 +249,18 @@ class IssueReactionPublicViewSet(BaseViewSet): serializer = IssueReactionSerializer(data=request.data) if serializer.is_valid(): serializer.save( - project_id=project_id, issue_id=issue_id, actor=request.user + project_id=project_deploy_board.project_id, + issue_id=issue_id, + actor=request.user, ) if not ProjectMember.objects.filter( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, is_active=True, ).exists(): # Add the user for workspace tracking _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, ) issue_activity.delay( @@ -272,25 +270,25 @@ class IssueReactionPublicViewSet(BaseViewSet): ), actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), + project_id=str(project_deploy_board.project_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, slug, project_id, issue_id, reaction_code): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def destroy(self, request, anchor, issue_id, reaction_code): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) - if not project_deploy_board.reactions: + if not project_deploy_board.is_reactions_enabled: return Response( {"error": "Reactions are not enabled for this project board"}, status=status.HTTP_400_BAD_REQUEST, ) issue_reaction = IssueReaction.objects.get( - workspace__slug=slug, + workspace_id=project_deploy_board.workspace_id, issue_id=issue_id, reaction=reaction_code, actor=request.user, @@ -300,7 +298,7 @@ class IssueReactionPublicViewSet(BaseViewSet): requested_data=None, actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( { "reaction": str(reaction_code), @@ -319,30 +317,29 @@ class CommentReactionPublicViewSet(BaseViewSet): def get_queryset(self): try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), + project_deploy_board = DeployBoard.objects.get( + anchor=self.kwargs.get("anchor"), entity_name="project" ) - if project_deploy_board.reactions: + if project_deploy_board.is_reactions_enabled: return ( super() .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) + .filter(workspace_id=project_deploy_board.workspace_id) + .filter(project_id=project_deploy_board.project_id) .filter(comment_id=self.kwargs.get("comment_id")) .order_by("-created_at") .distinct() ) return CommentReaction.objects.none() - except ProjectDeployBoard.DoesNotExist: + except DeployBoard.DoesNotExist: return CommentReaction.objects.none() - def create(self, request, slug, project_id, comment_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def create(self, request, anchor, comment_id): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) - if not project_deploy_board.reactions: + if not project_deploy_board.is_reactions_enabled: return Response( {"error": "Reactions are not enabled for this board"}, status=status.HTTP_400_BAD_REQUEST, @@ -351,18 +348,18 @@ class CommentReactionPublicViewSet(BaseViewSet): serializer = CommentReactionSerializer(data=request.data) if serializer.is_valid(): serializer.save( - project_id=project_id, + project_id=project_deploy_board.project_id, comment_id=comment_id, actor=request.user, ) if not ProjectMember.objects.filter( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, is_active=True, ).exists(): # Add the user for workspace tracking _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, ) issue_activity.delay( @@ -379,19 +376,19 @@ class CommentReactionPublicViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, slug, project_id, comment_id, reaction_code): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def destroy(self, request, anchor, comment_id, reaction_code): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) - if not project_deploy_board.reactions: + if not project_deploy_board.is_reactions_enabled: return Response( {"error": "Reactions are not enabled for this board"}, status=status.HTTP_400_BAD_REQUEST, ) comment_reaction = CommentReaction.objects.get( - project_id=project_id, - workspace__slug=slug, + project_id=project_deploy_board.project_id, + workspace_id=project_deploy_board.workspace_id, comment_id=comment_id, reaction=reaction_code, actor=request.user, @@ -401,7 +398,7 @@ class CommentReactionPublicViewSet(BaseViewSet): requested_data=None, actor_id=str(self.request.user.id), issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( { "reaction": str(reaction_code), @@ -421,36 +418,42 @@ class IssueVotePublicViewSet(BaseViewSet): def get_queryset(self): try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), + project_deploy_board = DeployBoard.objects.get( + workspace__slug=self.kwargs.get("anchor"), + entity_name="project", ) - if project_deploy_board.votes: + if project_deploy_board.is_votes_enabled: return ( super() .get_queryset() .filter(issue_id=self.kwargs.get("issue_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) + .filter(workspace_id=project_deploy_board.workspace_id) + .filter(project_id=project_deploy_board.project_id) ) return IssueVote.objects.none() - except ProjectDeployBoard.DoesNotExist: + except DeployBoard.DoesNotExist: return IssueVote.objects.none() - def create(self, request, slug, project_id, issue_id): + def create(self, request, anchor, issue_id): + print("hite") + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" + ) + print("awer") issue_vote, _ = IssueVote.objects.get_or_create( actor_id=request.user.id, - project_id=project_id, + project_id=project_deploy_board.project_id, issue_id=issue_id, ) + print("AWer") # Add the user for workspace tracking if not ProjectMember.objects.filter( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, is_active=True, ).exists(): _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, ) issue_vote.vote = request.data.get("vote", 1) @@ -462,26 +465,29 @@ class IssueVotePublicViewSet(BaseViewSet): ), actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), + project_id=str(project_deploy_board.project_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) serializer = IssueVoteSerializer(issue_vote) return Response(serializer.data, status=status.HTTP_201_CREATED) - def destroy(self, request, slug, project_id, issue_id): + def destroy(self, request, anchor, issue_id): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" + ) issue_vote = IssueVote.objects.get( - workspace__slug=slug, - project_id=project_id, issue_id=issue_id, actor_id=request.user.id, + project_id=project_deploy_board.project_id, + workspace_id=project_deploy_board.workspace_id, ) issue_activity.delay( type="issue_vote.activity.deleted", requested_data=None, actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( { "vote": str(issue_vote.vote), @@ -499,9 +505,14 @@ class IssueRetrievePublicEndpoint(BaseAPIView): AllowAny, ] - def get(self, request, slug, project_id, issue_id): + def get(self, request, anchor, issue_id): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" + ) issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=issue_id + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, + pk=issue_id, ) serializer = IssuePublicSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) @@ -512,14 +523,17 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): AllowAny, ] - def get(self, request, slug, project_id): - if not ProjectDeployBoard.objects.filter( - workspace__slug=slug, project_id=project_id + def get(self, request, anchor): + if not DeployBoard.objects.filter( + anchor=anchor, entity_name="project" ).exists(): return Response( {"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND, ) + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" + ) filters = issue_filters(request.query_params, "GET") @@ -544,8 +558,8 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .filter(project_id=project_id) - .filter(workspace__slug=slug) + .filter(project_id=project_deploy_board.project_id) + .filter(workspace_id=project_deploy_board.workspace_id) .select_related("project", "workspace", "state", "parent") .prefetch_related("assignees", "labels") .prefetch_related( @@ -652,8 +666,8 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): states = ( State.objects.filter( ~Q(name="Triage"), - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, ) .annotate( custom_order=Case( @@ -670,7 +684,8 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): ) labels = Label.objects.filter( - workspace__slug=slug, project_id=project_id + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, ).values("id", "name", "color", "parent") ## Grouping the results diff --git a/apiserver/plane/space/views/project.py b/apiserver/plane/space/views/project.py index 10a3c3879..76f1600ee 100644 --- a/apiserver/plane/space/views/project.py +++ b/apiserver/plane/space/views/project.py @@ -11,10 +11,10 @@ from rest_framework.permissions import AllowAny # Module imports from .base import BaseAPIView -from plane.app.serializers import ProjectDeployBoardSerializer +from plane.app.serializers import DeployBoardSerializer from plane.db.models import ( Project, - ProjectDeployBoard, + DeployBoard, ) @@ -23,11 +23,11 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): AllowAny, ] - def get(self, request, slug, project_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + def get(self, request, anchor): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" ) - serializer = ProjectDeployBoardSerializer(project_deploy_board) + serializer = DeployBoardSerializer(project_deploy_board) return Response(serializer.data, status=status.HTTP_200_OK) @@ -36,13 +36,16 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): AllowAny, ] - def get(self, request, slug): + def get(self, request, anchor): + deploy_board = DeployBoard.objects.filter(anchor=anchor, entity_name="project").values_list projects = ( - Project.objects.filter(workspace__slug=slug) + Project.objects.filter(workspace=deploy_board.workspace) .annotate( is_public=Exists( - ProjectDeployBoard.objects.filter( - workspace__slug=slug, project_id=OuterRef("pk") + DeployBoard.objects.filter( + anchor=anchor, + project_id=OuterRef("pk"), + entity_name="project", ) ) ) @@ -58,3 +61,16 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): ) return Response(projects, status=status.HTTP_200_OK) + + +class WorkspaceProjectAnchorEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id): + project_deploy_board = DeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + serializer = DeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index cd57690c6..59ddbd933 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -4,18 +4,28 @@ from itertools import groupby # Django import from django.db import models -from django.db.models import Case, CharField, Count, F, Sum, Value, When +from django.db.models import ( + Case, + CharField, + Count, + F, + Sum, + Value, + When, + IntegerField, +) from django.db.models.functions import ( Coalesce, Concat, ExtractMonth, ExtractYear, TruncDate, + Cast, ) from django.utils import timezone # Module imports -from plane.db.models import Issue +from plane.db.models import Issue, Project def annotate_with_monthly_dimension(queryset, field_name, attribute): @@ -87,9 +97,9 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None): # Estimate else: - queryset = queryset.annotate(estimate=Sum("estimate_point")).order_by( - x_axis - ) + queryset = queryset.annotate( + estimate=Sum(Cast("estimate_point__value", IntegerField())) + ).order_by(x_axis) queryset = ( queryset.annotate(segment=F(segment)) if segment else queryset ) @@ -110,9 +120,33 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None): return sort_data(grouped_data, temp_axis) -def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): +def burndown_plot( + queryset, + slug, + project_id, + plot_type, + cycle_id=None, + module_id=None, +): # Total Issues in Cycle or Module total_issues = queryset.total_issues + # check whether the estimate is a point or not + estimate_type = Project.objects.filter( + workspace__slug=slug, + pk=project_id, + estimate__isnull=False, + estimate__type="points", + ).exists() + if estimate_type and plot_type == "points": + issue_estimates = Issue.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_cycle__cycle_id=cycle_id, + estimate_point__isnull=False, + ).values_list("estimate_point__value", flat=True) + + issue_estimates = [int(value) for value in issue_estimates] + total_estimate_points = sum(issue_estimates) if cycle_id: if queryset.end_date and queryset.start_date: @@ -128,18 +162,32 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): chart_data = {str(date): 0 for date in date_range} - completed_issues_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_cycle__cycle_id=cycle_id, + if plot_type == "points": + completed_issues_estimate_point_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_cycle__cycle_id=cycle_id, + estimate_point__isnull=False, + ) + .annotate(date=TruncDate("completed_at")) + .values("date") + .values("date", "estimate_point__value") + .order_by("date") + ) + else: + completed_issues_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_cycle__cycle_id=cycle_id, + ) + .annotate(date=TruncDate("completed_at")) + .values("date") + .annotate(total_completed=Count("id")) + .values("date", "total_completed") + .order_by("date") ) - .annotate(date=TruncDate("completed_at")) - .values("date") - .annotate(total_completed=Count("id")) - .values("date", "total_completed") - .order_by("date") - ) if module_id: # Get all dates between the two dates @@ -152,31 +200,59 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): chart_data = {str(date): 0 for date in date_range} - completed_issues_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_module__module_id=module_id, + if plot_type == "points": + completed_issues_estimate_point_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_module__module_id=module_id, + estimate_point__isnull=False, + ) + .annotate(date=TruncDate("completed_at")) + .values("date") + .values("date", "estimate_point__value") + .order_by("date") + ) + else: + completed_issues_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_module__module_id=module_id, + ) + .annotate(date=TruncDate("completed_at")) + .values("date") + .annotate(total_completed=Count("id")) + .values("date", "total_completed") + .order_by("date") ) - .annotate(date=TruncDate("completed_at")) - .values("date") - .annotate(total_completed=Count("id")) - .values("date", "total_completed") - .order_by("date") - ) for date in date_range: - cumulative_pending_issues = total_issues - total_completed = 0 - total_completed = sum( - item["total_completed"] - for item in completed_issues_distribution - if item["date"] is not None and item["date"] <= date - ) - cumulative_pending_issues -= total_completed - if date > timezone.now().date(): - chart_data[str(date)] = None + if plot_type == "points": + cumulative_pending_issues = total_estimate_points + total_completed = 0 + total_completed = sum( + int(item["estimate_point__value"]) + for item in completed_issues_estimate_point_distribution + if item["date"] is not None and item["date"] <= date + ) + cumulative_pending_issues -= total_completed + if date > timezone.now().date(): + chart_data[str(date)] = None + else: + chart_data[str(date)] = cumulative_pending_issues else: - chart_data[str(date)] = cumulative_pending_issues + cumulative_pending_issues = total_issues + total_completed = 0 + total_completed = sum( + item["total_completed"] + for item in completed_issues_distribution + if item["date"] is not None and item["date"] <= date + ) + cumulative_pending_issues -= total_completed + if date > timezone.now().date(): + chart_data[str(date)] = None + else: + chart_data[str(date)] = cumulative_pending_issues return chart_data diff --git a/packages/types/src/analytics.d.ts b/packages/types/src/analytics.d.ts index 35da4b723..2fb7ad51a 100644 --- a/packages/types/src/analytics.d.ts +++ b/packages/types/src/analytics.d.ts @@ -54,7 +54,7 @@ export type TXAxisValues = | "state__group" | "labels__id" | "assignees__id" - | "estimate_point" + | "estimate_point__value" | "issue_cycle__cycle_id" | "issue_module__module_id" | "priority" diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index a4d098506..cc8575374 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -24,3 +24,16 @@ export enum EIssueCommentAccessSpecifier { EXTERNAL = "EXTERNAL", INTERNAL = "INTERNAL", } + +// estimates +export enum EEstimateSystem { + POINTS = "points", + CATEGORIES = "categories", + TIME = "time", +} + +export enum EEstimateUpdateStages { + CREATE = "create", + EDIT = "edit", + SWITCH = "switch", +} diff --git a/packages/types/src/estimate.d.ts b/packages/types/src/estimate.d.ts index 96b584ce1..9bad7e260 100644 --- a/packages/types/src/estimate.d.ts +++ b/packages/types/src/estimate.d.ts @@ -1,40 +1,77 @@ -export interface IEstimate { - created_at: Date; - created_by: string; - description: string; - id: string; - name: string; - project: string; - project_detail: IProject; - updated_at: Date; - updated_by: string; - points: IEstimatePoint[]; - workspace: string; - workspace_detail: IWorkspace; -} +import { EEstimateSystem, EEstimateUpdateStages } from "./enums"; export interface IEstimatePoint { - created_at: string; - created_by: string; - description: string; - estimate: string; - id: string; - key: number; - project: string; - updated_at: string; - updated_by: string; - value: string; - workspace: string; + id: string | undefined; + key: number | undefined; + value: string | undefined; + description: string | undefined; + workspace: string | undefined; + project: string | undefined; + estimate: string | undefined; + created_at: Date | undefined; + updated_at: Date | undefined; + created_by: string | undefined; + updated_by: string | undefined; +} + +export type TEstimateSystemKeys = + | EEstimateSystem.POINTS + | EEstimateSystem.CATEGORIES + | EEstimateSystem.TIME; + +export interface IEstimate { + id: string | undefined; + name: string | undefined; + description: string | undefined; + type: TEstimateSystemKeys | undefined; // categories, points, time + points: IEstimatePoint[] | undefined; + workspace: string | undefined; + project: string | undefined; + last_used: boolean | undefined; + created_at: Date | undefined; + updated_at: Date | undefined; + created_by: string | undefined; + updated_by: string | undefined; } export interface IEstimateFormData { - estimate: { - name: string; - description: string; + estimate?: { + name?: string; + type?: string; + last_used?: boolean; }; estimate_points: { - id?: string; + id?: string | undefined; key: number; value: string; }[]; } + +export type TEstimatePointsObject = { + id?: string | undefined; + key: number; + value: string; +}; + +export type TTemplateValues = { + title: string; + values: TEstimatePointsObject[]; + hide?: boolean; +}; + +export type TEstimateSystem = { + name: string; + templates: Record; + is_available: boolean; + is_ee: boolean; +}; + +export type TEstimateSystems = { + [K in TEstimateSystemKeys]: TEstimateSystem; +}; + +// update estimates +export type TEstimateUpdateStageKeys = + | EEstimateUpdateStages.CREATE + | EEstimateUpdateStages.EDIT + | EEstimateUpdateStages.SWITCH; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index b8dd2d3c1..25c2b255b 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -15,7 +15,6 @@ export * from "./importer"; export * from "./inbox"; export * from "./analytics"; export * from "./api_token"; -export * from "./app"; export * from "./auth"; export * from "./calendar"; export * from "./instance"; @@ -28,3 +27,4 @@ export * from "./webhook"; export * from "./workspace-views"; export * from "./common"; export * from "./pragmatic"; +export * from "./publish"; diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index 42c95dc4e..990b308e7 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -15,7 +15,7 @@ export type TIssue = { priority: TIssuePriorities; label_ids: string[]; assignee_ids: string[]; - estimate_point: number | null; + estimate_point: string | null; sub_issues_count: number; attachment_count: number; diff --git a/packages/types/src/module/modules.d.ts b/packages/types/src/module/modules.d.ts index 0019781ba..53be14347 100644 --- a/packages/types/src/module/modules.d.ts +++ b/packages/types/src/module/modules.d.ts @@ -44,6 +44,8 @@ export interface IModule { target_date: string | null; total_issues: number; unstarted_issues: number; + total_estimate_points?: number; + completed_estimate_points?: number; updated_at: string; updated_by?: string; archived_at: string | null; diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index ee974fd63..59ccf73b6 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -32,7 +32,7 @@ export interface IProject { estimate: string | null; id: string; identifier: string; - is_deployed: boolean; + anchor: string | null; is_favorite: boolean; is_member: boolean; logo_props: TLogoProps; diff --git a/packages/types/src/publish.d.ts b/packages/types/src/publish.d.ts new file mode 100644 index 000000000..883ef8dd6 --- /dev/null +++ b/packages/types/src/publish.d.ts @@ -0,0 +1,41 @@ +import { IProject, IProjectLite, IWorkspaceLite } from "@plane/types"; + +export type TPublishEntityType = "project"; + +export type TProjectPublishLayouts = + | "calendar" + | "gantt" + | "kanban" + | "list" + | "spreadsheet"; + +export type TPublishViewProps = { + calendar?: boolean; + gantt?: boolean; + kanban?: boolean; + list?: boolean; + spreadsheet?: boolean; +}; + +export type TProjectDetails = IProjectLite & + Pick; + +export type TPublishSettings = { + anchor: string | undefined; + is_comments_enabled: boolean; + created_at: string | undefined; + created_by: string | undefined; + entity_identifier: string | undefined; + entity_name: TPublishEntityType | undefined; + id: string | undefined; + inbox: unknown; + project: string | undefined; + project_details: TProjectDetails | undefined; + is_reactions_enabled: boolean; + updated_at: string | undefined; + updated_by: string | undefined; + view_props: TViewProps | undefined; + is_votes_enabled: boolean; + workspace: string | undefined; + workspace_detail: IWorkspaceLite | undefined; +}; diff --git a/packages/ui/package.json b/packages/ui/package.json index 84a655bf7..ed0932339 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -24,7 +24,7 @@ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@blueprintjs/core": "^4.16.3", "@blueprintjs/popover2": "^1.13.3", - "@headlessui/react": "^1.7.17", + "@headlessui/react": "^2.0.3", "@popperjs/core": "^2.11.8", "clsx": "^2.0.0", "emoji-picker-react": "^4.5.16", @@ -33,7 +33,7 @@ "react-color": "^2.19.3", "react-dom": "^18.2.0", "react-popper": "^2.3.0", - "sonner": "^1.4.2", + "sonner": "^1.4.41", "tailwind-merge": "^2.0.0" }, "devDependencies": { diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index c3c0cd4f1..05adddfb0 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -14,5 +14,6 @@ export * from "./loader"; export * from "./control-link"; export * from "./toast"; export * from "./drag-handle"; +export * from "./typography"; export * from "./drop-indicator"; export * from "./sortable"; diff --git a/packages/ui/src/sortable/sortable.stories.tsx b/packages/ui/src/sortable/sortable.stories.tsx index 6d40ddc2e..2d469b767 100644 --- a/packages/ui/src/sortable/sortable.stories.tsx +++ b/packages/ui/src/sortable/sortable.stories.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; import React from "react"; -import { Draggable } from "./draggable"; import { Sortable } from "./sortable"; const meta: Meta = { @@ -13,7 +12,7 @@ type Story = StoryObj; const data = [ { id: "1", name: "John Doe" }, - { id: "2", name: "Jane Doe 2" }, + { id: "2", name: "Satish" }, { id: "3", name: "Alice" }, { id: "4", name: "Bob" }, { id: "5", name: "Charlie" }, diff --git a/packages/ui/src/sortable/sortable.tsx b/packages/ui/src/sortable/sortable.tsx index 9d79a8f59..b495d535e 100644 --- a/packages/ui/src/sortable/sortable.tsx +++ b/packages/ui/src/sortable/sortable.tsx @@ -8,7 +8,7 @@ type Props = { onChange: (data: T[]) => void; keyExtractor: (item: T, index: number) => string; containerClassName?: string; - id: string; + id?: string; }; const moveItem = ( @@ -17,7 +17,7 @@ const moveItem = ( destination: T & Record, keyExtractor: (item: T, index: number) => string ) => { - const sourceIndex = data.indexOf(source); + const sourceIndex = data.findIndex((item, index) => keyExtractor(item, index) === keyExtractor(source, 0)); if (sourceIndex === -1) return data; const destinationIndex = data.findIndex((item, index) => keyExtractor(item, index) === keyExtractor(destination, 0)); diff --git a/space/app/[workspaceSlug]/[projectId]/page.ts b/space/app/[workspaceSlug]/[projectId]/page.ts new file mode 100644 index 000000000..4f18e8bd5 --- /dev/null +++ b/space/app/[workspaceSlug]/[projectId]/page.ts @@ -0,0 +1,42 @@ +import { notFound, redirect } from "next/navigation"; +// types +import { TPublishSettings } from "@plane/types"; +// services +import PublishService from "@/services/publish.service"; + +const publishService = new PublishService(); + +type Props = { + params: { + workspaceSlug: string; + projectId: string; + }; + searchParams: any; +}; + +export default async function IssuesPage(props: Props) { + const { params, searchParams } = props; + // query params + const { workspaceSlug, projectId } = params; + const { board, peekId } = searchParams; + + let response: TPublishSettings | undefined = undefined; + try { + response = await publishService.fetchAnchorFromProjectDetails(workspaceSlug, projectId); + } catch (error) { + // redirect to 404 page on error + notFound(); + } + + let url = ""; + if (response?.entity_name === "project") { + url = `/issues/${response?.anchor}`; + const params = new URLSearchParams(); + if (board) params.append("board", board); + if (peekId) params.append("peekId", peekId); + if (params.toString()) url += `?${params.toString()}`; + redirect(url); + } else { + notFound(); + } +} diff --git a/space/app/[workspace_slug]/[project_id]/page.tsx b/space/app/[workspace_slug]/[project_id]/page.tsx deleted file mode 100644 index 0d08ae7eb..000000000 --- a/space/app/[workspace_slug]/[project_id]/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import { useSearchParams } from "next/navigation"; -// components -import { ProjectDetailsView } from "@/components/views"; - -export default function WorkspaceProjectPage({ params }: { params: { workspace_slug: any; project_id: any } }) { - const { workspace_slug, project_id } = params; - - const searchParams = useSearchParams(); - const peekId = searchParams.get("peekId") || undefined; - - if (!workspace_slug || !project_id) return <>; - - return ; -} diff --git a/space/app/error.tsx b/space/app/error.tsx index 2d6f22e90..e47a1af1d 100644 --- a/space/app/error.tsx +++ b/space/app/error.tsx @@ -1,38 +1,47 @@ "use client"; -import Image from "next/image"; -import { useTheme } from "next-themes"; +// ui import { Button } from "@plane/ui"; -// assets -import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg"; -import InstanceFailureImage from "@/public/instance/instance-failure.svg"; - -export default function InstanceError() { - const { resolvedTheme } = useTheme(); - - const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage; +const ErrorPage = () => { const handleRetry = () => { window.location.reload(); }; return ( -
-
-
- Plane instance failure image -

Unable to fetch instance details.

-

- We were unable to fetch the details of the instance.
- Fret not, it might just be a connectivity issue. +

+
+
+

Exception Detected!

+

+ We{"'"}re Sorry! An exception has been detected, and our engineering team has been notified. We apologize + for any inconvenience this may have caused. Please reach out to our engineering team at{" "} + + support@plane.so + {" "} + or on our{" "} + + Discord + {" "} + server for further assistance.

-
- + {/* */}
); -} +}; + +export default ErrorPage; diff --git a/space/app/[workspace_slug]/[project_id]/layout.tsx b/space/app/issues/[anchor]/layout.tsx similarity index 54% rename from space/app/[workspace_slug]/[project_id]/layout.tsx rename to space/app/issues/[anchor]/layout.tsx index b1e134ea6..91291e481 100644 --- a/space/app/[workspace_slug]/[project_id]/layout.tsx +++ b/space/app/issues/[anchor]/layout.tsx @@ -1,25 +1,39 @@ +"use client"; + +import { observer } from "mobx-react-lite"; import Image from "next/image"; -import { notFound } from "next/navigation"; +import useSWR from "swr"; // components -import IssueNavbar from "@/components/issues/navbar"; +import { LogoSpinner } from "@/components/common"; +import { IssuesNavbarRoot } from "@/components/issues"; +// hooks +import { usePublish, usePublishList } from "@/hooks/store"; // assets -import planeLogo from "public/plane-logo.svg"; +import planeLogo from "@/public/plane-logo.svg"; -export default async function ProjectLayout({ - children, - params, -}: { +type Props = { children: React.ReactNode; - params: { workspace_slug: string; project_id: string }; -}) { - const { workspace_slug, project_id } = params; + params: { + anchor: string; + }; +}; - if (!workspace_slug || !project_id) notFound(); +const IssuesLayout = observer((props: Props) => { + const { children, params } = props; + // params + const { anchor } = params; + // store hooks + const { fetchPublishSettings } = usePublishList(); + const publishSettings = usePublish(anchor); + // fetch publish settings + useSWR(anchor ? `PUBLISH_SETTINGS_${anchor}` : null, anchor ? () => fetchPublishSettings(anchor) : null); + + if (!publishSettings) return ; return (
- +
{children}
); -} +}); + +export default IssuesLayout; diff --git a/space/app/issues/[anchor]/page.tsx b/space/app/issues/[anchor]/page.tsx new file mode 100644 index 000000000..b3c9353e6 --- /dev/null +++ b/space/app/issues/[anchor]/page.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { observer } from "mobx-react-lite"; +import { useSearchParams } from "next/navigation"; +// components +import { IssuesLayoutsRoot } from "@/components/issues"; +// hooks +import { usePublish } from "@/hooks/store"; + +type Props = { + params: { + anchor: string; + }; +}; + +const IssuesPage = observer((props: Props) => { + const { params } = props; + const { anchor } = params; + // params + const searchParams = useSearchParams(); + const peekId = searchParams.get("peekId") || undefined; + + const publishSettings = usePublish(anchor); + + if (!publishSettings) return null; + + return ; +}); + +export default IssuesPage; diff --git a/space/app/not-found.tsx b/space/app/not-found.tsx index cae576319..c5320b2dc 100644 --- a/space/app/not-found.tsx +++ b/space/app/not-found.tsx @@ -4,20 +4,18 @@ import Image from "next/image"; // assets import UserLoggedInImage from "public/user-logged-in.svg"; -export default function NotFound() { - return ( -
-
-
-
-
- User already logged in -
-
-

Not Found

-

Please enter the appropriate project URL to view the issue board.

+const NotFound = () => ( +
+
+
+
+ User already logged in
+

Not Found

+

Please enter the appropriate project URL to view the issue board.

- ); -} +
+); + +export default NotFound; diff --git a/space/components/account/user-logged-in.tsx b/space/components/account/user-logged-in.tsx index 33be330fa..5975d73b6 100644 --- a/space/components/account/user-logged-in.tsx +++ b/space/components/account/user-logged-in.tsx @@ -1,36 +1,44 @@ "use client"; +import { observer } from "mobx-react-lite"; import Image from "next/image"; +import { useTheme } from "next-themes"; // components -import { UserAvatar } from "@/components/issues/navbar/user-avatar"; +import { UserAvatar } from "@/components/issues"; // hooks import { useUser } from "@/hooks/store"; // assets -import PlaneLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png"; +import PlaneBlackLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png"; +import PlaneWhiteLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png"; import UserLoggedInImage from "@/public/user-logged-in.svg"; -export const UserLoggedIn = () => { +export const UserLoggedIn = observer(() => { + // store hooks const { data: user } = useUser(); + // next-themes + const { resolvedTheme } = useTheme(); + + const logo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo; if (!user) return null; return ( -
+
-
- User already logged in +
+ Plane logo
-
+
-
-
+
+
User already logged in
-

Logged in Successfully!

+

Logged in successfully!

You{"'"}ve successfully logged in. Please enter the appropriate project URL to view the issue board.

@@ -38,4 +46,4 @@ export const UserLoggedIn = () => {
); -}; +}); diff --git a/space/components/common/index.ts b/space/components/common/index.ts index c4ea97f3c..1949c069b 100644 --- a/space/components/common/index.ts +++ b/space/components/common/index.ts @@ -1,3 +1,2 @@ -export * from "./latest-feature-block"; export * from "./project-logo"; export * from "./logo-spinner"; diff --git a/space/components/common/latest-feature-block.tsx b/space/components/common/latest-feature-block.tsx deleted file mode 100644 index c1b5db954..000000000 --- a/space/components/common/latest-feature-block.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; -import { useTheme } from "next-themes"; -// icons -import { Lightbulb } from "lucide-react"; -// images -import latestFeatures from "public/onboarding/onboarding-pages.svg"; - -export const LatestFeatureBlock = () => { - const { resolvedTheme } = useTheme(); - - return ( - <> -
- -

- Pages gets a facelift! Write anything and use Galileo to help you start.{" "} - - Learn more - -

-
-
-
- Plane Issues -
-
- - ); -}; diff --git a/space/components/instance/index.ts b/space/components/instance/index.ts index 6568894f0..be80bc669 100644 --- a/space/components/instance/index.ts +++ b/space/components/instance/index.ts @@ -1,2 +1 @@ -export * from "./not-ready-view"; export * from "./instance-failure-view"; diff --git a/space/components/instance/not-ready-view.tsx b/space/components/instance/not-ready-view.tsx deleted file mode 100644 index be46a9473..000000000 --- a/space/components/instance/not-ready-view.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client"; - -import { FC } from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { useTheme } from "next-themes"; -// ui -import { Button } from "@plane/ui"; -// helper -import { GOD_MODE_URL, SPACE_BASE_PATH } from "@/helpers/common.helper"; -// images -import PlaneTakeOffImage from "@/public/instance/plane-takeoff.png"; -import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg"; -import PlaneBackgroundPattern from "public/auth/background-pattern.svg"; -import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png"; -import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png"; - -export const InstanceNotReady: FC = () => { - const { resolvedTheme } = useTheme(); - const patternBackground = resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern; - - const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo; - - return ( -
-
-
-
- - Plane logo - -
-
- -
- Plane background pattern -
- -
-
-
-
-

Welcome aboard Plane!

- Plane Logo -

- Get started by setting up your instance and workspace -

-
- -
-
-
-
-
- ); -}; diff --git a/space/components/issues/board-views/block-downvotes.tsx b/space/components/issues/board-views/block-downvotes.tsx deleted file mode 100644 index 4326a8823..000000000 --- a/space/components/issues/board-views/block-downvotes.tsx +++ /dev/null @@ -1,10 +0,0 @@ -"use client"; - -export const IssueBlockDownVotes = ({ number }: { number: number }) => ( -
- - arrow_upward_alt - - {number} -
-); diff --git a/space/components/issues/board-views/block-due-date.tsx b/space/components/issues/board-views/block-due-date.tsx deleted file mode 100644 index ecf229562..000000000 --- a/space/components/issues/board-views/block-due-date.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -// helpers -import { renderFullDate } from "@/helpers/date-time.helper"; - -export const dueDateIconDetails = ( - date: string, - stateGroup: string -): { - iconName: string; - className: string; -} => { - let iconName = "calendar_today"; - let className = ""; - - if (!date || ["completed", "cancelled"].includes(stateGroup)) { - iconName = "calendar_today"; - className = ""; - } else { - const today = new Date(); - today.setHours(0, 0, 0, 0); - const targetDate = new Date(date); - targetDate.setHours(0, 0, 0, 0); - - const timeDifference = targetDate.getTime() - today.getTime(); - - if (timeDifference < 0) { - iconName = "event_busy"; - className = "text-red-500"; - } else if (timeDifference === 0) { - iconName = "today"; - className = "text-red-500"; - } else if (timeDifference === 24 * 60 * 60 * 1000) { - iconName = "event"; - className = "text-yellow-500"; - } else { - iconName = "calendar_today"; - className = ""; - } - } - - return { - iconName, - className, - }; -}; - -export const IssueBlockDueDate = ({ due_date, group }: { due_date: string; group: string }) => { - const iconDetails = dueDateIconDetails(due_date, group); - - return ( -
- - {iconDetails.iconName} - - {renderFullDate(due_date)} -
- ); -}; diff --git a/space/components/issues/board-views/block-labels.tsx b/space/components/issues/board-views/block-labels.tsx deleted file mode 100644 index 05f6a039f..000000000 --- a/space/components/issues/board-views/block-labels.tsx +++ /dev/null @@ -1,19 +0,0 @@ -"use client"; - -export const IssueBlockLabels = ({ labels }: any) => ( -
- {labels && - labels.length > 0 && - labels.map((_label: any) => ( -
-
-
-
{_label?.name}
-
-
- ))} -
-); diff --git a/space/components/issues/board-views/block-state.tsx b/space/components/issues/board-views/block-state.tsx deleted file mode 100644 index 39b10ceb0..000000000 --- a/space/components/issues/board-views/block-state.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// ui -import { StateGroupIcon } from "@plane/ui"; -// constants -import { issueGroupFilter } from "@/constants/issue"; - -export const IssueBlockState = ({ state }: any) => { - const stateGroup = issueGroupFilter(state.group); - - if (stateGroup === null) return <>; - return ( -
-
- -
{state?.name}
-
-
- ); -}; diff --git a/space/components/issues/board-views/block-upvotes.tsx b/space/components/issues/board-views/block-upvotes.tsx deleted file mode 100644 index 3927acac4..000000000 --- a/space/components/issues/board-views/block-upvotes.tsx +++ /dev/null @@ -1,8 +0,0 @@ -"use client"; - -export const IssueBlockUpVotes = ({ number }: { number: number }) => ( -
- arrow_upward_alt - {number} -
-); diff --git a/space/components/issues/board-views/calendar/index.tsx b/space/components/issues/board-views/calendar/index.tsx deleted file mode 100644 index 0edeca96c..000000000 --- a/space/components/issues/board-views/calendar/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export const IssueCalendarView = () =>
; diff --git a/space/components/issues/board-views/gantt/index.tsx b/space/components/issues/board-views/gantt/index.tsx deleted file mode 100644 index 5da924b2c..000000000 --- a/space/components/issues/board-views/gantt/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export const IssueGanttView = () =>
; diff --git a/space/components/issues/board-views/kanban/block.tsx b/space/components/issues/board-views/kanban/block.tsx deleted file mode 100644 index e34222dd4..000000000 --- a/space/components/issues/board-views/kanban/block.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -import { useRouter, useSearchParams } from "next/navigation"; -// components -import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date"; -import { IssueBlockPriority } from "@/components/issues/board-views/block-priority"; -import { IssueBlockState } from "@/components/issues/board-views/block-state"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useIssueDetails, useProject } from "@/hooks/store"; -// interfaces -import { IIssue } from "@/types/issue"; - -type IssueKanBanBlockProps = { - issue: IIssue; - workspaceSlug: string; - projectId: string; - params: any; -}; - -export const IssueKanBanBlock: FC = observer((props) => { - const router = useRouter(); - const searchParams = useSearchParams(); - // query params - const board = searchParams.get("board") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const labels = searchParams.get("labels") || undefined; - // props - const { workspaceSlug, projectId, issue } = props; - // hooks - const { project } = useProject(); - const { setPeekId } = useIssueDetails(); - - const handleBlockClick = () => { - setPeekId(issue.id); - const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels }); - router.push(`/${workspaceSlug}/${projectId}?${queryParam}`); - }; - - return ( -
- {/* id */} -
- {project?.identifier}-{issue?.sequence_id} -
- - {/* name */} -
- {issue.name} -
- -
- {/* priority */} - {issue?.priority && ( -
- -
- )} - {/* state */} - {issue?.state_detail && ( -
- -
- )} - {/* due date */} - {issue?.target_date && ( -
- -
- )} -
-
- ); -}); diff --git a/space/components/issues/board-views/kanban/header.tsx b/space/components/issues/board-views/kanban/header.tsx deleted file mode 100644 index baf5612b3..000000000 --- a/space/components/issues/board-views/kanban/header.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// ui -import { StateGroupIcon } from "@plane/ui"; -// constants -import { issueGroupFilter } from "@/constants/issue"; -// mobx hook -// import { useIssue } from "@/hooks/store"; -// interfaces -import { IIssueState } from "@/types/issue"; - -export const IssueKanBanHeader = observer(({ state }: { state: IIssueState }) => { - // const { getCountOfIssuesByState } = useIssue(); - const stateGroup = issueGroupFilter(state.group); - - if (stateGroup === null) return <>; - - return ( -
-
- -
-
{state?.name}
- {/* {getCountOfIssuesByState(state.id)} */} -
- ); -}); diff --git a/space/components/issues/board-views/kanban/index.tsx b/space/components/issues/board-views/kanban/index.tsx deleted file mode 100644 index e2e4e9900..000000000 --- a/space/components/issues/board-views/kanban/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { IssueKanBanBlock } from "@/components/issues/board-views/kanban/block"; -import { IssueKanBanHeader } from "@/components/issues/board-views/kanban/header"; -// ui -import { Icon } from "@/components/ui"; -// mobx hook -import { useIssue } from "@/hooks/store"; -// interfaces -import { IIssueState, IIssue } from "@/types/issue"; - -type IssueKanbanViewProps = { - workspaceSlug: string; - projectId: string; -}; - -export const IssueKanbanView: FC = observer((props) => { - const { workspaceSlug, projectId } = props; - // store hooks - const { states, getFilteredIssuesByState } = useIssue(); - - return ( -
- {states && - states.length > 0 && - states.map((_state: IIssueState) => ( -
-
- -
-
- {getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? ( -
- {getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( - - ))} -
- ) : ( -
- - No issues in this state -
- )} -
-
- ))} -
- ); -}); diff --git a/space/components/issues/board-views/list/index.tsx b/space/components/issues/board-views/list/index.tsx deleted file mode 100644 index 2a2b958be..000000000 --- a/space/components/issues/board-views/list/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { IssueListBlock } from "@/components/issues/board-views/list/block"; -import { IssueListHeader } from "@/components/issues/board-views/list/header"; -// mobx hook -import { useIssue } from "@/hooks/store"; -// types -import { IIssueState, IIssue } from "@/types/issue"; - -type IssueListViewProps = { - workspaceSlug: string; - projectId: string; -}; - -export const IssueListView: FC = observer((props) => { - const { workspaceSlug, projectId } = props; - // store hooks - const { states, getFilteredIssuesByState } = useIssue(); - - return ( - <> - {states && - states.length > 0 && - states.map((_state: IIssueState) => ( -
- - {getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? ( -
- {getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( - - ))} -
- ) : ( -
No issues.
- )} -
- ))} - - ); -}); diff --git a/space/components/issues/board-views/spreadsheet/index.tsx b/space/components/issues/board-views/spreadsheet/index.tsx deleted file mode 100644 index 45ebf2792..000000000 --- a/space/components/issues/board-views/spreadsheet/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export const IssueSpreadsheetView = () =>
; diff --git a/space/components/issues/filters/applied-filters/filters-list.tsx b/space/components/issues/filters/applied-filters/filters-list.tsx index 83d651f5d..87089c500 100644 --- a/space/components/issues/filters/applied-filters/filters-list.tsx +++ b/space/components/issues/filters/applied-filters/filters-list.tsx @@ -1,10 +1,10 @@ "use client"; -// icons import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // types -import { IIssueLabel, IIssueState, TFilters } from "@/types/issue"; +import { IStateLite } from "@plane/types"; +import { IIssueLabel, TFilters } from "@/types/issue"; // components import { AppliedPriorityFilters } from "./priority"; import { AppliedStateFilters } from "./state"; @@ -14,7 +14,7 @@ type Props = { handleRemoveAllFilters: () => void; handleRemoveFilter: (key: keyof TFilters, value: string | null) => void; labels?: IIssueLabel[] | undefined; - states?: IIssueState[] | undefined; + states?: IStateLite[] | undefined; }; export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); diff --git a/space/components/issues/filters/applied-filters/root.tsx b/space/components/issues/filters/applied-filters/root.tsx index 9dd1eb013..9b6625d75 100644 --- a/space/components/issues/filters/applied-filters/root.tsx +++ b/space/components/issues/filters/applied-filters/root.tsx @@ -12,18 +12,18 @@ import { TIssueQueryFilters } from "@/types/issue"; import { AppliedFiltersList } from "./filters-list"; type TIssueAppliedFilters = { - workspaceSlug: string; - projectId: string; + anchor: string; }; export const IssueAppliedFilters: FC = observer((props) => { + const { anchor } = props; + // router const router = useRouter(); - // props - const { workspaceSlug, projectId } = props; - // hooks - const { issueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter(); + // store hooks + const { getIssueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter(); const { states, labels } = useIssue(); - + // derived values + const issueFilters = getIssueFilters(anchor); const activeLayout = issueFilters?.display_filters?.layout || undefined; const userFilters = issueFilters?.filters || {}; @@ -46,30 +46,26 @@ export const IssueAppliedFilters: FC = observer((props) => if (labels.length > 0) params = { ...params, labels: labels.join(",") }; params = new URLSearchParams(params).toString(); - router.push(`/${workspaceSlug}/${projectId}?${params}`); + router.push(`/issues/${anchor}?${params}`); }, - [workspaceSlug, projectId, activeLayout, issueFilters, router] + [activeLayout, anchor, issueFilters, router] ); const handleFilters = useCallback( (key: keyof TIssueQueryFilters, value: string | null) => { - if (!projectId) return; - let newValues = cloneDeep(issueFilters?.filters?.[key]) ?? []; if (value === null) newValues = []; else if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1); - updateIssueFilters(projectId, "filters", key, newValues); + updateIssueFilters(anchor, "filters", key, newValues); updateRouteParams(key, newValues); }, - [projectId, issueFilters, updateIssueFilters, updateRouteParams] + [anchor, issueFilters, updateIssueFilters, updateRouteParams] ); const handleRemoveAllFilters = () => { - if (!projectId) return; - - initIssueFilters(projectId, { + initIssueFilters(anchor, { display_filters: { layout: activeLayout || "list" }, filters: { state: [], @@ -78,13 +74,13 @@ export const IssueAppliedFilters: FC = observer((props) => }, }); - router.push(`/${workspaceSlug}/${projectId}?${`board=${activeLayout || "list"}`}`); + router.push(`/issues/${anchor}?${`board=${activeLayout || "list"}`}`); }; if (Object.keys(appliedFilters).length === 0) return null; return ( -
+
void; - states: IIssueState[]; + states: IStateLite[]; values: string[]; }; diff --git a/space/components/issues/filters/root.tsx b/space/components/issues/filters/root.tsx index de972ea8a..dba13f9fb 100644 --- a/space/components/issues/filters/root.tsx +++ b/space/components/issues/filters/root.tsx @@ -17,17 +17,18 @@ import { useIssue, useIssueFilter } from "@/hooks/store"; import { TIssueQueryFilters } from "@/types/issue"; type IssueFiltersDropdownProps = { - workspaceSlug: string; - projectId: string; + anchor: string; }; export const IssueFiltersDropdown: FC = observer((props) => { + const { anchor } = props; + // router const router = useRouter(); - const { workspaceSlug, projectId } = props; // hooks - const { issueFilters, updateIssueFilters } = useIssueFilter(); + const { getIssueFilters, updateIssueFilters } = useIssueFilter(); const { states, labels } = useIssue(); - + // derived values + const issueFilters = getIssueFilters(anchor); const activeLayout = issueFilters?.display_filters?.layout || undefined; const updateRouteParams = useCallback( @@ -37,24 +38,24 @@ export const IssueFiltersDropdown: FC = observer((pro const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? []; const { queryParam } = queryParamGenerator({ board: activeLayout, priority, state, labels }); - router.push(`/${workspaceSlug}/${projectId}?${queryParam}`); + router.push(`/issues/${anchor}?${queryParam}`); }, - [workspaceSlug, projectId, activeLayout, issueFilters, router] + [anchor, activeLayout, issueFilters, router] ); const handleFilters = useCallback( (key: keyof TIssueQueryFilters, value: string) => { - if (!projectId || !value) return; + if (!value) return; const newValues = cloneDeep(issueFilters?.filters?.[key]) ?? []; if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1); else newValues.push(value); - updateIssueFilters(projectId, "filters", key, newValues); + updateIssueFilters(anchor, "filters", key, newValues); updateRouteParams(key, newValues); }, - [projectId, issueFilters, updateIssueFilters, updateRouteParams] + [anchor, issueFilters, updateIssueFilters, updateRouteParams] ); return ( diff --git a/space/components/issues/filters/selection.tsx b/space/components/issues/filters/selection.tsx index a1180b0ee..926fbf5b0 100644 --- a/space/components/issues/filters/selection.tsx +++ b/space/components/issues/filters/selection.tsx @@ -4,7 +4,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search, X } from "lucide-react"; // types -import { IIssueState, IIssueLabel, IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue"; +import { IStateLite } from "@plane/types"; +import { IIssueLabel, IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue"; // components import { FilterPriority, FilterState } from "./"; @@ -13,7 +14,7 @@ type Props = { handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void; layoutDisplayFiltersOptions: TIssueFilterKeys[]; labels?: IIssueLabel[] | undefined; - states?: IIssueState[] | undefined; + states?: IStateLite[] | undefined; }; export const FilterSelection: React.FC = observer((props) => { diff --git a/space/components/issues/filters/state.tsx b/space/components/issues/filters/state.tsx index 24b6bb5c8..f61237eef 100644 --- a/space/components/issues/filters/state.tsx +++ b/space/components/issues/filters/state.tsx @@ -1,17 +1,18 @@ "use client"; import React, { useState } from "react"; +// types +import { IStateLite } from "@plane/types"; +// ui import { Loader, StateGroupIcon } from "@plane/ui"; // components import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers"; -// types -import { IIssueState } from "@/types/issue"; type Props = { appliedFilters: string[] | null; handleUpdate: (val: string) => void; searchQuery: string; - states: IIssueState[] | undefined; + states: IStateLite[] | undefined; }; export const FilterState: React.FC = (props) => { diff --git a/space/components/issues/index.ts b/space/components/issues/index.ts new file mode 100644 index 000000000..6aee62097 --- /dev/null +++ b/space/components/issues/index.ts @@ -0,0 +1,2 @@ +export * from "./issue-layouts"; +export * from "./navbar"; diff --git a/space/components/issues/issue-layouts/index.ts b/space/components/issues/issue-layouts/index.ts new file mode 100644 index 000000000..5ab6813cd --- /dev/null +++ b/space/components/issues/issue-layouts/index.ts @@ -0,0 +1,4 @@ +export * from "./kanban"; +export * from "./list"; +export * from "./properties"; +export * from "./root"; diff --git a/space/components/issues/issue-layouts/kanban/block.tsx b/space/components/issues/issue-layouts/kanban/block.tsx new file mode 100644 index 000000000..ac03823b4 --- /dev/null +++ b/space/components/issues/issue-layouts/kanban/block.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +// components +import { IssueBlockDueDate, IssueBlockPriority, IssueBlockState } from "@/components/issues"; +// helpers +import { queryParamGenerator } from "@/helpers/query-param-generator"; +// hooks +import { useIssueDetails, usePublish } from "@/hooks/store"; +// interfaces +import { IIssue } from "@/types/issue"; + +type Props = { + anchor: string; + issue: IIssue; + params: any; +}; + +export const IssueKanBanBlock: FC = observer((props) => { + const { anchor, issue } = props; + const searchParams = useSearchParams(); + // query params + const board = searchParams.get("board"); + const state = searchParams.get("state"); + const priority = searchParams.get("priority"); + const labels = searchParams.get("labels"); + // store hooks + const { project_details } = usePublish(anchor); + const { setPeekId } = useIssueDetails(); + + const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels }); + + const handleBlockClick = () => { + setPeekId(issue.id); + }; + + return ( + + {/* id */} +
+ {project_details?.identifier}-{issue?.sequence_id} +
+ + {/* name */} +
+ {issue.name} +
+ +
+ {/* priority */} + {issue?.priority && ( +
+ +
+ )} + {/* state */} + {issue?.state_detail && ( +
+ +
+ )} + {/* due date */} + {issue?.target_date && ( +
+ +
+ )} +
+ + ); +}); diff --git a/space/components/issues/issue-layouts/kanban/header.tsx b/space/components/issues/issue-layouts/kanban/header.tsx new file mode 100644 index 000000000..ee5433d68 --- /dev/null +++ b/space/components/issues/issue-layouts/kanban/header.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { observer } from "mobx-react-lite"; +// types +import { IStateLite } from "@plane/types"; +// ui +import { StateGroupIcon } from "@plane/ui"; + +type Props = { + state: IStateLite; +}; + +export const IssueKanBanHeader: React.FC = observer((props) => { + const { state } = props; + + return ( +
+
+ +
+
{state?.name}
+ {/* {getCountOfIssuesByState(state.id)} */} +
+ ); +}); diff --git a/space/components/issues/issue-layouts/kanban/index.ts b/space/components/issues/issue-layouts/kanban/index.ts new file mode 100644 index 000000000..62874fbda --- /dev/null +++ b/space/components/issues/issue-layouts/kanban/index.ts @@ -0,0 +1,3 @@ +export * from "./block"; +export * from "./header"; +export * from "./root"; diff --git a/space/components/issues/issue-layouts/kanban/root.tsx b/space/components/issues/issue-layouts/kanban/root.tsx new file mode 100644 index 000000000..e0a5593e9 --- /dev/null +++ b/space/components/issues/issue-layouts/kanban/root.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { IssueKanBanBlock, IssueKanBanHeader } from "@/components/issues"; +// ui +import { Icon } from "@/components/ui"; +// mobx hook +import { useIssue } from "@/hooks/store"; + +type Props = { + anchor: string; +}; + +export const IssueKanbanLayoutRoot: FC = observer((props) => { + const { anchor } = props; + // store hooks + const { states, getFilteredIssuesByState } = useIssue(); + + return ( +
+ {states?.map((state) => { + const issues = getFilteredIssuesByState(state.id); + + return ( +
+
+ +
+
+ {issues && issues.length > 0 ? ( +
+ {issues.map((issue) => ( + + ))} +
+ ) : ( +
+ + No issues in this state +
+ )} +
+
+ ); + })} +
+ ); +}); diff --git a/space/components/issues/board-views/list/block.tsx b/space/components/issues/issue-layouts/list/block.tsx similarity index 64% rename from space/components/issues/board-views/list/block.tsx rename to space/components/issues/issue-layouts/list/block.tsx index 6b6231fcf..8c241753d 100644 --- a/space/components/issues/board-views/list/block.tsx +++ b/space/components/issues/issue-layouts/list/block.tsx @@ -1,56 +1,52 @@ "use client"; import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter, useSearchParams } from "next/navigation"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; // components -import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date"; -import { IssueBlockLabels } from "@/components/issues/board-views/block-labels"; -import { IssueBlockPriority } from "@/components/issues/board-views/block-priority"; -import { IssueBlockState } from "@/components/issues/board-views/block-state"; +import { IssueBlockDueDate, IssueBlockLabels, IssueBlockPriority, IssueBlockState } from "@/components/issues"; // helpers import { queryParamGenerator } from "@/helpers/query-param-generator"; // hook -import { useIssueDetails, useProject } from "@/hooks/store"; -// interfaces +import { useIssueDetails, usePublish } from "@/hooks/store"; +// types import { IIssue } from "@/types/issue"; -// store type IssueListBlockProps = { + anchor: string; issue: IIssue; - workspaceSlug: string; - projectId: string; }; -export const IssueListBlock: FC = observer((props) => { - const { workspaceSlug, projectId, issue } = props; - const searchParams = useSearchParams(); +export const IssueListLayoutBlock: FC = observer((props) => { + const { anchor, issue } = props; // query params + const searchParams = useSearchParams(); const board = searchParams.get("board") || undefined; const state = searchParams.get("state") || undefined; const priority = searchParams.get("priority") || undefined; const labels = searchParams.get("labels") || undefined; - // store - const { project } = useProject(); + // store hooks const { setPeekId } = useIssueDetails(); - // router - const router = useRouter(); + const { project_details } = usePublish(anchor); + const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels }); const handleBlockClick = () => { setPeekId(issue.id); - - const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels }); - router.push(`/${workspaceSlug}/${projectId}?${queryParam}`); }; return ( -
+
{/* id */}
- {project?.identifier}-{issue?.sequence_id} + {project_details?.identifier}-{issue?.sequence_id}
{/* name */} -
+
{issue.name}
@@ -84,6 +80,6 @@ export const IssueListBlock: FC = observer((props) => {
)}
-
+ ); }); diff --git a/space/components/issues/board-views/list/header.tsx b/space/components/issues/issue-layouts/list/header.tsx similarity index 54% rename from space/components/issues/board-views/list/header.tsx rename to space/components/issues/issue-layouts/list/header.tsx index 2f8f6c018..a038050a9 100644 --- a/space/components/issues/board-views/list/header.tsx +++ b/space/components/issues/issue-layouts/list/header.tsx @@ -1,20 +1,18 @@ "use client"; + +import React from "react"; import { observer } from "mobx-react-lite"; +// types +import { IStateLite } from "@plane/types"; // ui import { StateGroupIcon } from "@plane/ui"; -// constants -import { issueGroupFilter } from "@/constants/issue"; -// mobx hook -// import { useIssue } from "@/hooks/store"; -// types -import { IIssueState } from "@/types/issue"; -export const IssueListHeader = observer(({ state }: { state: IIssueState }) => { - // const { getCountOfIssuesByState } = useIssue(); - const stateGroup = issueGroupFilter(state.group); - // const count = getCountOfIssuesByState(state.id); +type Props = { + state: IStateLite; +}; - if (stateGroup === null) return <>; +export const IssueListLayoutHeader: React.FC = observer((props) => { + const { state } = props; return (
diff --git a/space/components/issues/issue-layouts/list/index.ts b/space/components/issues/issue-layouts/list/index.ts new file mode 100644 index 000000000..62874fbda --- /dev/null +++ b/space/components/issues/issue-layouts/list/index.ts @@ -0,0 +1,3 @@ +export * from "./block"; +export * from "./header"; +export * from "./root"; diff --git a/space/components/issues/issue-layouts/list/root.tsx b/space/components/issues/issue-layouts/list/root.tsx new file mode 100644 index 000000000..02cd25b40 --- /dev/null +++ b/space/components/issues/issue-layouts/list/root.tsx @@ -0,0 +1,40 @@ +"use client"; +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { IssueListLayoutBlock, IssueListLayoutHeader } from "@/components/issues"; +// mobx hook +import { useIssue } from "@/hooks/store"; + +type Props = { + anchor: string; +}; + +export const IssuesListLayoutRoot: FC = observer((props) => { + const { anchor } = props; + // store hooks + const { states, getFilteredIssuesByState } = useIssue(); + + return ( + <> + {states?.map((state) => { + const issues = getFilteredIssuesByState(state.id); + + return ( +
+ + {issues && issues.length > 0 ? ( +
+ {issues.map((issue) => ( + + ))} +
+ ) : ( +
No issues.
+ )} +
+ ); + })} + + ); +}); diff --git a/space/components/issues/issue-layouts/properties/due-date.tsx b/space/components/issues/issue-layouts/properties/due-date.tsx new file mode 100644 index 000000000..3b73973e7 --- /dev/null +++ b/space/components/issues/issue-layouts/properties/due-date.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { CalendarCheck2 } from "lucide-react"; +// types +import { TStateGroups } from "@plane/types"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; + +type Props = { + due_date: string; + group: TStateGroups; +}; + +export const IssueBlockDueDate = (props: Props) => { + const { due_date, group } = props; + + return ( +
+ + {renderFormattedDate(due_date)} +
+ ); +}; diff --git a/space/components/issues/issue-layouts/properties/index.ts b/space/components/issues/issue-layouts/properties/index.ts new file mode 100644 index 000000000..de78f9966 --- /dev/null +++ b/space/components/issues/issue-layouts/properties/index.ts @@ -0,0 +1,4 @@ +export * from "./due-date"; +export * from "./labels"; +export * from "./priority"; +export * from "./state"; diff --git a/space/components/issues/issue-layouts/properties/labels.tsx b/space/components/issues/issue-layouts/properties/labels.tsx new file mode 100644 index 000000000..75c32c4a0 --- /dev/null +++ b/space/components/issues/issue-layouts/properties/labels.tsx @@ -0,0 +1,17 @@ +"use client"; + +export const IssueBlockLabels = ({ labels }: any) => ( +
+ {labels?.map((_label: any) => ( +
+
+
+
{_label?.name}
+
+
+ ))} +
+); diff --git a/space/components/issues/board-views/block-priority.tsx b/space/components/issues/issue-layouts/properties/priority.tsx similarity index 85% rename from space/components/issues/board-views/block-priority.tsx rename to space/components/issues/issue-layouts/properties/priority.tsx index 3110930ec..b91d56bb8 100644 --- a/space/components/issues/board-views/block-priority.tsx +++ b/space/components/issues/issue-layouts/properties/priority.tsx @@ -1,11 +1,11 @@ "use client"; // types -import { issuePriorityFilter } from "@/constants/issue"; -import { TIssueFilterPriority } from "@/types/issue"; +import { TIssuePriorities } from "@plane/types"; // constants +import { issuePriorityFilter } from "@/constants/issue"; -export const IssueBlockPriority = ({ priority }: { priority: TIssueFilterPriority | null }) => { +export const IssueBlockPriority = ({ priority }: { priority: TIssuePriorities | null }) => { const priority_detail = priority != null ? issuePriorityFilter(priority) : null; if (priority_detail === null) return <>; diff --git a/space/components/issues/issue-layouts/properties/state.tsx b/space/components/issues/issue-layouts/properties/state.tsx new file mode 100644 index 000000000..b80f1f3df --- /dev/null +++ b/space/components/issues/issue-layouts/properties/state.tsx @@ -0,0 +1,11 @@ +// ui +import { StateGroupIcon } from "@plane/ui"; + +export const IssueBlockState = ({ state }: any) => ( +
+
+ +
{state?.name}
+
+
+); diff --git a/space/components/views/project-details.tsx b/space/components/issues/issue-layouts/root.tsx similarity index 52% rename from space/components/views/project-details.tsx rename to space/components/issues/issue-layouts/root.tsx index 462c656f0..e53986c85 100644 --- a/space/components/views/project-details.tsx +++ b/space/components/issues/issue-layouts/root.tsx @@ -6,69 +6,55 @@ import Image from "next/image"; import { useSearchParams } from "next/navigation"; import useSWR from "swr"; // components -import { IssueCalendarView } from "@/components/issues/board-views/calendar"; -import { IssueGanttView } from "@/components/issues/board-views/gantt"; -import { IssueKanbanView } from "@/components/issues/board-views/kanban"; -import { IssueListView } from "@/components/issues/board-views/list"; -import { IssueSpreadsheetView } from "@/components/issues/board-views/spreadsheet"; +import { IssueKanbanLayoutRoot, IssuesListLayoutRoot } from "@/components/issues"; import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root"; import { IssuePeekOverview } from "@/components/issues/peek-overview"; -// mobx store -import { useIssue, useUser, useIssueDetails, useIssueFilter, useProject } from "@/hooks/store"; +// hooks +import { useIssue, useIssueDetails, useIssueFilter } from "@/hooks/store"; +// store +import { PublishStore } from "@/store/publish/publish.store"; // assets import SomethingWentWrongImage from "public/something-went-wrong.svg"; -type ProjectDetailsViewProps = { - workspaceSlug: string; - projectId: string; +type Props = { peekId: string | undefined; + publishSettings: PublishStore; }; -export const ProjectDetailsView: FC = observer((props) => { - // router - const searchParams = useSearchParams(); +export const IssuesLayoutsRoot: FC = observer((props) => { + const { peekId, publishSettings } = props; // query params + const searchParams = useSearchParams(); const states = searchParams.get("states") || undefined; const priority = searchParams.get("priority") || undefined; const labels = searchParams.get("labels") || undefined; - - const { workspaceSlug, projectId, peekId } = props; - // hooks - const { fetchProjectSettings } = useProject(); - const { issueFilters } = useIssueFilter(); + // store hooks + const { getIssueFilters } = useIssueFilter(); const { loader, issues, error, fetchPublicIssues } = useIssue(); const issueDetailStore = useIssueDetails(); - const { data: currentUser, fetchCurrentUser } = useUser(); + // derived values + const { anchor } = publishSettings; + const issueFilters = anchor ? getIssueFilters(anchor) : undefined; useSWR( - workspaceSlug && projectId ? "WORKSPACE_PROJECT_SETTINGS" : null, - workspaceSlug && projectId ? () => fetchProjectSettings(workspaceSlug, projectId) : null - ); - useSWR( - (workspaceSlug && projectId) || states || priority || labels ? "WORKSPACE_PROJECT_PUBLIC_ISSUES" : null, - (workspaceSlug && projectId) || states || priority || labels - ? () => fetchPublicIssues(workspaceSlug, projectId, { states, priority, labels }) - : null - ); - useSWR( - workspaceSlug && projectId && !currentUser ? "WORKSPACE_PROJECT_CURRENT_USER" : null, - workspaceSlug && projectId && !currentUser ? () => fetchCurrentUser() : null + anchor ? `PUBLIC_ISSUES_${anchor}` : null, + anchor ? () => fetchPublicIssues(anchor, { states, priority, labels }) : null ); useEffect(() => { - if (peekId && workspaceSlug && projectId) { + if (peekId) { issueDetailStore.setPeekId(peekId.toString()); } - }, [peekId, issueDetailStore, projectId, workspaceSlug]); + }, [peekId, issueDetailStore]); // derived values const activeLayout = issueFilters?.display_filters?.layout || undefined; + if (!anchor) return null; + return (
- {workspaceSlug && projectId && peekId && ( - - )} + {peekId && } {loader && !issues ? (
Loading...
@@ -90,21 +76,18 @@ export const ProjectDetailsView: FC = observer((props) activeLayout && (
{/* applied filters */} - + {activeLayout === "list" && (
- +
)} {activeLayout === "kanban" && (
- +
)} - {activeLayout === "calendar" && } - {activeLayout === "spreadsheet" && } - {activeLayout === "gantt" && }
) )} diff --git a/space/components/issues/navbar/controls.tsx b/space/components/issues/navbar/controls.tsx index 20c0ca408..25f2edfb0 100644 --- a/space/components/issues/navbar/controls.tsx +++ b/space/components/issues/navbar/controls.tsx @@ -4,26 +4,25 @@ import { useEffect, FC } from "react"; import { observer } from "mobx-react-lite"; import { useRouter, useSearchParams } from "next/navigation"; // components +import { IssuesLayoutSelection, NavbarTheme, UserAvatar } from "@/components/issues"; import { IssueFiltersDropdown } from "@/components/issues/filters"; -import { NavbarIssueBoardView } from "@/components/issues/navbar/issue-board-view"; -import { NavbarTheme } from "@/components/issues/navbar/theme"; -import { UserAvatar } from "@/components/issues/navbar/user-avatar"; // helpers import { queryParamGenerator } from "@/helpers/query-param-generator"; // hooks -import { useProject, useIssueFilter, useIssueDetails } from "@/hooks/store"; +import { useIssueFilter, useIssueDetails } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; +// store +import { PublishStore } from "@/store/publish/publish.store"; // types import { TIssueLayout } from "@/types/issue"; export type NavbarControlsProps = { - workspaceSlug: string; - projectId: string; + publishSettings: PublishStore; }; export const NavbarControls: FC = observer((props) => { // props - const { workspaceSlug, projectId } = props; + const { publishSettings } = props; // router const router = useRouter(); const searchParams = useSearchParams(); @@ -34,24 +33,25 @@ export const NavbarControls: FC = observer((props) => { const priority = searchParams.get("priority") || undefined; const peekId = searchParams.get("peekId") || undefined; // hooks - const { issueFilters, isIssueFiltersUpdated, initIssueFilters } = useIssueFilter(); - const { settings } = useProject(); + const { getIssueFilters, isIssueFiltersUpdated, initIssueFilters } = useIssueFilter(); const { setPeekId } = useIssueDetails(); // derived values + const { anchor, view_props, workspace_detail } = publishSettings; + const issueFilters = anchor ? getIssueFilters(anchor) : undefined; const activeLayout = issueFilters?.display_filters?.layout || undefined; const isInIframe = useIsInIframe(); useEffect(() => { - if (workspaceSlug && projectId && settings) { + if (anchor && workspace_detail) { const viewsAcceptable: string[] = []; let currentBoard: TIssueLayout | null = null; - if (settings?.views?.list) viewsAcceptable.push("list"); - if (settings?.views?.kanban) viewsAcceptable.push("kanban"); - if (settings?.views?.calendar) viewsAcceptable.push("calendar"); - if (settings?.views?.gantt) viewsAcceptable.push("gantt"); - if (settings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet"); + if (view_props?.list) viewsAcceptable.push("list"); + if (view_props?.kanban) viewsAcceptable.push("kanban"); + if (view_props?.calendar) viewsAcceptable.push("calendar"); + if (view_props?.gantt) viewsAcceptable.push("gantt"); + if (view_props?.spreadsheet) viewsAcceptable.push("spreadsheet"); if (board) { if (viewsAcceptable.includes(board.toString())) currentBoard = board.toString() as TIssueLayout; @@ -74,39 +74,41 @@ export const NavbarControls: FC = observer((props) => { }, }; - if (!isIssueFiltersUpdated(params)) { - initIssueFilters(projectId, params); - router.push(`/${workspaceSlug}/${projectId}?${queryParam}`); + if (!isIssueFiltersUpdated(anchor, params)) { + initIssueFilters(anchor, params); + router.push(`/issues/${anchor}?${queryParam}`); } } } } }, [ - workspaceSlug, - projectId, + anchor, board, labels, state, priority, peekId, - settings, activeLayout, router, initIssueFilters, setPeekId, isIssueFiltersUpdated, + view_props, + workspace_detail, ]); + if (!anchor) return null; + return ( <> {/* issue views */}
- +
{/* issue filters */}
- +
{/* theming */} diff --git a/space/components/issues/navbar/index.ts b/space/components/issues/navbar/index.ts new file mode 100644 index 000000000..e1bb02d91 --- /dev/null +++ b/space/components/issues/navbar/index.ts @@ -0,0 +1,5 @@ +export * from "./controls"; +export * from "./layout-selection"; +export * from "./root"; +export * from "./theme"; +export * from "./user-avatar"; diff --git a/space/components/issues/navbar/issue-board-view.tsx b/space/components/issues/navbar/issue-board-view.tsx deleted file mode 100644 index 711229961..000000000 --- a/space/components/issues/navbar/issue-board-view.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -import { useRouter, useSearchParams } from "next/navigation"; -// constants -import { issueLayoutViews } from "@/constants/issue"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useIssueFilter } from "@/hooks/store"; -// mobx -import { TIssueLayout } from "@/types/issue"; - -type NavbarIssueBoardViewProps = { - workspaceSlug: string; - projectId: string; -}; - -export const NavbarIssueBoardView: FC = observer((props) => { - const router = useRouter(); - const searchParams = useSearchParams(); - // query params - const labels = searchParams.get("labels") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const peekId = searchParams.get("peekId") || undefined; - // props - const { workspaceSlug, projectId } = props; - // hooks - const { layoutOptions, issueFilters, updateIssueFilters } = useIssueFilter(); - - // derived values - const activeLayout = issueFilters?.display_filters?.layout || undefined; - - const handleCurrentBoardView = (boardView: TIssueLayout) => { - updateIssueFilters(projectId, "display_filters", "layout", boardView); - const { queryParam } = queryParamGenerator({ board: boardView, peekId, priority, state, labels }); - router.push(`/${workspaceSlug}/${projectId}?${queryParam}`); - }; - - return ( - <> - {issueLayoutViews && - Object.keys(issueLayoutViews).map((key: string) => { - const layoutKey = key as TIssueLayout; - if (layoutOptions[layoutKey]) { - return ( -
handleCurrentBoardView(layoutKey)} - title={layoutKey} - > - - {issueLayoutViews[layoutKey]?.icon} - -
- ); - } - })} - - ); -}); diff --git a/space/components/issues/navbar/layout-selection.tsx b/space/components/issues/navbar/layout-selection.tsx new file mode 100644 index 000000000..1989710b5 --- /dev/null +++ b/space/components/issues/navbar/layout-selection.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +import { useRouter, useSearchParams } from "next/navigation"; +// ui +import { Tooltip } from "@plane/ui"; +// constants +import { ISSUE_LAYOUTS } from "@/constants/issue"; +// helpers +import { queryParamGenerator } from "@/helpers/query-param-generator"; +// hooks +import { useIssueFilter } from "@/hooks/store"; +// mobx +import { TIssueLayout } from "@/types/issue"; + +type Props = { + anchor: string; +}; + +export const IssuesLayoutSelection: FC = observer((props) => { + const { anchor } = props; + // router + const router = useRouter(); + const searchParams = useSearchParams(); + // query params + const labels = searchParams.get("labels"); + const state = searchParams.get("state"); + const priority = searchParams.get("priority"); + const peekId = searchParams.get("peekId"); + // hooks + const { layoutOptions, getIssueFilters, updateIssueFilters } = useIssueFilter(); + // derived values + const issueFilters = getIssueFilters(anchor); + const activeLayout = issueFilters?.display_filters?.layout || undefined; + + const handleCurrentBoardView = (boardView: TIssueLayout) => { + updateIssueFilters(anchor, "display_filters", "layout", boardView); + const { queryParam } = queryParamGenerator({ board: boardView, peekId, priority, state, labels }); + router.push(`/issues/${anchor}?${queryParam}`); + }; + + return ( +
+ {ISSUE_LAYOUTS.map((layout) => { + if (!layoutOptions[layout.key]) return; + + return ( + + + + ); + })} +
+ ); +}); diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/root.tsx similarity index 58% rename from space/components/issues/navbar/index.tsx rename to space/components/issues/navbar/root.tsx index f5d60b8b0..1d1a294d9 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/root.tsx @@ -4,41 +4,40 @@ import { observer } from "mobx-react-lite"; import { Briefcase } from "lucide-react"; // components import { ProjectLogo } from "@/components/common"; -import { NavbarControls } from "@/components/issues/navbar/controls"; -// hooks -import { useProject } from "@/hooks/store"; +import { NavbarControls } from "@/components/issues"; +// store +import { PublishStore } from "@/store/publish/publish.store"; -type IssueNavbarProps = { - workspaceSlug: string; - projectId: string; +type Props = { + publishSettings: PublishStore; }; -const IssueNavbar: FC = observer((props) => { - const { workspaceSlug, projectId } = props; +export const IssuesNavbarRoot: FC = observer((props) => { + const { publishSettings } = props; // hooks - const { project } = useProject(); + const { project_details } = publishSettings; return (
{/* project detail */}
- {project ? ( + {project_details ? ( - + ) : ( )} -
{project?.name || `...`}
+
+ {project_details?.name || `...`} +
- +
); }); - -export default IssueNavbar; diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index a1647c9c5..57ed0b6f6 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -8,7 +8,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui"; // editor components import { LiteTextEditor } from "@/components/editor/lite-text-editor"; // hooks -import { useIssueDetails, useProject, useUser } from "@/hooks/store"; +import { useIssueDetails, usePublish, useUser } from "@/hooks/store"; // types import { Comment } from "@/types/issue"; @@ -17,22 +17,18 @@ const defaultValues: Partial = { }; type Props = { + anchor: string; disabled?: boolean; - workspaceSlug: string; - projectId: string; }; export const AddComment: React.FC = observer((props) => { - // const { disabled = false } = props; - const { workspaceSlug, projectId } = props; + const { anchor } = props; // refs const editorRef = useRef(null); // store hooks - const { workspace } = useProject(); const { peekId: issueId, addIssueComment } = useIssueDetails(); const { data: currentUser } = useUser(); - // derived values - const workspaceId = workspace?.id; + const { workspaceSlug, workspace: workspaceID } = usePublish(anchor); // form info const { handleSubmit, @@ -43,9 +39,9 @@ export const AddComment: React.FC = observer((props) => { } = useForm({ defaultValues }); const onSubmit = async (formData: Comment) => { - if (!workspaceSlug || !projectId || !issueId || isSubmitting || !formData.comment_html) return; + if (!anchor || !issueId || isSubmitting || !formData.comment_html) return; - await addIssueComment(workspaceSlug, projectId, issueId, formData) + await addIssueComment(anchor, issueId, formData) .then(() => { reset(defaultValues); editorRef.current?.clearEditor(); @@ -71,8 +67,8 @@ export const AddComment: React.FC = observer((props) => { onEnterKeyPress={(e) => { if (currentUser) handleSubmit(onSubmit)(e); }} - workspaceId={workspaceId as string} - workspaceSlug={workspaceSlug} + workspaceId={workspaceID?.toString() ?? ""} + workspaceSlug={workspaceSlug?.toString() ?? ""} ref={editorRef} initialValue={ !value || value === "" || (typeof value === "object" && Object.keys(value).length === 0) diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx index 3ede0333b..31e5f7324 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -10,25 +10,23 @@ import { CommentReactions } from "@/components/issues/peek-overview"; // helpers import { timeAgo } from "@/helpers/date-time.helper"; // hooks -import { useIssueDetails, useProject, useUser } from "@/hooks/store"; +import { useIssueDetails, usePublish, useUser } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; // types import { Comment } from "@/types/issue"; type Props = { - workspaceSlug: string; + anchor: string; comment: Comment; }; export const CommentCard: React.FC = observer((props) => { - const { comment, workspaceSlug } = props; + const { anchor, comment } = props; // store hooks - const { workspace } = useProject(); const { peekId, deleteIssueComment, updateIssueComment } = useIssueDetails(); const { data: currentUser } = useUser(); + const { workspaceSlug, workspace: workspaceID } = usePublish(anchor); const isInIframe = useIsInIframe(); - // derived values - const workspaceId = workspace?.id; // states const [isEditing, setIsEditing] = useState(false); @@ -45,13 +43,13 @@ export const CommentCard: React.FC = observer((props) => { }); const handleDelete = () => { - if (!workspaceSlug || !peekId) return; - deleteIssueComment(workspaceSlug, comment.project, peekId, comment.id); + if (!anchor || !peekId) return; + deleteIssueComment(anchor, peekId, comment.id); }; const handleCommentUpdate = async (formData: Comment) => { - if (!workspaceSlug || !peekId) return; - updateIssueComment(workspaceSlug, comment.project, peekId, comment.id, formData); + if (!anchor || !peekId) return; + updateIssueComment(anchor, peekId, comment.id, formData); setIsEditing(false); editorRef.current?.setEditorValue(formData.comment_html); showEditorRef.current?.setEditorValue(formData.comment_html); @@ -103,8 +101,8 @@ export const CommentCard: React.FC = observer((props) => { name="comment_html" render={({ field: { onChange, value } }) => ( = observer((props) => {
- +
diff --git a/space/components/issues/peek-overview/comment/comment-reactions.tsx b/space/components/issues/peek-overview/comment/comment-reactions.tsx index ed915eff4..1b3977794 100644 --- a/space/components/issues/peek-overview/comment/comment-reactions.tsx +++ b/space/components/issues/peek-overview/comment/comment-reactions.tsx @@ -13,12 +13,12 @@ import { useIssueDetails, useUser } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; type Props = { + anchor: string; commentId: string; - projectId: string; - workspaceSlug: string; }; export const CommentReactions: React.FC = observer((props) => { + const { anchor, commentId } = props; const router = useRouter(); const pathName = usePathname(); const searchParams = useSearchParams(); @@ -28,7 +28,6 @@ export const CommentReactions: React.FC = observer((props) => { const priority = searchParams.get("priority") || undefined; const labels = searchParams.get("labels") || undefined; - const { commentId, projectId, workspaceSlug } = props; // hooks const { addCommentReaction, removeCommentReaction, details, peekId } = useIssueDetails(); const { data: user } = useUser(); @@ -40,13 +39,13 @@ export const CommentReactions: React.FC = observer((props) => { const userReactions = commentReactions?.filter((r) => r?.actor_detail?.id === user?.id); const handleAddReaction = (reactionHex: string) => { - if (!workspaceSlug || !projectId || !peekId) return; - addCommentReaction(workspaceSlug, projectId, peekId, commentId, reactionHex); + if (!anchor || !peekId) return; + addCommentReaction(anchor, peekId, commentId, reactionHex); }; const handleRemoveReaction = (reactionHex: string) => { - if (!workspaceSlug || !projectId || !peekId) return; - removeCommentReaction(workspaceSlug, projectId, peekId, commentId, reactionHex); + if (!anchor || !peekId) return; + removeCommentReaction(anchor, peekId, commentId, reactionHex); }; const handleReactionClick = (reactionHex: string) => { diff --git a/space/components/issues/peek-overview/full-screen-peek-view.tsx b/space/components/issues/peek-overview/full-screen-peek-view.tsx index f5918de43..4e9d5ed8c 100644 --- a/space/components/issues/peek-overview/full-screen-peek-view.tsx +++ b/space/components/issues/peek-overview/full-screen-peek-view.tsx @@ -11,14 +11,13 @@ import { import { IIssue } from "@/types/issue"; type Props = { + anchor: string; handleClose: () => void; issueDetails: IIssue | undefined; - workspaceSlug: string; - projectId: string; }; export const FullScreenPeekView: React.FC = observer((props) => { - const { handleClose, issueDetails, workspaceSlug, projectId } = props; + const { anchor, handleClose, issueDetails } = props; return (
@@ -30,17 +29,13 @@ export const FullScreenPeekView: React.FC = observer((props) => {
{/* issue title and description */}
- +
{/* divider */}
{/* issue activity/comments */}
- +
) : ( diff --git a/space/components/issues/peek-overview/header.tsx b/space/components/issues/peek-overview/header.tsx index 0e9b93ab9..3ad07e06b 100644 --- a/space/components/issues/peek-overview/header.tsx +++ b/space/components/issues/peek-overview/header.tsx @@ -1,10 +1,9 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { MoveRight } from "lucide-react"; +import { Link2, MoveRight } from "lucide-react"; import { Listbox, Transition } from "@headlessui/react"; // ui -import { setToast, TOAST_TYPE } from "@plane/ui"; -import { Icon } from "@/components/ui"; +import { CenterPanelIcon, FullScreenPanelIcon, setToast, SidePanelIcon, TOAST_TYPE } from "@plane/ui"; // helpers import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks @@ -18,21 +17,21 @@ type Props = { issueDetails: IIssue | undefined; }; -const peekModes: { +const PEEK_MODES: { key: IPeekMode; - icon: string; + icon: any; label: string; }[] = [ - { key: "side", icon: "side_navigation", label: "Side Peek" }, + { key: "side", icon: SidePanelIcon, label: "Side Peek" }, { key: "modal", - icon: "dialogs", - label: "Modal Peek", + icon: CenterPanelIcon, + label: "Modal", }, { key: "full", - icon: "nearby", - label: "Full Screen Peek", + icon: FullScreenPanelIcon, + label: "Full Screen", }, ]; @@ -47,20 +46,22 @@ export const PeekOverviewHeader: React.FC = observer((props) => { copyTextToClipboard(urlToCopy).then(() => { setToast({ - type: TOAST_TYPE.INFO, + type: TOAST_TYPE.SUCCESS, title: "Link copied!", - message: "Issue link copied to clipboard", + message: "Issue link copied to clipboard.", }); }); }; + const Icon = PEEK_MODES.find((m) => m.key === peekMode)?.icon ?? SidePanelIcon; + return ( <>
{peekMode === "side" && ( - )} = observer((props) => { onChange={(val) => setPeekMode(val)} className="relative flex-shrink-0 text-left" > - - m.key === peekMode)?.icon ?? ""} className="text-[1rem]" /> + + = observer((props) => { >
- {peekModes.map((mode) => ( + {PEEK_MODES.map((mode) => ( = observer((props) => {
{isClipboardWriteAllowed && (peekMode === "side" || peekMode === "modal") && (
-
)} diff --git a/space/components/issues/peek-overview/issue-activity.tsx b/space/components/issues/peek-overview/issue-activity.tsx index ec73bda7b..1ccb7fa88 100644 --- a/space/components/issues/peek-overview/issue-activity.tsx +++ b/space/components/issues/peek-overview/issue-activity.tsx @@ -7,61 +7,58 @@ import { Button } from "@plane/ui"; import { CommentCard, AddComment } from "@/components/issues/peek-overview"; import { Icon } from "@/components/ui"; // hooks -import { useIssueDetails, useProject, useUser } from "@/hooks/store"; +import { useIssueDetails, usePublish, useUser } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; // types import { IIssue } from "@/types/issue"; type Props = { + anchor: string; issueDetails: IIssue; - workspaceSlug: string; - projectId: string; }; export const PeekOverviewIssueActivity: React.FC = observer((props) => { - const { workspaceSlug, projectId } = props; + const { anchor } = props; // router const pathname = usePathname(); - // store - const { canComment } = useProject(); + // store hooks const { details, peekId } = useIssueDetails(); const { data: currentUser } = useUser(); - const isInIframe = useIsInIframe(); - + const { canComment } = usePublish(anchor); + // derived values const comments = details[peekId || ""]?.comments || []; + const isInIframe = useIsInIframe(); return (

Comments

- {workspaceSlug && ( -
-
- {comments.map((comment: any) => ( - - ))} -
- {!isInIframe && - (currentUser ? ( - <> - {canComment && ( -
- -
- )} - - ) : ( -
-

- - Sign in to add your comment -

- - - -
- ))} +
+
+ {comments.map((comment) => ( + + ))}
- )} + {!isInIframe && + (currentUser ? ( + <> + {canComment && ( +
+ +
+ )} + + ) : ( +
+

+ + Sign in to add your comment +

+ + + +
+ ))} +
); }); diff --git a/space/components/issues/peek-overview/issue-details.tsx b/space/components/issues/peek-overview/issue-details.tsx index 5fe73f67a..97a659554 100644 --- a/space/components/issues/peek-overview/issue-details.tsx +++ b/space/components/issues/peek-overview/issue-details.tsx @@ -5,26 +5,33 @@ import { IssueReactions } from "@/components/issues/peek-overview"; import { IIssue } from "@/types/issue"; type Props = { + anchor: string; issueDetails: IIssue; }; -export const PeekOverviewIssueDetails: React.FC = ({ issueDetails }) => ( -
-
- {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} -
-

{issueDetails.name}

- {issueDetails.description_html !== "" && issueDetails.description_html !== "

" && ( -

" - : issueDetails.description_html - } - /> - )} - -
-); +export const PeekOverviewIssueDetails: React.FC = (props) => { + const { anchor, issueDetails } = props; + + const description = issueDetails.description_html; + + return ( +
+
+ {issueDetails.project_detail?.identifier}-{issueDetails?.sequence_id} +
+

{issueDetails.name}

+ {description !== "" && description !== "

" && ( +

" + : description + } + /> + )} + +
+ ); +}; diff --git a/space/components/issues/peek-overview/issue-emoji-reactions.tsx b/space/components/issues/peek-overview/issue-emoji-reactions.tsx index 4a0e61554..ae960eab3 100644 --- a/space/components/issues/peek-overview/issue-emoji-reactions.tsx +++ b/space/components/issues/peek-overview/issue-emoji-reactions.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; // lib @@ -11,11 +10,12 @@ import { queryParamGenerator } from "@/helpers/query-param-generator"; import { useIssueDetails, useUser } from "@/hooks/store"; type IssueEmojiReactionsProps = { - workspaceSlug: string; - projectId: string; + anchor: string; }; export const IssueEmojiReactions: React.FC = observer((props) => { + const { anchor } = props; + // router const router = useRouter(); const pathName = usePathname(); const searchParams = useSearchParams(); @@ -25,11 +25,9 @@ export const IssueEmojiReactions: React.FC = observer( const state = searchParams.get("state") || undefined; const priority = searchParams.get("priority") || undefined; const labels = searchParams.get("labels") || undefined; - - const { workspaceSlug, projectId } = props; - // store + // store hooks const issueDetailsStore = useIssueDetails(); - const { data: user, fetchCurrentUser } = useUser(); + const { data: user } = useUser(); const issueId = issueDetailsStore.peekId; const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : []; @@ -38,13 +36,13 @@ export const IssueEmojiReactions: React.FC = observer( const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id); const handleAddReaction = (reactionHex: string) => { - if (!workspaceSlug || !projectId || !issueId) return; - issueDetailsStore.addIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex); + if (!issueId) return; + issueDetailsStore.addIssueReaction(anchor, issueId, reactionHex); }; const handleRemoveReaction = (reactionHex: string) => { - if (!workspaceSlug || !projectId || !issueId) return; - issueDetailsStore.removeIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex); + if (!issueId) return; + issueDetailsStore.removeIssueReaction(anchor, issueId, reactionHex); }; const handleReactionClick = (reactionHex: string) => { @@ -53,11 +51,6 @@ export const IssueEmojiReactions: React.FC = observer( else handleAddReaction(reactionHex); }; - useEffect(() => { - if (user) return; - fetchCurrentUser(); - }, [user, fetchCurrentUser]); - // derived values const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels }); diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx index 08d22b312..2bdfe21bb 100644 --- a/space/components/issues/peek-overview/issue-properties.tsx +++ b/space/components/issues/peek-overview/issue-properties.tsx @@ -1,16 +1,17 @@ +import { CalendarCheck2, Signal } from "lucide-react"; // ui -import { StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui"; -// icons +import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui"; +// components import { Icon } from "@/components/ui"; // constants -import { issueGroupFilter, issuePriorityFilter } from "@/constants/issue"; +import { issuePriorityFilter } from "@/constants/issue"; // helpers -import { renderFullDate } from "@/helpers/date-time.helper"; +import { cn } from "@/helpers/common.helper"; +import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper"; // types import { IIssue, IPeekMode } from "@/types/issue"; -// components -import { dueDateIconDetails } from "../board-views/block-due-date"; type Props = { issueDetails: IIssue; @@ -19,12 +20,9 @@ type Props = { export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mode }) => { const state = issueDetails.state_detail; - const stateGroup = issueGroupFilter(state.group); const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null; - const dueDateIcon = dueDateIconDetails(issueDetails.target_date, state.group); - const handleCopyLink = () => { const urlToCopy = window.location.href; @@ -51,28 +49,22 @@ export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mod
)} -
-
-
- - State +
+
+
+ + State
-
- {stateGroup && ( -
-
- - {addSpaceIfCamelCase(state?.name ?? "")} -
-
- )} +
+ + {addSpaceIfCamelCase(state?.name ?? "")}
-
-
- - Priority +
+
+ + Priority
= ({ issueDetails, mod
-
-
- - Due date + +
+
+ + Due date
{issueDetails.target_date ? ( -
- - {dueDateIcon.iconName} - - {renderFullDate(issueDetails.target_date)} +
+ + {renderFormattedDate(issueDetails.target_date)}
) : ( Empty diff --git a/space/components/issues/peek-overview/issue-reaction.tsx b/space/components/issues/peek-overview/issue-reaction.tsx index 87210f377..c3b580abc 100644 --- a/space/components/issues/peek-overview/issue-reaction.tsx +++ b/space/components/issues/peek-overview/issue-reaction.tsx @@ -1,33 +1,31 @@ -import { useParams } from "next/navigation"; +import { observer } from "mobx-react-lite"; import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview"; -import { useProject } from "@/hooks/store"; +// hooks +import { usePublish } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; -// type IssueReactionsProps = { -// workspaceSlug: string; -// projectId: string; -// }; +type Props = { + anchor: string; +}; -export const IssueReactions: React.FC = () => { - const { workspace_slug: workspaceSlug, project_id: projectId } = useParams(); - - const { canVote, canReact } = useProject(); +export const IssueReactions: React.FC = observer((props) => { + const { anchor } = props; + // store hooks + const { canVote, canReact } = usePublish(anchor); const isInIframe = useIsInIframe(); return (
{canVote && ( - <> -
- -
- +
+ +
)} {!isInIframe && canReact && (
- +
)}
); -}; +}); diff --git a/space/components/issues/peek-overview/issue-vote-reactions.tsx b/space/components/issues/peek-overview/issue-vote-reactions.tsx index 1e565e862..6b24e5a9f 100644 --- a/space/components/issues/peek-overview/issue-vote-reactions.tsx +++ b/space/components/issues/peek-overview/issue-vote-reactions.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { observer } from "mobx-react-lite"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Tooltip } from "@plane/ui"; @@ -12,11 +12,14 @@ import { useIssueDetails, useUser } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; type TIssueVotes = { - workspaceSlug: string; - projectId: string; + anchor: string; }; export const IssueVotes: React.FC = observer((props) => { + const { anchor } = props; + // states + const [isSubmitting, setIsSubmitting] = useState(false); + // router const router = useRouter(); const pathName = usePathname(); const searchParams = useSearchParams(); @@ -26,13 +29,9 @@ export const IssueVotes: React.FC = observer((props) => { const state = searchParams.get("state") || undefined; const priority = searchParams.get("priority") || undefined; const labels = searchParams.get("labels") || undefined; - - const { workspaceSlug, projectId } = props; - // states - const [isSubmitting, setIsSubmitting] = useState(false); - + // store hooks const issueDetailsStore = useIssueDetails(); - const { data: user, fetchCurrentUser } = useUser(); + const { data: user } = useUser(); const isInIframe = useIsInIframe(); @@ -47,28 +46,22 @@ export const IssueVotes: React.FC = observer((props) => { const isDownVotedByUser = allDownVotes?.some((vote) => vote.actor === user?.id); const handleVote = async (e: any, voteValue: 1 | -1) => { - if (!workspaceSlug || !projectId || !issueId) return; + if (!issueId) return; setIsSubmitting(true); const actionPerformed = votes?.find((vote) => vote.actor === user?.id && vote.vote === voteValue); - if (actionPerformed) - await issueDetailsStore.removeIssueVote(workspaceSlug.toString(), projectId.toString(), issueId); - else - await issueDetailsStore.addIssueVote(workspaceSlug.toString(), projectId.toString(), issueId, { + if (actionPerformed) await issueDetailsStore.removeIssueVote(anchor, issueId); + else { + await issueDetailsStore.addIssueVote(anchor, issueId, { vote: voteValue, }); + } setIsSubmitting(false); }; - useEffect(() => { - if (user) return; - - fetchCurrentUser(); - }, [user, fetchCurrentUser]); - const VOTES_LIMIT = 1000; // derived values diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx index 453cc59f3..d1fe6f7aa 100644 --- a/space/components/issues/peek-overview/layout.tsx +++ b/space/components/issues/peek-overview/layout.tsx @@ -10,13 +10,12 @@ import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overv import { useIssue, useIssueDetails } from "@/hooks/store"; type TIssuePeekOverview = { - workspaceSlug: string; - projectId: string; + anchor: string; peekId: string; }; export const IssuePeekOverview: FC = observer((props) => { - const { workspaceSlug, projectId, peekId } = props; + const { anchor, peekId } = props; const router = useRouter(); const searchParams = useSearchParams(); // query params @@ -34,21 +33,23 @@ export const IssuePeekOverview: FC = observer((props) => { const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined; useEffect(() => { - if (workspaceSlug && projectId && peekId && issueStore.issues && issueStore.issues.length > 0) { + if (anchor && peekId && issueStore.issues && issueStore.issues.length > 0) { if (!issueDetails) { - issueDetailStore.fetchIssueDetails(workspaceSlug.toString(), projectId.toString(), peekId.toString()); + issueDetailStore.fetchIssueDetails(anchor, peekId.toString()); } } - }, [workspaceSlug, projectId, issueDetailStore, issueDetails, peekId, issueStore.issues]); + }, [anchor, issueDetailStore, issueDetails, peekId, issueStore.issues]); const handleClose = () => { issueDetailStore.setPeekId(null); - let queryParams: any = { board: board }; + let queryParams: any = { + board, + }; if (priority && priority.length > 0) queryParams = { ...queryParams, priority: priority }; if (state && state.length > 0) queryParams = { ...queryParams, state: state }; if (labels && labels.length > 0) queryParams = { ...queryParams, labels: labels }; queryParams = new URLSearchParams(queryParams).toString(); - router.push(`/${workspaceSlug}/${projectId}?${queryParams}`); + router.push(`/issues/${anchor}?${queryParams}`); }; useEffect(() => { @@ -80,12 +81,7 @@ export const IssuePeekOverview: FC = observer((props) => { leaveTo="translate-x-full" > - + @@ -119,20 +115,10 @@ export const IssuePeekOverview: FC = observer((props) => { }`} > {issueDetailStore.peekMode === "modal" && ( - + )} {issueDetailStore.peekMode === "full" && ( - + )}
diff --git a/space/components/issues/peek-overview/side-peek-view.tsx b/space/components/issues/peek-overview/side-peek-view.tsx index a0b544bdd..894441418 100644 --- a/space/components/issues/peek-overview/side-peek-view.tsx +++ b/space/components/issues/peek-overview/side-peek-view.tsx @@ -7,22 +7,21 @@ import { PeekOverviewIssueDetails, PeekOverviewIssueProperties, } from "@/components/issues/peek-overview"; -// hooks -import { useProject } from "@/hooks/store"; +// store hooks +import { usePublish } from "@/hooks/store"; // types import { IIssue } from "@/types/issue"; type Props = { + anchor: string; handleClose: () => void; issueDetails: IIssue | undefined; - workspaceSlug: string; - projectId: string; }; export const SidePeekView: React.FC = observer((props) => { - const { handleClose, issueDetails, workspaceSlug, projectId } = props; - - const { settings } = useProject(); + const { anchor, handleClose, issueDetails } = props; + // store hooks + const { canComment } = usePublish(anchor); return (
@@ -33,7 +32,7 @@ export const SidePeekView: React.FC = observer((props) => {
{/* issue title and description */}
- +
{/* issue properties */}
@@ -42,13 +41,9 @@ export const SidePeekView: React.FC = observer((props) => { {/* divider */}
{/* issue activity/comments */} - {settings?.comments && ( + {canComment && (
- +
)}
diff --git a/space/components/ui/dropdown.tsx b/space/components/ui/dropdown.tsx deleted file mode 100644 index 788627094..000000000 --- a/space/components/ui/dropdown.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { Fragment, useState, useRef } from "react"; -import Link from "next/link"; -import { Check, ChevronLeft } from "lucide-react"; -import { Popover, Transition } from "@headlessui/react"; -// hooks -import useOutSideClick from "hooks/use-outside-click"; - -type ItemOptionType = { - display: React.ReactNode; - as?: "button" | "link" | "div"; - href?: string; - isSelected?: boolean; - onClick?: () => void; - children?: ItemOptionType[] | null; -}; - -type DropdownItemProps = { - item: ItemOptionType; -}; - -type DropDownListProps = { - open: boolean; - handleClose?: () => void; - items: ItemOptionType[]; -}; - -type DropdownProps = { - button: React.ReactNode | (() => React.ReactNode); - items: ItemOptionType[]; -}; - -const DropdownList: React.FC = (props) => { - const { open, items, handleClose } = props; - - const ref = useRef(null); - - useOutSideClick(ref, () => { - if (handleClose) handleClose(); - }); - - return ( - - - -
- {items.map((item, index) => ( - - ))} -
-
-
-
- ); -}; - -const DropdownItem: React.FC = (props) => { - const { item } = props; - const { display, children, as: itemAs, href, onClick, isSelected } = item; - - const [open, setOpen] = useState(false); - - return ( -
- {(!itemAs || itemAs === "button" || itemAs === "div") && ( - - )} - - {itemAs === "link" && {display}} - - {children && setOpen(false)} items={children} />} -
- ); -}; - -const Dropdown: React.FC = (props) => { - const { button, items } = props; - - return ( - - {({ open }) => ( - <> - - {typeof button === "function" ? button() : button} - - - - -
- {items.map((item, index) => ( - - ))} -
-
-
- - )} -
- ); -}; - -export { Dropdown }; diff --git a/space/components/ui/index.ts b/space/components/ui/index.ts index 1e523d5dd..ccd2303c4 100644 --- a/space/components/ui/index.ts +++ b/space/components/ui/index.ts @@ -1,3 +1,2 @@ -export * from "./dropdown"; export * from "./icon"; export * from "./reaction-selector"; diff --git a/space/components/views/index.ts b/space/components/views/index.ts index 251de14e3..97ccf7649 100644 --- a/space/components/views/index.ts +++ b/space/components/views/index.ts @@ -1,2 +1 @@ export * from "./auth"; -export * from "./project-details"; diff --git a/space/constants/issue.ts b/space/constants/issue.ts index fb9c78fcd..77297946f 100644 --- a/space/constants/issue.ts +++ b/space/constants/issue.ts @@ -1,13 +1,7 @@ -// interfaces -import { - TIssueLayout, - TIssueLayoutViews, - TIssueFilterKeys, - TIssueFilterPriority, - TIssueFilterPriorityObject, - TIssueFilterState, - TIssueFilterStateObject, -} from "types/issue"; +import { Calendar, GanttChartSquare, Kanban, List, Sheet } from "lucide-react"; +// types +import { TIssuePriorities } from "@plane/types"; +import { TIssueLayout, TIssueFilterKeys, TIssueFilterPriorityObject } from "@/types/issue"; // issue filters export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]> } = { @@ -28,20 +22,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [key in TIssueLayout]: Record<"f }, }; -export const issueLayoutViews: Partial = { - list: { - title: "List View", - icon: "format_list_bulleted", - className: "", - }, - kanban: { - title: "Board View", - icon: "grid_view", - className: "", - }, -}; +export const ISSUE_LAYOUTS: { + key: TIssueLayout; + title: string; + icon: any; +}[] = [ + { key: "list", title: "List", icon: List }, + { key: "kanban", title: "Kanban", icon: Kanban }, + { key: "calendar", title: "Calendar", icon: Calendar }, + { key: "spreadsheet", title: "Spreadsheet", icon: Sheet }, + { key: "gantt", title: "Gantt chart", icon: GanttChartSquare }, +]; -// issue priority filters export const issuePriorityFilters: TIssueFilterPriorityObject[] = [ { key: "urgent", @@ -75,7 +67,7 @@ export const issuePriorityFilters: TIssueFilterPriorityObject[] = [ }, ]; -export const issuePriorityFilter = (priorityKey: TIssueFilterPriority): TIssueFilterPriorityObject | undefined => { +export const issuePriorityFilter = (priorityKey: TIssuePriorities): TIssueFilterPriorityObject | undefined => { const currentIssuePriority: TIssueFilterPriorityObject | undefined = issuePriorityFilters && issuePriorityFilters.length > 0 ? issuePriorityFilters.find((_priority) => _priority.key === priorityKey) @@ -84,55 +76,3 @@ export const issuePriorityFilter = (priorityKey: TIssueFilterPriority): TIssueFi if (currentIssuePriority) return currentIssuePriority; return undefined; }; - -// issue group filters -export const issueGroupColors: { - [key in TIssueFilterState]: string; -} = { - backlog: "#d9d9d9", - unstarted: "#3f76ff", - started: "#f59e0b", - completed: "#16a34a", - cancelled: "#dc2626", -}; - -export const issueGroups: TIssueFilterStateObject[] = [ - { - key: "backlog", - title: "Backlog", - color: "#d9d9d9", - className: `text-[#d9d9d9] bg-[#d9d9d9]/10`, - }, - { - key: "unstarted", - title: "Unstarted", - color: "#3f76ff", - className: `text-[#3f76ff] bg-[#3f76ff]/10`, - }, - { - key: "started", - title: "Started", - color: "#f59e0b", - className: `text-[#f59e0b] bg-[#f59e0b]/10`, - }, - { - key: "completed", - title: "Completed", - color: "#16a34a", - className: `text-[#16a34a] bg-[#16a34a]/10`, - }, - { - key: "cancelled", - title: "Cancelled", - color: "#dc2626", - className: `text-[#dc2626] bg-[#dc2626]/10`, - }, -]; - -export const issueGroupFilter = (issueKey: TIssueFilterState): TIssueFilterStateObject | undefined => { - const currentIssueStateGroup: TIssueFilterStateObject | undefined = - issueGroups && issueGroups.length > 0 ? issueGroups.find((group) => group.key === issueKey) : undefined; - - if (currentIssueStateGroup) return currentIssueStateGroup; - return undefined; -}; diff --git a/space/constants/state.ts b/space/constants/state.ts new file mode 100644 index 000000000..b0fd622be --- /dev/null +++ b/space/constants/state.ts @@ -0,0 +1,37 @@ +import { TStateGroups } from "@plane/types"; + +export const STATE_GROUPS: { + [key in TStateGroups]: { + key: TStateGroups; + label: string; + color: string; + }; +} = { + backlog: { + key: "backlog", + label: "Backlog", + color: "#d9d9d9", + }, + unstarted: { + key: "unstarted", + label: "Unstarted", + color: "#3f76ff", + }, + started: { + key: "started", + label: "Started", + color: "#f59e0b", + }, + completed: { + key: "completed", + label: "Completed", + color: "#16a34a", + }, + cancelled: { + key: "cancelled", + label: "Canceled", + color: "#dc2626", + }, +}; + +export const ARCHIVABLE_STATE_GROUPS = [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key]; diff --git a/space/constants/workspace.ts b/space/constants/workspace.ts deleted file mode 100644 index 5ae5a7cf4..000000000 --- a/space/constants/workspace.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const USER_ROLES = [ - { value: "Product / Project Manager", label: "Product / Project Manager" }, - { value: "Development / Engineering", label: "Development / Engineering" }, - { value: "Founder / Executive", label: "Founder / Executive" }, - { value: "Freelancer / Consultant", label: "Freelancer / Consultant" }, - { value: "Marketing / Growth", label: "Marketing / Growth" }, - { value: "Sales / Business Development", label: "Sales / Business Development" }, - { value: "Support / Operations", label: "Support / Operations" }, - { value: "Student / Professor", label: "Student / Professor" }, - { value: "Human Resources", label: "Human Resources" }, - { value: "Other", label: "Other" }, -]; diff --git a/space/helpers/date-time.helper.ts b/space/helpers/date-time.helper.ts index f19a5358b..3930bcb83 100644 --- a/space/helpers/date-time.helper.ts +++ b/space/helpers/date-time.helper.ts @@ -1,3 +1,6 @@ +import { format, isValid } from "date-fns"; +import isNumber from "lodash/isNumber"; + export const timeAgo = (time: any) => { switch (typeof time) { case "number": @@ -14,24 +17,43 @@ export const timeAgo = (time: any) => { }; /** - * @description Returns date and month, if date is of the current year - * @description Returns date, month adn year, if date is of a different year than current - * @param {string} date - * @example renderFullDate("2023-01-01") // 1 Jan - * @example renderFullDate("2021-01-01") // 1 Jan, 2021 + * This method returns a date from string of type yyyy-mm-dd + * This method is recommended to use instead of new Date() as this does not introduce any timezone offsets + * @param date + * @returns date or undefined */ +export const getDate = (date: string | Date | undefined | null): Date | undefined => { + try { + if (!date || date === "") return; -export const renderFullDate = (date: string): string => { - if (!date) return ""; + if (typeof date !== "string" && !(date instanceof String)) return date; - const months: string[] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + const [yearString, monthString, dayString] = date.substring(0, 10).split("-"); + const year = parseInt(yearString); + const month = parseInt(monthString); + const day = parseInt(dayString); + if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return; - const currentDate: Date = new Date(); - const [year, month, day]: number[] = date.split("-").map(Number); - - const formattedMonth: string = months[month - 1]; - const formattedDay: string = day < 10 ? `0${day}` : day.toString(); - - if (currentDate.getFullYear() === year) return `${formattedDay} ${formattedMonth}`; - else return `${formattedDay} ${formattedMonth}, ${year}`; + return new Date(year, month - 1, day); + } catch (e) { + return undefined; + } +}; + +/** + * @returns {string | null} formatted date in the format of MMM dd, yyyy + * @description Returns date in the formatted format + * @param {Date | string} date + * @example renderFormattedDate("2024-01-01") // Jan 01, 2024 + */ +export const renderFormattedDate = (date: string | Date | undefined | null): string | null => { + // Parse the date to check if it is valid + const parsedDate = getDate(date); + // return if undefined + if (!parsedDate) return null; + // Check if the parsed date is valid before formatting + if (!isValid(parsedDate)) return null; // Return null for invalid dates + // Format the date in format (MMM dd, yyyy) + const formattedDate = format(parsedDate, "MMM dd, yyyy"); + return formattedDate; }; diff --git a/space/helpers/emoji.helper.tsx b/space/helpers/emoji.helper.tsx index 7c9f3cfcb..d5f9d1b5a 100644 --- a/space/helpers/emoji.helper.tsx +++ b/space/helpers/emoji.helper.tsx @@ -1,23 +1,3 @@ -export const getRandomEmoji = () => { - const emojis = [ - "8986", - "9200", - "128204", - "127773", - "127891", - "127947", - "128076", - "128077", - "128187", - "128188", - "128512", - "128522", - "128578", - ]; - - return emojis[Math.floor(Math.random() * emojis.length)]; -}; - export const renderEmoji = ( emoji: | string diff --git a/space/helpers/issue.helper.ts b/space/helpers/issue.helper.ts new file mode 100644 index 000000000..a5159edef --- /dev/null +++ b/space/helpers/issue.helper.ts @@ -0,0 +1,30 @@ +import { differenceInCalendarDays } from "date-fns"; +// types +import { TStateGroups } from "@plane/types"; +// constants +import { STATE_GROUPS } from "@/constants/state"; +// helpers +import { getDate } from "@/helpers/date-time.helper"; + +/** + * @description check if the issue due date should be highlighted + * @param date + * @param stateGroup + * @returns boolean + */ +export const shouldHighlightIssueDueDate = ( + date: string | Date | null, + stateGroup: TStateGroups | undefined +): boolean => { + if (!date || !stateGroup) return false; + // if the issue is completed or cancelled, don't highlight the due date + if ([STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateGroup)) return false; + + const parsedDate = getDate(date); + if (!parsedDate) return false; + + const targetDateDistance = differenceInCalendarDays(parsedDate, new Date()); + + // if the issue is overdue, highlight the due date + return targetDateDistance <= 0; +}; diff --git a/space/helpers/string.helper.ts b/space/helpers/string.helper.ts index 525a9fc99..f6319bc75 100644 --- a/space/helpers/string.helper.ts +++ b/space/helpers/string.helper.ts @@ -3,7 +3,7 @@ import DOMPurify from "dompurify"; export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2"); const fallbackCopyTextToClipboard = (text: string) => { - var textArea = document.createElement("textarea"); + const textArea = document.createElement("textarea"); textArea.value = text; // Avoid scrolling to bottom @@ -18,7 +18,7 @@ const fallbackCopyTextToClipboard = (text: string) => { try { // FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this. // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand - var successful = document.execCommand("copy"); + document.execCommand("copy"); } catch (err) {} document.body.removeChild(textArea); diff --git a/space/hooks/store/index.ts b/space/hooks/store/index.ts index 76b6f9315..3f82613d5 100644 --- a/space/hooks/store/index.ts +++ b/space/hooks/store/index.ts @@ -1,5 +1,5 @@ +export * from "./publish"; export * from "./use-instance"; -export * from "./use-project"; export * from "./use-issue"; export * from "./use-user"; export * from "./use-user-profile"; diff --git a/space/hooks/store/publish/index.ts b/space/hooks/store/publish/index.ts new file mode 100644 index 000000000..a7b42ad5b --- /dev/null +++ b/space/hooks/store/publish/index.ts @@ -0,0 +1,2 @@ +export * from "./use-publish-list"; +export * from "./use-publish"; diff --git a/space/hooks/store/publish/use-publish-list.ts b/space/hooks/store/publish/use-publish-list.ts new file mode 100644 index 000000000..aa50c295a --- /dev/null +++ b/space/hooks/store/publish/use-publish-list.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/store-provider"; +// store +import { IPublishListStore } from "@/store/publish/publish_list.store"; + +export const usePublishList = (): IPublishListStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("usePublishList must be used within StoreProvider"); + return context.publishList; +}; diff --git a/space/hooks/store/publish/use-publish.ts b/space/hooks/store/publish/use-publish.ts new file mode 100644 index 000000000..3d920e8cb --- /dev/null +++ b/space/hooks/store/publish/use-publish.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/store-provider"; +// store +import { PublishStore } from "@/store/publish/publish.store"; + +export const usePublish = (anchor: string): PublishStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("usePublish must be used within StoreProvider"); + return context.publishList.publishMap?.[anchor] ?? {}; +}; diff --git a/space/hooks/store/use-project.ts b/space/hooks/store/use-project.ts deleted file mode 100644 index cd3e28958..000000000 --- a/space/hooks/store/use-project.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import { IProjectStore } from "@/store/project.store"; - -export const useProject = (): IProjectStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); - return context.project; -}; diff --git a/space/hooks/use-mention.tsx b/space/hooks/use-mention.tsx index 8b2d69720..9e33f7d90 100644 --- a/space/hooks/use-mention.tsx +++ b/space/hooks/use-mention.tsx @@ -1,7 +1,9 @@ import { useRef, useEffect } from "react"; import useSWR from "swr"; +// types import { IUser } from "@plane/types"; -import { UserService } from "services/user.service"; +// services +import { UserService } from "@/services/user.service"; export const useMention = () => { const userService = new UserService(); diff --git a/space/lib/user-provider.tsx b/space/lib/user-provider.tsx deleted file mode 100644 index 1ac1c786c..000000000 --- a/space/lib/user-provider.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ReactNode } from "react"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; -import { useUser } from "@/hooks/store"; - -export const UserProvider = observer(({ children }: { children: ReactNode }) => { - const { fetchCurrentUser } = useUser(); - - useSWR("CURRENT_USER", () => fetchCurrentUser()); - - return <>{children}; -}); diff --git a/space/package.json b/space/package.json index e3dadbff8..0932d7abf 100644 --- a/space/package.json +++ b/space/package.json @@ -26,6 +26,7 @@ "@sentry/nextjs": "^8", "axios": "^1.3.4", "clsx": "^2.0.0", + "date-fns": "^3.6.0", "dompurify": "^3.0.11", "dotenv": "^16.3.1", "js-cookie": "^3.0.1", diff --git a/space/services/file.service.ts b/space/services/file.service.ts index 0e277af1e..9fe06cd36 100644 --- a/space/services/file.service.ts +++ b/space/services/file.service.ts @@ -4,30 +4,6 @@ import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; -interface UnSplashImage { - id: string; - created_at: Date; - updated_at: Date; - promoted_at: Date; - width: number; - height: number; - color: string; - blur_hash: string; - description: null; - alt_description: string; - urls: UnSplashImageUrls; - [key: string]: any; -} - -interface UnSplashImageUrls { - raw: string; - full: string; - regular: string; - small: string; - thumb: string; - small_s3: string; -} - class FileService extends APIService { private cancelSource: any; @@ -123,40 +99,6 @@ class FileService extends APIService { throw error?.response?.data; }); } - - async deleteFile(workspaceId: string, assetUrl: string): Promise { - const lastIndex = assetUrl.lastIndexOf("/"); - const assetId = assetUrl.substring(lastIndex + 1); - - return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async uploadUserFile(file: FormData): Promise { - return this.post(`/api/users/file-assets/`, file, { - headers: { - "Content-Type": "multipart/form-data", - }, - }) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async deleteUserFile(assetUrl: string): Promise { - const lastIndex = assetUrl.lastIndexOf("/"); - const assetId = assetUrl.substring(lastIndex + 1); - - return this.delete(`/api/users/file-assets/${assetId}`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } } const fileService = new FileService(); diff --git a/space/services/issue.service.ts b/space/services/issue.service.ts index 1913b678e..f86481812 100644 --- a/space/services/issue.service.ts +++ b/space/services/issue.service.ts @@ -1,14 +1,16 @@ import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; +// types +import { TIssuesResponse } from "@/types/issue"; class IssueService extends APIService { constructor() { super(API_BASE_URL); } - async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise { - return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/issues/`, { + async fetchPublicIssues(anchor: string, params: any): Promise { + return this.get(`/api/public/anchor/${anchor}/issues/`, { params, }) .then((response) => response?.data) @@ -17,115 +19,88 @@ class IssueService extends APIService { }); } - async getIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/`) + async getIssueById(anchor: string, issueID: string): Promise { + return this.get(`/api/public/anchor/${anchor}/issues/${issueID}/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async getIssueVotes(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`) + async getIssueVotes(anchor: string, issueID: string): Promise { + return this.get(`/api/public/anchor/${anchor}/issues/${issueID}/votes/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async createIssueVote(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise { - return this.post( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`, - data - ) + async createIssueVote(anchor: string, issueID: string, data: any): Promise { + return this.post(`/api/public/anchor/${anchor}/issues/${issueID}/votes/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async deleteIssueVote(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.delete(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`) + async deleteIssueVote(anchor: string, issueID: string): Promise { + return this.delete(`/api/public/anchor/${anchor}/issues/${issueID}/votes/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async getIssueReactions(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/`) + async getIssueReactions(anchor: string, issueID: string): Promise { + return this.get(`/api/public/anchor/${anchor}/issues/${issueID}/reactions/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async createIssueReaction(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise { - return this.post( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/`, - data - ) + async createIssueReaction(anchor: string, issueID: string, data: any): Promise { + return this.post(`/api/public/anchor/${anchor}/issues/${issueID}/reactions/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async deleteIssueReaction( - workspaceSlug: string, - projectId: string, - issueId: string, - reactionId: string - ): Promise { - return this.delete( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/${reactionId}/` - ) + async deleteIssueReaction(anchor: string, issueID: string, reactionId: string): Promise { + return this.delete(`/api/public/anchor/${anchor}/issues/${issueID}/reactions/${reactionId}/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async getIssueComments(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`) + async getIssueComments(anchor: string, issueID: string): Promise { + return this.get(`/api/public/anchor/${anchor}/issues/${issueID}/comments/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise { - return this.post( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`, - data - ) + async createIssueComment(anchor: string, issueID: string, data: any): Promise { + return this.post(`/api/public/anchor/${anchor}/issues/${issueID}/comments/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async updateIssueComment( - workspaceSlug: string, - projectId: string, - issueId: string, - commentId: string, - data: any - ): Promise { - return this.patch( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/${commentId}/`, - data - ) + async updateIssueComment(anchor: string, issueID: string, commentId: string, data: any): Promise { + return this.patch(`/api/public/anchor/${anchor}/issues/${issueID}/comments/${commentId}/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async deleteIssueComment(workspaceSlug: string, projectId: string, issueId: string, commentId: string): Promise { - return this.delete( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/${commentId}/` - ) + async deleteIssueComment(anchor: string, issueID: string, commentId: string): Promise { + return this.delete(`/api/public/anchor/${anchor}/issues/${issueID}/comments/${commentId}/`) .then((response) => response?.data) .catch((error) => { throw error?.response; @@ -133,32 +108,21 @@ class IssueService extends APIService { } async createCommentReaction( - workspaceSlug: string, - projectId: string, + anchor: string, commentId: string, data: { reaction: string; } ): Promise { - return this.post( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`, - data - ) + return this.post(`/api/public/anchor/${anchor}/comments/${commentId}/reactions/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async deleteCommentReaction( - workspaceSlug: string, - projectId: string, - commentId: string, - reactionHex: string - ): Promise { - return this.delete( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/${reactionHex}/` - ) + async deleteCommentReaction(anchor: string, commentId: string, reactionHex: string): Promise { + return this.delete(`/api/public/anchor/${anchor}/comments/${commentId}/reactions/${reactionHex}/`) .then((response) => response?.data) .catch((error) => { throw error?.response; diff --git a/space/services/project-member.service.ts b/space/services/project-member.service.ts index 264d53386..722380efa 100644 --- a/space/services/project-member.service.ts +++ b/space/services/project-member.service.ts @@ -9,16 +9,16 @@ export class ProjectMemberService extends APIService { super(API_BASE_URL); } - async fetchProjectMembers(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`) + async fetchProjectMembers(anchor: string): Promise { + return this.get(`/api/anchor/${anchor}/members/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } - async getProjectMember(workspaceSlug: string, projectId: string, memberId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`) + async getProjectMember(anchor: string, memberID: string): Promise { + return this.get(`/api/anchor/${anchor}/members/${memberID}/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/space/services/project.service.ts b/space/services/project.service.ts deleted file mode 100644 index 14ed7837b..000000000 --- a/space/services/project.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { API_BASE_URL } from "@/helpers/common.helper"; -// services -import { APIService } from "@/services/api.service"; - -class ProjectService extends APIService { - constructor() { - super(API_BASE_URL); - } - - async getProjectSettings(workspace_slug: string, project_slug: string): Promise { - return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/settings/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response; - }); - } -} - -export default ProjectService; diff --git a/space/services/publish.service.ts b/space/services/publish.service.ts new file mode 100644 index 000000000..0275142c8 --- /dev/null +++ b/space/services/publish.service.ts @@ -0,0 +1,30 @@ +// types +import { TPublishSettings } from "@plane/types"; +// helpers +import { API_BASE_URL } from "@/helpers/common.helper"; +// services +import { APIService } from "@/services/api.service"; + +class PublishService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async fetchPublishSettings(anchor: string): Promise { + return this.get(`/api/public/anchor/${anchor}/settings/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async fetchAnchorFromProjectDetails(workspaceSlug: string, projectID: string): Promise { + return this.get(`/api/public/workspaces/${workspaceSlug}/projects/${projectID}/anchor/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} + +export default PublishService; diff --git a/space/store/issue-detail.store.ts b/space/store/issue-detail.store.ts index 03f611cc0..672fe29ad 100644 --- a/space/store/issue-detail.store.ts +++ b/space/store/issue-detail.store.ts @@ -10,108 +10,102 @@ import { IIssue, IPeekMode, IVote } from "@/types/issue"; export interface IIssueDetailStore { loader: boolean; error: any; - // peek info + // observables peekId: string | null; peekMode: IPeekMode; details: { [key: string]: IIssue; }; - // peek actions - setPeekId: (issueId: string | null) => void; + // actions + setPeekId: (issueID: string | null) => void; setPeekMode: (mode: IPeekMode) => void; - // issue details - fetchIssueDetails: (workspaceId: string, projectId: string, issueId: string) => void; - // issue comments - addIssueComment: (workspaceId: string, projectId: string, issueId: string, data: any) => Promise; - updateIssueComment: ( - workspaceId: string, - projectId: string, - issueId: string, - comment_id: string, - data: any - ) => Promise; - deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void; - addCommentReaction: ( - workspaceId: string, - projectId: string, - issueId: string, - commentId: string, - reactionHex: string - ) => void; - removeCommentReaction: ( - workspaceId: string, - projectId: string, - issueId: string, - commentId: string, - reactionHex: string - ) => void; - // issue reactions - addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void; - removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void; - // issue votes - addIssueVote: (workspaceId: string, projectId: string, issueId: string, data: { vote: 1 | -1 }) => Promise; - removeIssueVote: (workspaceId: string, projectId: string, issueId: string) => Promise; + // issue actions + fetchIssueDetails: (anchor: string, issueID: string) => void; + // comment actions + addIssueComment: (anchor: string, issueID: string, data: any) => Promise; + updateIssueComment: (anchor: string, issueID: string, commentID: string, data: any) => Promise; + deleteIssueComment: (anchor: string, issueID: string, commentID: string) => void; + addCommentReaction: (anchor: string, issueID: string, commentID: string, reactionHex: string) => void; + removeCommentReaction: (anchor: string, issueID: string, commentID: string, reactionHex: string) => void; + // reaction actions + addIssueReaction: (anchor: string, issueID: string, reactionHex: string) => void; + removeIssueReaction: (anchor: string, issueID: string, reactionHex: string) => void; + // vote actions + addIssueVote: (anchor: string, issueID: string, data: { vote: 1 | -1 }) => Promise; + removeIssueVote: (anchor: string, issueID: string) => Promise; } export class IssueDetailStore implements IIssueDetailStore { loader: boolean = false; error: any = null; + // observables peekId: string | null = null; peekMode: IPeekMode = "side"; details: { [key: string]: IIssue; } = {}; - issueService; + // root store rootStore: RootStore; + // services + issueService: IssueService; constructor(_rootStore: RootStore) { makeObservable(this, { loader: observable.ref, error: observable.ref, - // peek + // observables peekId: observable.ref, peekMode: observable.ref, - details: observable.ref, + details: observable, // actions setPeekId: action, setPeekMode: action, + // issue actions fetchIssueDetails: action, + // comment actions addIssueComment: action, updateIssueComment: action, deleteIssueComment: action, addCommentReaction: action, removeCommentReaction: action, + // reaction actions addIssueReaction: action, removeIssueReaction: action, + // vote actions addIssueVote: action, removeIssueVote: action, }); - this.issueService = new IssueService(); this.rootStore = _rootStore; + this.issueService = new IssueService(); } - setPeekId = (issueId: string | null) => { - this.peekId = issueId; + setPeekId = (issueID: string | null) => { + this.peekId = issueID; }; setPeekMode = (mode: IPeekMode) => { this.peekMode = mode; }; - fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => { + /** + * @description fetc + * @param {string} anchor + * @param {string} issueID + */ + fetchIssueDetails = async (anchor: string, issueID: string) => { try { this.loader = true; this.error = null; - const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueId); - const commentsResponse = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueID); + const commentsResponse = await this.issueService.getIssueComments(anchor, issueID); if (issueDetails) { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...(this.details[issueId] ?? issueDetails), + [issueID]: { + ...(this.details[issueID] ?? issueDetails), comments: commentsResponse, }, }; @@ -123,17 +117,17 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - addIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, data: any) => { + addIssueComment = async (anchor: string, issueID: string, data: any) => { try { - const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueId); - const issueCommentResponse = await this.issueService.createIssueComment(workspaceSlug, projectId, issueId, data); + const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueID); + const issueCommentResponse = await this.issueService.createIssueComment(anchor, issueID, data); if (issueDetails) { runInAction(() => { this.details = { ...this.details, - [issueId]: { + [issueID]: { ...issueDetails, - comments: [...this.details[issueId].comments, issueCommentResponse], + comments: [...this.details[issueID].comments, issueCommentResponse], }, }; }); @@ -145,36 +139,30 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - updateIssueComment = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - commentId: string, - data: any - ) => { + updateIssueComment = async (anchor: string, issueID: string, commentID: string, data: any) => { try { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], - comments: this.details[issueId].comments.map((c) => ({ + [issueID]: { + ...this.details[issueID], + comments: this.details[issueID].comments.map((c) => ({ ...c, - ...(c.id === commentId ? data : {}), + ...(c.id === commentID ? data : {}), })), }, }; }); - await this.issueService.updateIssueComment(workspaceSlug, projectId, issueId, commentId, data); + await this.issueService.updateIssueComment(anchor, issueID, commentID, data); } catch (error) { - const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + const issueComments = await this.issueService.getIssueComments(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], comments: issueComments, }, }; @@ -182,15 +170,15 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - deleteIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, comment_id: string) => { + deleteIssueComment = async (anchor: string, issueID: string, commentID: string) => { try { - await this.issueService.deleteIssueComment(workspaceSlug, projectId, issueId, comment_id); - const remainingComments = this.details[issueId].comments.filter((c) => c.id != comment_id); + await this.issueService.deleteIssueComment(anchor, issueID, commentID); + const remainingComments = this.details[issueID].comments.filter((c) => c.id != commentID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], comments: remainingComments, }, }; @@ -200,47 +188,41 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - addCommentReaction = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - commentId: string, - reactionHex: string - ) => { + addCommentReaction = async (anchor: string, issueID: string, commentID: string, reactionHex: string) => { const newReaction = { id: uuidv4(), - comment: commentId, + comment: commentID, reaction: reactionHex, actor_detail: this.rootStore.user.currentActor, }; - const newComments = this.details[issueId].comments.map((comment) => ({ + const newComments = this.details[issueID].comments.map((comment) => ({ ...comment, comment_reactions: - comment.id === commentId ? [...comment.comment_reactions, newReaction] : comment.comment_reactions, + comment.id === commentID ? [...comment.comment_reactions, newReaction] : comment.comment_reactions, })); try { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], comments: [...newComments], }, }; }); - await this.issueService.createCommentReaction(workspaceSlug, projectId, commentId, { + await this.issueService.createCommentReaction(anchor, commentID, { reaction: reactionHex, }); } catch (error) { - const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + const issueComments = await this.issueService.getIssueComments(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], comments: issueComments, }, }; @@ -248,39 +230,33 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - removeCommentReaction = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - commentId: string, - reactionHex: string - ) => { + removeCommentReaction = async (anchor: string, issueID: string, commentID: string, reactionHex: string) => { try { - const comment = this.details[issueId].comments.find((c) => c.id === commentId); + const comment = this.details[issueID].comments.find((c) => c.id === commentID); const newCommentReactions = comment?.comment_reactions.filter((r) => r.reaction !== reactionHex) ?? []; runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], - comments: this.details[issueId].comments.map((c) => ({ + [issueID]: { + ...this.details[issueID], + comments: this.details[issueID].comments.map((c) => ({ ...c, - comment_reactions: c.id === commentId ? newCommentReactions : c.comment_reactions, + comment_reactions: c.id === commentID ? newCommentReactions : c.comment_reactions, })), }, }; }); - await this.issueService.deleteCommentReaction(workspaceSlug, projectId, commentId, reactionHex); + await this.issueService.deleteCommentReaction(anchor, commentID, reactionHex); } catch (error) { - const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + const issueComments = await this.issueService.getIssueComments(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], comments: issueComments, }, }; @@ -288,18 +264,18 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => { + addIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => { try { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], reactions: [ - ...this.details[issueId].reactions, + ...this.details[issueID].reactions, { id: uuidv4(), - issue: issueId, + issue: issueID, reaction: reactionHex, actor_detail: this.rootStore.user.currentActor, }, @@ -308,17 +284,17 @@ export class IssueDetailStore implements IIssueDetailStore { }; }); - await this.issueService.createIssueReaction(workspaceSlug, projectId, issueId, { + await this.issueService.createIssueReaction(anchor, issueID, { reaction: reactionHex, }); } catch (error) { console.log("Failed to add issue vote"); - const issueReactions = await this.issueService.getIssueReactions(workspaceSlug, projectId, issueId); + const issueReactions = await this.issueService.getIssueReactions(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], reactions: issueReactions, }, }; @@ -326,31 +302,31 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - removeIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => { + removeIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => { try { - const newReactions = this.details[issueId].reactions.filter( + const newReactions = this.details[issueID].reactions.filter( (_r) => !(_r.reaction === reactionHex && _r.actor_detail.id === this.rootStore.user.data?.id) ); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], reactions: newReactions, }, }; }); - await this.issueService.deleteIssueReaction(workspaceSlug, projectId, issueId, reactionHex); + await this.issueService.deleteIssueReaction(anchor, issueID, reactionHex); } catch (error) { console.log("Failed to remove issue reaction"); - const reactions = await this.issueService.getIssueReactions(workspaceSlug, projectId, issueId); + const reactions = await this.issueService.getIssueReactions(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], reactions: reactions, }, }; @@ -358,39 +334,44 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - addIssueVote = async (workspaceSlug: string, projectId: string, issueId: string, data: { vote: 1 | -1 }) => { + addIssueVote = async (anchor: string, issueID: string, data: { vote: 1 | -1 }) => { + const publishSettings = this.rootStore.publishList?.publishMap?.[anchor]; + const projectID = publishSettings?.project; + const workspaceSlug = publishSettings?.workspace_detail?.slug; + if (!projectID || !workspaceSlug) throw new Error("Publish settings not found"); + const newVote: IVote = { actor: this.rootStore.user.data?.id ?? "", actor_detail: this.rootStore.user.currentActor, - issue: issueId, - project: projectId, + issue: issueID, + project: projectID, workspace: workspaceSlug, vote: data.vote, }; - const filteredVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.data?.id); + const filteredVotes = this.details[issueID].votes.filter((v) => v.actor !== this.rootStore.user.data?.id); try { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], votes: [...filteredVotes, newVote], }, }; }); - await this.issueService.createIssueVote(workspaceSlug, projectId, issueId, data); + await this.issueService.createIssueVote(anchor, issueID, data); } catch (error) { console.log("Failed to add issue vote"); - const issueVotes = await this.issueService.getIssueVotes(workspaceSlug, projectId, issueId); + const issueVotes = await this.issueService.getIssueVotes(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], votes: issueVotes, }, }; @@ -398,30 +379,30 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - removeIssueVote = async (workspaceSlug: string, projectId: string, issueId: string) => { - const newVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.data?.id); + removeIssueVote = async (anchor: string, issueID: string) => { + const newVotes = this.details[issueID].votes.filter((v) => v.actor !== this.rootStore.user.data?.id); try { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], votes: newVotes, }, }; }); - await this.issueService.deleteIssueVote(workspaceSlug, projectId, issueId); + await this.issueService.deleteIssueVote(anchor, issueID); } catch (error) { console.log("Failed to remove issue vote"); - const issueVotes = await this.issueService.getIssueVotes(workspaceSlug, projectId, issueId); + const issueVotes = await this.issueService.getIssueVotes(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], votes: issueVotes, }, }; diff --git a/space/store/issue-filters.store.ts b/space/store/issue-filters.store.ts index b7b311af4..daf797f90 100644 --- a/space/store/issue-filters.store.ts +++ b/space/store/issue-filters.store.ts @@ -1,7 +1,7 @@ import cloneDeep from "lodash/cloneDeep"; import isEqual from "lodash/isEqual"; import set from "lodash/set"; -import { action, makeObservable, observable, runInAction, computed } from "mobx"; +import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; @@ -19,16 +19,17 @@ import { export interface IIssueFilterStore { // observables layoutOptions: TIssueLayoutOptions; - filters: { [projectId: string]: TIssueFilters } | undefined; + filters: { [anchor: string]: TIssueFilters } | undefined; // computed - issueFilters: TIssueFilters | undefined; - appliedFilters: TIssueQueryFiltersParams | undefined; - isIssueFiltersUpdated: (filters: TIssueFilters) => boolean; + isIssueFiltersUpdated: (anchor: string, filters: TIssueFilters) => boolean; + // helpers + getIssueFilters: (anchor: string) => TIssueFilters | undefined; + getAppliedFilters: (anchor: string) => TIssueQueryFiltersParams | undefined; // actions updateLayoutOptions: (layout: TIssueLayoutOptions) => void; - initIssueFilters: (projectId: string, filters: TIssueFilters) => void; + initIssueFilters: (anchor: string, filters: TIssueFilters) => void; updateIssueFilters: ( - projectId: string, + anchor: string, filterKind: K, filterKey: keyof TIssueFilters[K], filters: TIssueFilters[K][typeof filterKey] @@ -44,16 +45,13 @@ export class IssueFilterStore implements IIssueFilterStore { gantt: false, spreadsheet: false, }; - filters: { [projectId: string]: TIssueFilters } | undefined = undefined; + filters: { [anchor: string]: TIssueFilters } | undefined = undefined; constructor(private store: RootStore) { makeObservable(this, { // observables layoutOptions: observable, filters: observable, - // computed - issueFilters: computed, - appliedFilters: computed, // actions updateLayoutOptions: action, initIssueFilters: action, @@ -82,79 +80,70 @@ export class IssueFilterStore implements IIssueFilterStore { }; // computed - get issueFilters() { - const projectId = this.store.project.project?.id; - if (!projectId) return undefined; - - const currentFilters = this.filters?.[projectId]; - if (!currentFilters) return undefined; - + getIssueFilters = computedFn((anchor: string) => { + const currentFilters = this.filters?.[anchor]; return currentFilters; - } + }); - get appliedFilters() { - const currentIssueFilters = this.issueFilters; - if (!currentIssueFilters) return undefined; + getAppliedFilters = computedFn((anchor: string) => { + const issueFilters = this.getIssueFilters(anchor); + if (!issueFilters) return undefined; - const currentLayout = currentIssueFilters?.display_filters?.layout; + const currentLayout = issueFilters?.display_filters?.layout; if (!currentLayout) return undefined; const currentFilters: TIssueQueryFilters = { - priority: currentIssueFilters?.filters?.priority || undefined, - state: currentIssueFilters?.filters?.state || undefined, - labels: currentIssueFilters?.filters?.labels || undefined, + priority: issueFilters?.filters?.priority || undefined, + state: issueFilters?.filters?.state || undefined, + labels: issueFilters?.filters?.labels || undefined, }; const filteredParams = ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[currentLayout]?.filters || []; const currentFilterQueryParams: TIssueQueryFiltersParams = this.computedFilter(currentFilters, filteredParams); return currentFilterQueryParams; - } + }); - isIssueFiltersUpdated = computedFn((userFilters: TIssueFilters) => { - if (!this.issueFilters) return false; + isIssueFiltersUpdated = computedFn((anchor: string, userFilters: TIssueFilters) => { + const issueFilters = this.getIssueFilters(anchor); + if (!issueFilters) return false; const currentUserFilters = cloneDeep(userFilters?.filters || {}); - const currentIssueFilters = cloneDeep(this.issueFilters?.filters || {}); + const currentIssueFilters = cloneDeep(issueFilters?.filters || {}); return isEqual(currentUserFilters, currentIssueFilters); }); // actions updateLayoutOptions = (options: TIssueLayoutOptions) => set(this, ["layoutOptions"], options); - initIssueFilters = async (projectId: string, initFilters: TIssueFilters) => { + initIssueFilters = async (anchor: string, initFilters: TIssueFilters) => { try { - if (!projectId) return; if (this.filters === undefined) runInAction(() => (this.filters = {})); - if (this.filters && initFilters) set(this.filters, [projectId], initFilters); + if (this.filters && initFilters) set(this.filters, [anchor], initFilters); - const workspaceSlug = this.store.project.workspace?.slug; - const currentAppliedFilters = this.appliedFilters; + const appliedFilters = this.getAppliedFilters(anchor); - if (!workspaceSlug) return; - await this.store.issue.fetchPublicIssues(workspaceSlug, projectId, currentAppliedFilters); + await this.store.issue.fetchPublicIssues(anchor, appliedFilters); } catch (error) { throw error; } }; updateIssueFilters = async ( - projectId: string, + anchor: string, filterKind: K, filterKey: keyof TIssueFilters[K], filterValue: TIssueFilters[K][typeof filterKey] ) => { try { - if (!projectId || !filterKind || !filterKey || !filterValue) return; + if (!filterKind || !filterKey || !filterValue) return; if (this.filters === undefined) runInAction(() => (this.filters = {})); runInAction(() => { - if (this.filters) set(this.filters, [projectId, filterKind, filterKey], filterValue); + if (this.filters) set(this.filters, [anchor, filterKind, filterKey], filterValue); }); - const workspaceSlug = this.store.project.workspace?.slug; - const currentAppliedFilters = this.appliedFilters; + const appliedFilters = this.getAppliedFilters(anchor); - if (!workspaceSlug) return; - await this.store.issue.fetchPublicIssues(workspaceSlug, projectId, currentAppliedFilters); + await this.store.issue.fetchPublicIssues(anchor, appliedFilters); } catch (error) { throw error; } diff --git a/space/store/issue.store.ts b/space/store/issue.store.ts index 7967aafb1..4f2d845b5 100644 --- a/space/store/issue.store.ts +++ b/space/store/issue.store.ts @@ -1,87 +1,87 @@ import { observable, action, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; +// types +import { IStateLite } from "@plane/types"; // services import IssueService from "@/services/issue.service"; // types -import { IIssue, IIssueState, IIssueLabel } from "@/types/issue"; +import { IIssue, IIssueLabel } from "@/types/issue"; // store import { RootStore } from "./root.store"; -// import { IssueDetailType, TIssueBoardKeys } from "types/issue"; export interface IIssueStore { loader: boolean; error: any; - // issue options - issues: IIssue[] | null; - states: IIssueState[] | null; - labels: IIssueLabel[] | null; - // filtering + // observables + issues: IIssue[]; + states: IStateLite[]; + labels: IIssueLabel[]; + // filter observables filteredStates: string[]; filteredLabels: string[]; filteredPriorities: string[]; - // service - issueService: any; // actions - fetchPublicIssues: (workspace_slug: string, project_slug: string, params: any) => Promise; - getCountOfIssuesByState: (state: string) => number; - getFilteredIssuesByState: (state: string) => IIssue[]; + fetchPublicIssues: (anchor: string, params: any) => Promise; + // helpers + getCountOfIssuesByState: (stateID: string) => number; + getFilteredIssuesByState: (stateID: string) => IIssue[]; } export class IssueStore implements IIssueStore { loader: boolean = false; error: any | null = null; - - states: IIssueState[] | null = []; - labels: IIssueLabel[] | null = []; - + // observables + states: IStateLite[] = []; + labels: IIssueLabel[] = []; + issues: IIssue[] = []; + // filter observables filteredStates: string[] = []; filteredLabels: string[] = []; filteredPriorities: string[] = []; - - issues: IIssue[] | null = []; - issue_detail: any = {}; - + // root store rootStore: RootStore; - issueService: any; + // services + issueService: IssueService; - constructor(_rootStore: any) { + constructor(_rootStore: RootStore) { makeObservable(this, { - // observable - loader: observable, + loader: observable.ref, error: observable, - // issue options - states: observable.ref, - labels: observable.ref, - // filtering - filteredStates: observable.ref, - filteredLabels: observable.ref, - filteredPriorities: observable.ref, - // issues - issues: observable.ref, - issue_detail: observable.ref, + // observables + states: observable, + labels: observable, + issues: observable, + // filter observables + filteredStates: observable, + filteredLabels: observable, + filteredPriorities: observable, // actions fetchPublicIssues: action, - getFilteredIssuesByState: action, }); this.rootStore = _rootStore; this.issueService = new IssueService(); } - fetchPublicIssues = async (workspaceSlug: string, projectId: string, params: any) => { + /** + * @description fetch issues, states and labels + * @param {string} anchor + * @param params + */ + fetchPublicIssues = async (anchor: string, params: any) => { try { - this.loader = true; - this.error = null; + runInAction(() => { + this.loader = true; + this.error = null; + }); - const response = await this.issueService.getPublicIssues(workspaceSlug, projectId, params); + const response = await this.issueService.fetchPublicIssues(anchor, params); if (response) { - const states: IIssueState[] = [...response?.states]; - const labels: IIssueLabel[] = [...response?.labels]; - const issues: IIssue[] = [...response?.issues]; runInAction(() => { - this.states = states; - this.labels = labels; - this.issues = issues; + this.states = response.states; + this.labels = response.labels; + this.issues = response.issues; this.loader = false; }); } @@ -91,11 +91,21 @@ export class IssueStore implements IIssueStore { } }; - // computed - getCountOfIssuesByState(state_id: string): number { - return this.issues?.filter((issue) => issue.state == state_id).length || 0; - } + /** + * @description get total count of issues under a particular state + * @param {string} stateID + * @returns {number} + */ + getCountOfIssuesByState = computedFn( + (stateID: string) => this.issues?.filter((issue) => issue.state == stateID).length || 0 + ); - getFilteredIssuesByState = (state_id: string): IIssue[] | [] => - this.issues?.filter((issue) => issue.state == state_id) || []; + /** + * @description get array of issues under a particular state + * @param {string} stateID + * @returns {IIssue[]} + */ + getFilteredIssuesByState = computedFn( + (stateID: string) => this.issues?.filter((issue) => issue.state == stateID) || [] + ); } diff --git a/space/store/project.store.ts b/space/store/project.store.ts deleted file mode 100644 index 02f250323..000000000 --- a/space/store/project.store.ts +++ /dev/null @@ -1,96 +0,0 @@ -// mobx -import { observable, action, makeObservable, runInAction, computed } from "mobx"; -// service -import ProjectService from "@/services/project.service"; -// store types -import { RootStore } from "@/store/root.store"; -// types -import { TWorkspaceDetails, TProjectDetails, TProjectSettings } from "@/types/project"; - -export interface IProjectStore { - // observables - loader: boolean; - error: any | undefined; - settings: TProjectSettings | undefined; - workspace: TWorkspaceDetails | undefined; - project: TProjectDetails | undefined; - canReact: boolean; - canComment: boolean; - canVote: boolean; - // actions - fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise; - hydrate: (projectSettings: any) => void; -} - -export class ProjectStore implements IProjectStore { - // observables - loader: boolean = false; - error: any | undefined = undefined; - settings: TProjectSettings | undefined = undefined; - workspace: TWorkspaceDetails | undefined = undefined; - project: TProjectDetails | undefined = undefined; - // service - projectService; - - constructor(private store: RootStore) { - makeObservable(this, { - // loaders and error observables - loader: observable, - error: observable.ref, - // observable - workspace: observable, - project: observable, - settings: observable, - // computed - canReact: computed, - canComment: computed, - canVote: computed, - // actions - fetchProjectSettings: action, - hydrate: action, - }); - // services - this.projectService = new ProjectService(); - } - - // computed - get canReact() { - return this.settings?.reactions ?? false; - } - get canComment() { - return this.settings?.comments ?? false; - } - get canVote() { - return this.settings?.votes ?? false; - } - - fetchProjectSettings = async (workspace_slug: string, project_slug: string) => { - try { - this.loader = true; - this.error = null; - - const response = await this.projectService.getProjectSettings(workspace_slug, project_slug); - - if (response) { - this.store.issueFilter.updateLayoutOptions(response?.views); - runInAction(() => { - this.project = response?.project_details; - this.workspace = response?.workspace_detail; - this.settings = response; - this.loader = false; - }); - } - return response; - } catch (error) { - this.loader = false; - this.error = error; - return error; - } - }; - - hydrate = (projectSettings: TProjectSettings) => { - const { workspace_detail, project_details } = projectSettings; - this.workspace = workspace_detail; - this.project = project_details; - }; -} diff --git a/space/store/publish/publish.store.ts b/space/store/publish/publish.store.ts new file mode 100644 index 000000000..29cbc53ab --- /dev/null +++ b/space/store/publish/publish.store.ts @@ -0,0 +1,111 @@ +import { observable, makeObservable, computed } from "mobx"; +// types +import { IWorkspaceLite, TProjectDetails, TPublishEntityType, TPublishSettings, TPublishViewProps } from "@plane/types"; +// store types +import { RootStore } from "@/store/root.store"; + +export interface IPublishStore extends TPublishSettings { + // computed + workspaceSlug: string | undefined; + canComment: boolean; + canReact: boolean; + canVote: boolean; +} + +export class PublishStore implements IPublishStore { + // observables + anchor: string | undefined; + is_comments_enabled: boolean; + created_at: string | undefined; + created_by: string | undefined; + entity_identifier: string | undefined; + entity_name: TPublishEntityType | undefined; + id: string | undefined; + inbox: unknown; + project: string | undefined; + project_details: TProjectDetails | undefined; + is_reactions_enabled: boolean; + updated_at: string | undefined; + updated_by: string | undefined; + view_props: TPublishViewProps | undefined; + is_votes_enabled: boolean; + workspace: string | undefined; + workspace_detail: IWorkspaceLite | undefined; + + constructor( + private store: RootStore, + publishSettings: TPublishSettings + ) { + this.anchor = publishSettings.anchor; + this.is_comments_enabled = publishSettings.is_comments_enabled; + this.created_at = publishSettings.created_at; + this.created_by = publishSettings.created_by; + this.entity_identifier = publishSettings.entity_identifier; + this.entity_name = publishSettings.entity_name; + this.id = publishSettings.id; + this.inbox = publishSettings.inbox; + this.project = publishSettings.project; + this.project_details = publishSettings.project_details; + this.is_reactions_enabled = publishSettings.is_reactions_enabled; + this.updated_at = publishSettings.updated_at; + this.updated_by = publishSettings.updated_by; + this.view_props = publishSettings.view_props; + this.is_votes_enabled = publishSettings.is_votes_enabled; + this.workspace = publishSettings.workspace; + this.workspace_detail = publishSettings.workspace_detail; + + makeObservable(this, { + // observables + anchor: observable.ref, + is_comments_enabled: observable.ref, + created_at: observable.ref, + created_by: observable.ref, + entity_identifier: observable.ref, + entity_name: observable.ref, + id: observable.ref, + inbox: observable, + project: observable.ref, + project_details: observable, + is_reactions_enabled: observable.ref, + updated_at: observable.ref, + updated_by: observable.ref, + view_props: observable, + is_votes_enabled: observable.ref, + workspace: observable.ref, + workspace_detail: observable, + // computed + workspaceSlug: computed, + canComment: computed, + canReact: computed, + canVote: computed, + }); + } + + /** + * @description returns the workspace slug from the workspace details + */ + get workspaceSlug() { + return this?.workspace_detail?.slug ?? undefined; + } + + /** + * @description returns whether commenting is enabled or not + */ + get canComment() { + return !!this.is_comments_enabled; + } + + /** + * @description returns whether reacting is enabled or not + */ + get canReact() { + return !!this.is_reactions_enabled; + } + + /** + * @description returns whether voting is enabled or not + */ + get canVote() { + return !!this.is_votes_enabled; + } +} diff --git a/space/store/publish/publish_list.store.ts b/space/store/publish/publish_list.store.ts new file mode 100644 index 000000000..b6722115d --- /dev/null +++ b/space/store/publish/publish_list.store.ts @@ -0,0 +1,55 @@ +import set from "lodash/set"; +import { makeObservable, observable, runInAction, action } from "mobx"; +// types +import { TPublishSettings } from "@plane/types"; +// services +import PublishService from "@/services/publish.service"; +// store +import { PublishStore } from "@/store/publish/publish.store"; +// store +import { RootStore } from "../root.store"; + +export interface IPublishListStore { + // observables + publishMap: Record; // anchor => PublishStore + // actions + fetchPublishSettings: (pageId: string) => Promise; +} + +export class PublishListStore implements IPublishListStore { + // observables + publishMap: Record = {}; // anchor => PublishStore + // service + publishService; + + constructor(private store: RootStore) { + makeObservable(this, { + // observables + publishMap: observable, + // actions + fetchPublishSettings: action, + }); + // services + this.publishService = new PublishService(); + } + + /** + * @description fetch publish settings + * @param {string} anchor + */ + fetchPublishSettings = async (anchor: string) => { + try { + const response = await this.publishService.fetchPublishSettings(anchor); + runInAction(() => { + if (response.anchor && response.view_props) { + this.store.issueFilter.updateLayoutOptions(response?.view_props); + set(this.publishMap, [response.anchor], new PublishStore(this.store, response)); + } + }); + + return response; + } catch (error) { + throw error; + } + }; +} diff --git a/space/store/root.store.ts b/space/store/root.store.ts index 4a31840db..082220f5d 100644 --- a/space/store/root.store.ts +++ b/space/store/root.store.ts @@ -3,30 +3,30 @@ import { enableStaticRendering } from "mobx-react-lite"; import { IInstanceStore, InstanceStore } from "@/store/instance.store"; import { IssueDetailStore, IIssueDetailStore } from "@/store/issue-detail.store"; import { IssueStore, IIssueStore } from "@/store/issue.store"; -import { IProjectStore, ProjectStore } from "@/store/project.store"; import { IUserStore, UserStore } from "@/store/user.store"; import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store"; import { IMentionsStore, MentionsStore } from "./mentions.store"; +import { IPublishListStore, PublishListStore } from "./publish/publish_list.store"; enableStaticRendering(typeof window === "undefined"); export class RootStore { instance: IInstanceStore; user: IUserStore; - project: IProjectStore; issue: IIssueStore; issueDetail: IIssueDetailStore; mentionStore: IMentionsStore; issueFilter: IIssueFilterStore; + publishList: IPublishListStore; constructor() { this.instance = new InstanceStore(this); this.user = new UserStore(this); - this.project = new ProjectStore(this); this.issue = new IssueStore(this); this.issueDetail = new IssueDetailStore(this); this.mentionStore = new MentionsStore(this); this.issueFilter = new IssueFilterStore(this); + this.publishList = new PublishListStore(this); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -40,10 +40,10 @@ export class RootStore { localStorage.setItem("theme", "system"); this.instance = new InstanceStore(this); this.user = new UserStore(this); - this.project = new ProjectStore(this); this.issue = new IssueStore(this); this.issueDetail = new IssueDetailStore(this); this.mentionStore = new MentionsStore(this); this.issueFilter = new IssueFilterStore(this); + this.publishList = new PublishListStore(this); }; } diff --git a/space/styles/globals.css b/space/styles/globals.css index 47804b768..0b41d8481 100644 --- a/space/styles/globals.css +++ b/space/styles/globals.css @@ -302,6 +302,23 @@ } } +* { + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + font-variant-ligatures: none; + -webkit-font-variant-ligatures: none; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} + +body { + color: rgba(var(--color-text-100)); +} + ::-webkit-scrollbar { width: 5px; height: 5px; diff --git a/space/types/app.d.ts b/space/types/app.d.ts deleted file mode 100644 index bd4af3b0c..000000000 --- a/space/types/app.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface IAppConfig { - email_password_login: boolean; - file_size_limit: number; - google_client_id: string | null; - github_app_name: string | null; - github_client_id: string | null; - magic_login: boolean; - slack_client_id: string | null; - posthog_api_key: string | null; - posthog_host: string | null; - has_openai_configured: boolean; - has_unsplash_configured: boolean; - is_self_managed: boolean; -} diff --git a/space/types/issue.d.ts b/space/types/issue.d.ts index f2625fb76..5b729d1c0 100644 --- a/space/types/issue.d.ts +++ b/space/types/issue.d.ts @@ -1,27 +1,17 @@ +import { IStateLite, IWorkspaceLite, TIssuePriorities, TStateGroups } from "@plane/types"; + export type TIssueLayout = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt"; export type TIssueLayoutOptions = { [key in TIssueLayout]: boolean; }; -export type TIssueLayoutViews = { - [key in TIssueLayout]: { title: string; icon: string; className: string }; -}; -export type TIssueFilterPriority = "urgent" | "high" | "medium" | "low" | "none"; export type TIssueFilterPriorityObject = { - key: TIssueFilterPriority; + key: TIssuePriorities; title: string; className: string; icon: string; }; -export type TIssueFilterState = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; -export type TIssueFilterStateObject = { - key: TIssueFilterState; - title: string; - color: string; - className: string; -}; - export type TIssueFilterKeys = "priority" | "state" | "labels"; export type TDisplayFilters = { @@ -29,8 +19,8 @@ export type TDisplayFilters = { }; export type TFilters = { - state: TIssueFilterState[]; - priority: TIssueFilterPriority[]; + state: TStateGroups[]; + priority: TIssuePriorities[]; labels: string[]; }; @@ -43,6 +33,12 @@ export type TIssueQueryFilters = Partial; export type TIssueQueryFiltersParams = Partial>; +export type TIssuesResponse = { + states: IStateLite[]; + labels: IIssueLabel[]; + issues: IIssue[]; +}; + export interface IIssue { id: string; comments: Comment[]; @@ -68,17 +64,11 @@ export interface IIssue { export type IPeekMode = "side" | "modal" | "full"; -export interface IIssueState { - id: string; - name: string; - group: TIssueGroupKey; - color: string; -} - export interface IIssueLabel { id: string; name: string; color: string; + parent: string | null; } export interface IVote { @@ -114,7 +104,7 @@ export interface Comment { updated_at: Date; updated_by: string; workspace: string; - workspace_detail: WorkspaceDetail; + workspace_detail: IWorkspaceLite; } export interface IIssueReaction { @@ -175,52 +165,8 @@ export interface ProjectDetail { description: string; } -export interface WorkspaceDetail { - name: string; - slug: string; - id: string; -} - -export interface IssueDetailType { - [issueId: string]: { - issue: IIssue; - comments: Comment[]; - reactions: any[]; - votes: any[]; - }; -} - -export type TIssueGroupByOptions = "state" | "priority" | "labels" | null; - -export type TIssueParams = "priority" | "state" | "labels"; - export interface IIssueFilterOptions { state?: string[] | null; labels?: string[] | null; priority?: string[] | null; } - -// issues -export interface IGroupedIssues { - [group_id: string]: string[]; -} - -export interface ISubGroupedIssues { - [sub_grouped_id: string]: { - [group_id: string]: string[]; - }; -} - -export type TUnGroupedIssues = string[]; - -export interface IIssueResponse { - [issue_id: string]: IIssue; -} - -export type TLoader = "init-loader" | "mutation" | undefined; - -export interface ViewFlags { - enableQuickAdd: boolean; - enableIssueCreation: boolean; - enableInlineEditing: boolean; -} diff --git a/space/types/project.d.ts b/space/types/project.d.ts deleted file mode 100644 index 90c89ed80..000000000 --- a/space/types/project.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { TLogoProps } from "@plane/types"; - -export type TWorkspaceDetails = { - name: string; - slug: string; - id: string; -}; - -export type TViewDetails = { - list: boolean; - gantt: boolean; - kanban: boolean; - calendar: boolean; - spreadsheet: boolean; -}; - -export type TProjectDetails = { - id: string; - identifier: string; - name: string; - cover_image: string | undefined; - logo_props: TLogoProps; - description: string; -}; - -export type TProjectSettings = { - id: string; - anchor: string; - comments: boolean; - reactions: boolean; - votes: boolean; - inbox: unknown; - workspace: string; - workspace_detail: TWorkspaceDetails; - project: string; - project_details: TProjectDetails; - views: TViewDetails; - created_by: string; - updated_by: string; - created_at: string; - updated_at: string; -}; diff --git a/space/types/theme.d.ts b/space/types/theme.d.ts deleted file mode 100644 index ca306be51..000000000 --- a/space/types/theme.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IThemeStore { - theme: string; - setTheme: (theme: "light" | "dark" | string) => void; -} diff --git a/space/types/user.d.ts b/space/types/user.d.ts deleted file mode 100644 index d58827876..000000000 --- a/space/types/user.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface IUser { - avatar: string; - cover_image: string | null; - created_at: Date; - created_location: string; - date_joined: Date; - email: string; - display_name: string; - first_name: string; - id: string; - is_email_verified: boolean; - is_onboarded: boolean; - is_tour_completed: boolean; - last_location: string; - last_login: Date; - last_name: string; - mobile_number: string; - role: string; - is_password_autoset: boolean; - onboarding_step: { - workspace_join?: boolean; - profile_complete?: boolean; - workspace_create?: boolean; - workspace_invite?: boolean; - }; - token: string; - updated_at: Date; - username: string; - user_timezone: string; -} diff --git a/web/components/analytics/custom-analytics/select/y-axis.tsx b/web/components/analytics/custom-analytics/select/y-axis.tsx index a33feb967..1baf010d2 100644 --- a/web/components/analytics/custom-analytics/select/y-axis.tsx +++ b/web/components/analytics/custom-analytics/select/y-axis.tsx @@ -1,26 +1,54 @@ -// ui +import { observer } from "mobx-react"; import { TYAxisValues } from "@plane/types"; import { CustomSelect } from "@plane/ui"; -// types -import { ANALYTICS_Y_AXIS_VALUES } from "@/constants/analytics"; // constants +import { ANALYTICS_Y_AXIS_VALUES } from "@/constants/analytics"; +import { EEstimateSystem } from "@/constants/estimates"; +// hooks +import { useAppRouter, useProjectEstimates } from "@/hooks/store"; type Props = { value: TYAxisValues; onChange: () => void; }; -export const SelectYAxis: React.FC = ({ value, onChange }) => ( - {ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"}} - onChange={onChange} - maxHeight="lg" - > - {ANALYTICS_Y_AXIS_VALUES.map((item) => ( - - {item.label} - - ))} - -); +export const SelectYAxis: React.FC = observer(({ value, onChange }) => { + // hooks + const { projectId } = useAppRouter(); + const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); + + const isEstimateEnabled = (analyticsOption: string) => { + if (analyticsOption === "estimate") { + if ( + projectId && + currentActiveEstimateId && + areEstimateEnabledByProjectId(projectId) && + estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS + ) { + return true; + } else { + return false; + } + } + + return true; + }; + + return ( + {ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"}} + onChange={onChange} + maxHeight="lg" + > + {ANALYTICS_Y_AXIS_VALUES.map( + (item) => + isEstimateEnabled(item.value) && ( + + {item.label} + + ) + )} + + ); +}); diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 28d84ffe4..5def2d7a9 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -24,7 +24,7 @@ import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; import { capitalizeFirstLetter } from "@/helpers/string.helper"; -import { useEstimate, useLabel } from "@/hooks/store"; +import { useLabel } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; // types @@ -97,22 +97,6 @@ const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; works ); }); -const EstimatePoint = observer((props: { point: string }) => { - const { point } = props; - const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate(); - const currentPoint = Number(point) + 1; - - const estimateValue = getEstimatePointValue(Number(point), null); - - return ( - - {areEstimatesEnabledForCurrentProject - ? estimateValue - : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} - - ); -}); - const inboxActivityMessage = { declined: { showIssue: "declined issue", @@ -267,7 +251,7 @@ const activityDetails: { else return ( <> - set the estimate point to + set the estimate point to {activity.new_value} {showIssue && ( <> {" "} diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index 099b90254..0c9c30b31 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef, Fragment } from "react"; +import React, { useEffect, useState, useRef, Fragment, Ref } from "react"; import { Placement } from "@popperjs/core"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; // services @@ -196,7 +196,7 @@ export const GptAssistantPopover: React.FC = (props) => { } style={styles.popper} {...attributes.popper} > diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx index 58243cc22..99e8106de 100644 --- a/web/components/dropdowns/estimate.tsx +++ b/web/components/dropdowns/estimate.tsx @@ -1,5 +1,4 @@ import { Fragment, ReactNode, useRef, useState } from "react"; -import sortBy from "lodash/sortBy"; import { observer } from "mobx-react"; import { usePopper } from "react-popper"; import { Check, ChevronDown, Search, Triangle } from "lucide-react"; @@ -7,7 +6,12 @@ import { Combobox } from "@headlessui/react"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useAppRouter, useEstimate } from "@/hooks/store"; +import { + useAppRouter, + useEstimate, + useProjectEstimates, + // useEstimate +} from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; // components import { DropdownButton } from "./buttons"; @@ -19,15 +23,15 @@ type Props = TDropdownProps & { button?: ReactNode; dropdownArrow?: boolean; dropdownArrowClassName?: string; - onChange: (val: number | null) => void; + onChange: (val: string | undefined) => void; onClose?: () => void; projectId: string; - value: number | null; + value: string | undefined; }; type DropdownOptions = | { - value: number | null; + value: string | null; query: string; content: JSX.Element; }[] @@ -76,19 +80,29 @@ export const EstimateDropdown: React.FC = observer((props) => { }); // store hooks const { workspaceSlug } = useAppRouter(); - const { fetchProjectEstimates, getProjectActiveEstimateDetails, getEstimatePointValue } = useEstimate(); - const activeEstimate = getProjectActiveEstimateDetails(projectId); - const options: DropdownOptions = sortBy(activeEstimate?.points ?? [], "key")?.map((point) => ({ - value: point.key, - query: `${point?.value}`, - content: ( -
- - {point.value} -
- ), - })); + const { currentActiveEstimateId, getProjectEstimates } = useProjectEstimates(); + const { estimatePointIds, estimatePointById } = useEstimate( + currentActiveEstimateId ? currentActiveEstimateId : undefined + ); + + const options: DropdownOptions = (estimatePointIds ?? []) + ?.map((estimatePoint) => { + const currentEstimatePoint = estimatePointById(estimatePoint); + if (currentEstimatePoint) + return { + value: currentEstimatePoint.id, + query: `${currentEstimatePoint?.value}`, + content: ( +
+ + {currentEstimatePoint.value} +
+ ), + }; + else undefined; + }) + .filter((estimatePointDropdownOption) => estimatePointDropdownOption != undefined) as DropdownOptions; options?.unshift({ value: null, query: "No estimate", @@ -103,10 +117,10 @@ export const EstimateDropdown: React.FC = observer((props) => { const filteredOptions = query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null; + const selectedEstimate = value && estimatePointById ? estimatePointById(value) : undefined; const onOpen = async () => { - if (!activeEstimate && workspaceSlug) await fetchProjectEstimates(workspaceSlug, projectId); + if (!currentActiveEstimateId && workspaceSlug) await getProjectEstimates(workspaceSlug, projectId); }; const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({ @@ -120,7 +134,7 @@ export const EstimateDropdown: React.FC = observer((props) => { setQuery, }); - const dropdownOnChange = (val: number | null) => { + const dropdownOnChange = (val: string | undefined) => { onChange(val); handleClose(); }; @@ -164,13 +178,13 @@ export const EstimateDropdown: React.FC = observer((props) => { className={buttonClassName} isActive={isOpen} tooltipHeading="Estimate" - tooltipContent={selectedEstimate !== null ? selectedEstimate : placeholder} + tooltipContent={selectedEstimate ? selectedEstimate?.value : placeholder} showTooltip={showTooltip} variant={buttonVariant} > {!hideIcon && } {(selectedEstimate || placeholder) && BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( - {selectedEstimate !== null ? selectedEstimate : placeholder} + {selectedEstimate ? selectedEstimate?.value : placeholder} )} {dropdownArrow && (