diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 4f895aeba..e84b6dd0a 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -80,7 +80,7 @@ class CycleViewSet(BaseViewSet): issue_id=str(self.kwargs.get("pk", None)), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_destroy(instance) @@ -102,48 +102,84 @@ class CycleViewSet(BaseViewSet): .select_related("workspace") .select_related("owned_by") .annotate(is_favorite=Exists(subquery)) - .annotate(total_issues=Count("issue_cycle")) + .annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) .annotate( completed_issues=Count( "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="completed"), + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate( cancelled_issues=Count( "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="cancelled"), + filter=Q( + issue_cycle__issue__state__group="cancelled", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate( started_issues=Count( "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="started"), + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate( unstarted_issues=Count( "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="unstarted"), + filter=Q( + issue_cycle__issue__state__group="unstarted", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate( backlog_issues=Count( "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="backlog"), + filter=Q( + issue_cycle__issue__state__group="backlog", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) .annotate( completed_estimates=Sum( "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="completed"), + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate( started_estimates=Sum( "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="started"), + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .prefetch_related( @@ -196,17 +232,30 @@ class CycleViewSet(BaseViewSet): .annotate(assignee_id=F("assignees__id")) .annotate(avatar=F("assignees__avatar")) .values("display_name", "assignee_id", "avatar") - .annotate(total_issues=Count("assignee_id")) + .annotate( + total_issues=Count( + "assignee_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) .annotate( completed_issues=Count( "assignee_id", - filter=Q(completed_at__isnull=False), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) ) .annotate( pending_issues=Count( "assignee_id", - filter=Q(completed_at__isnull=True), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) ) .order_by("display_name") @@ -222,17 +271,30 @@ class CycleViewSet(BaseViewSet): .annotate(color=F("labels__color")) .annotate(label_id=F("labels__id")) .values("label_name", "color", "label_id") - .annotate(total_issues=Count("label_id")) + .annotate( + total_issues=Count( + "label_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ) + ) .annotate( completed_issues=Count( "label_id", - filter=Q(completed_at__isnull=False), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) ) .annotate( pending_issues=Count( "label_id", - filter=Q(completed_at__isnull=True), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) ) .order_by("label_name") @@ -385,17 +447,30 @@ class CycleViewSet(BaseViewSet): .values( "first_name", "last_name", "assignee_id", "avatar", "display_name" ) - .annotate(total_issues=Count("assignee_id")) + .annotate( + total_issues=Count( + "assignee_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) .annotate( completed_issues=Count( "assignee_id", - filter=Q(completed_at__isnull=False), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) ) .annotate( pending_issues=Count( "assignee_id", - filter=Q(completed_at__isnull=True), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) ) .order_by("first_name", "last_name") @@ -412,17 +487,30 @@ class CycleViewSet(BaseViewSet): .annotate(color=F("labels__color")) .annotate(label_id=F("labels__id")) .values("label_name", "color", "label_id") - .annotate(total_issues=Count("label_id")) + .annotate( + total_issues=Count( + "label_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) .annotate( completed_issues=Count( "label_id", - filter=Q(completed_at__isnull=False), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) ) .annotate( pending_issues=Count( "label_id", - filter=Q(completed_at__isnull=True), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) ) .order_by("label_name") @@ -488,7 +576,7 @@ class CycleIssueViewSet(BaseViewSet): issue_id=str(self.kwargs.get("pk", None)), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_destroy(instance) @@ -664,7 +752,7 @@ class CycleIssueViewSet(BaseViewSet): ), } ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) # Return all Cycle Issues diff --git a/apiserver/plane/api/views/importer.py b/apiserver/plane/api/views/importer.py index 0a92b3850..18d9a1d69 100644 --- a/apiserver/plane/api/views/importer.py +++ b/apiserver/plane/api/views/importer.py @@ -384,7 +384,7 @@ class BulkImportIssuesEndpoint(BaseAPIView): sort_order=largest_sort_order, start_date=issue_data.get("start_date", None), target_date=issue_data.get("target_date", None), - priority=issue_data.get("priority", None), + priority=issue_data.get("priority", "none"), created_by=request.user, ) ) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 1a0284ea4..79294275e 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -173,12 +173,12 @@ class InboxIssueViewSet(BaseViewSet): ) # Check for valid priority - if not request.data.get("issue", {}).get("priority", None) in [ + if not request.data.get("issue", {}).get("priority", "none") in [ "low", "medium", "high", "urgent", - None, + "none", ]: return Response( {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST @@ -213,7 +213,7 @@ class InboxIssueViewSet(BaseViewSet): issue_id=str(issue.id), project_id=str(project_id), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) # create an inbox issue InboxIssue.objects.create( @@ -278,7 +278,7 @@ class InboxIssueViewSet(BaseViewSet): IssueSerializer(current_instance).data, cls=DjangoJSONEncoder, ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) issue_serializer.save() else: @@ -480,12 +480,12 @@ class InboxIssuePublicViewSet(BaseViewSet): ) # Check for valid priority - if not request.data.get("issue", {}).get("priority", None) in [ + if not request.data.get("issue", {}).get("priority", "none") in [ "low", "medium", "high", "urgent", - None, + "none", ]: return Response( {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST @@ -520,7 +520,7 @@ class InboxIssuePublicViewSet(BaseViewSet): issue_id=str(issue.id), project_id=str(project_id), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) # create an inbox issue InboxIssue.objects.create( @@ -585,7 +585,7 @@ class InboxIssuePublicViewSet(BaseViewSet): IssueSerializer(current_instance).data, cls=DjangoJSONEncoder, ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) issue_serializer.save() return Response(issue_serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index e653f3d44..003a8ae32 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -130,7 +130,7 @@ class IssueViewSet(BaseViewSet): current_instance=json.dumps( IssueSerializer(current_instance).data, cls=DjangoJSONEncoder ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_update(serializer) @@ -151,7 +151,7 @@ class IssueViewSet(BaseViewSet): current_instance=json.dumps( IssueSerializer(current_instance).data, cls=DjangoJSONEncoder ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_destroy(instance) @@ -318,7 +318,7 @@ class IssueViewSet(BaseViewSet): issue_id=str(serializer.data.get("id", None)), project_id=str(project_id), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -577,7 +577,7 @@ class IssueCommentViewSet(BaseViewSet): issue_id=str(self.kwargs.get("issue_id")), project_id=str(self.kwargs.get("project_id")), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) def perform_update(self, serializer): @@ -596,7 +596,7 @@ class IssueCommentViewSet(BaseViewSet): IssueCommentSerializer(current_instance).data, cls=DjangoJSONEncoder, ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_update(serializer) @@ -618,7 +618,7 @@ class IssueCommentViewSet(BaseViewSet): IssueCommentSerializer(current_instance).data, cls=DjangoJSONEncoder, ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_destroy(instance) @@ -902,7 +902,7 @@ class IssueLinkViewSet(BaseViewSet): issue_id=str(self.kwargs.get("issue_id")), project_id=str(self.kwargs.get("project_id")), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) def perform_update(self, serializer): @@ -921,7 +921,7 @@ class IssueLinkViewSet(BaseViewSet): IssueLinkSerializer(current_instance).data, cls=DjangoJSONEncoder, ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_update(serializer) @@ -943,7 +943,7 @@ class IssueLinkViewSet(BaseViewSet): IssueLinkSerializer(current_instance).data, cls=DjangoJSONEncoder, ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_destroy(instance) @@ -1022,7 +1022,7 @@ class IssueAttachmentEndpoint(BaseAPIView): serializer.data, cls=DjangoJSONEncoder, ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -1045,7 +1045,7 @@ class IssueAttachmentEndpoint(BaseAPIView): issue_id=str(self.kwargs.get("issue_id", None)), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return Response(status=status.HTTP_204_NO_CONTENT) @@ -1248,7 +1248,7 @@ class IssueArchiveViewSet(BaseViewSet): issue_id=str(issue.id), project_id=str(project_id), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) @@ -1453,7 +1453,7 @@ class IssueReactionViewSet(BaseViewSet): issue_id=str(self.kwargs.get("issue_id", None)), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) def destroy(self, request, slug, project_id, issue_id, reaction_code): @@ -1477,7 +1477,7 @@ class IssueReactionViewSet(BaseViewSet): "identifier": str(issue_reaction.id), } ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) issue_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -1526,7 +1526,7 @@ class CommentReactionViewSet(BaseViewSet): issue_id=None, project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) def destroy(self, request, slug, project_id, comment_id, reaction_code): @@ -1551,7 +1551,7 @@ class CommentReactionViewSet(BaseViewSet): "comment_id": str(comment_id), } ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) comment_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -1648,7 +1648,7 @@ class IssueCommentPublicViewSet(BaseViewSet): issue_id=str(issue_id), project_id=str(project_id), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) if not ProjectMember.objects.filter( project_id=project_id, @@ -1698,7 +1698,7 @@ class IssueCommentPublicViewSet(BaseViewSet): IssueCommentSerializer(comment).data, cls=DjangoJSONEncoder, ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -1732,7 +1732,7 @@ class IssueCommentPublicViewSet(BaseViewSet): IssueCommentSerializer(comment).data, cls=DjangoJSONEncoder, ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) comment.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -1807,7 +1807,7 @@ class IssueReactionPublicViewSet(BaseViewSet): issue_id=str(self.kwargs.get("issue_id", None)), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -1852,7 +1852,7 @@ class IssueReactionPublicViewSet(BaseViewSet): "identifier": str(issue_reaction.id), } ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) issue_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -1926,7 +1926,7 @@ class CommentReactionPublicViewSet(BaseViewSet): issue_id=None, project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -1978,7 +1978,7 @@ class CommentReactionPublicViewSet(BaseViewSet): "comment_id": str(comment_id), } ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) comment_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -2042,7 +2042,7 @@ class IssueVotePublicViewSet(BaseViewSet): issue_id=str(self.kwargs.get("issue_id", None)), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) serializer = IssueVoteSerializer(issue_vote) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -2077,7 +2077,7 @@ class IssueVotePublicViewSet(BaseViewSet): "identifier": str(issue_vote.id), } ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) issue_vote.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -2111,7 +2111,7 @@ class IssueRelationViewSet(BaseViewSet): IssueRelationSerializer(current_instance).data, cls=DjangoJSONEncoder, ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_destroy(instance) @@ -2145,7 +2145,7 @@ class IssueRelationViewSet(BaseViewSet): issue_id=str(issue_id), project_id=str(project_id), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) if relation == "blocking": @@ -2417,7 +2417,7 @@ class IssueDraftViewSet(BaseViewSet): current_instance=json.dumps( IssueSerializer(current_instance).data, cls=DjangoJSONEncoder ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_update(serializer) @@ -2439,6 +2439,7 @@ class IssueDraftViewSet(BaseViewSet): current_instance=json.dumps( IssueSerializer(current_instance).data, cls=DjangoJSONEncoder ), + epoch=int(timezone.now().timestamp()) ) return super().perform_destroy(instance) @@ -2602,7 +2603,7 @@ class IssueDraftViewSet(BaseViewSet): issue_id=str(serializer.data.get("id", None)), project_id=str(project_id), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index c2a15da1c..1489edb2d 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -40,6 +40,7 @@ from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters from plane.utils.analytics_plot import burndown_plot + class ModuleViewSet(BaseViewSet): model = Module permission_classes = [ @@ -78,35 +79,63 @@ class ModuleViewSet(BaseViewSet): queryset=ModuleLink.objects.select_related("module", "created_by"), ) ) - .annotate(total_issues=Count("issue_module")) + .annotate( + total_issues=Count( + "issue_module", + filter=Q( + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ), + ) .annotate( completed_issues=Count( "issue_module__issue__state__group", - filter=Q(issue_module__issue__state__group="completed"), + filter=Q( + issue_module__issue__state__group="completed", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), ) ) .annotate( cancelled_issues=Count( "issue_module__issue__state__group", - filter=Q(issue_module__issue__state__group="cancelled"), + filter=Q( + issue_module__issue__state__group="cancelled", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), ) ) .annotate( started_issues=Count( "issue_module__issue__state__group", - filter=Q(issue_module__issue__state__group="started"), + filter=Q( + issue_module__issue__state__group="started", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), ) ) .annotate( unstarted_issues=Count( "issue_module__issue__state__group", - filter=Q(issue_module__issue__state__group="unstarted"), + filter=Q( + issue_module__issue__state__group="unstarted", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), ) ) .annotate( backlog_issues=Count( "issue_module__issue__state__group", - filter=Q(issue_module__issue__state__group="backlog"), + filter=Q( + issue_module__issue__state__group="backlog", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), ) ) .order_by(order_by, "name") @@ -130,7 +159,7 @@ class ModuleViewSet(BaseViewSet): issue_id=str(self.kwargs.get("pk", None)), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_destroy(instance) @@ -179,18 +208,36 @@ class ModuleViewSet(BaseViewSet): .annotate(assignee_id=F("assignees__id")) .annotate(display_name=F("assignees__display_name")) .annotate(avatar=F("assignees__avatar")) - .values("first_name", "last_name", "assignee_id", "avatar", "display_name") - .annotate(total_issues=Count("assignee_id")) + .values( + "first_name", "last_name", "assignee_id", "avatar", "display_name" + ) + .annotate( + total_issues=Count( + "assignee_id", + filter=Q( + archived_at__isnull=True, + is_draft=False, + ), + ) + ) .annotate( completed_issues=Count( "assignee_id", - filter=Q(completed_at__isnull=False), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) ) .annotate( pending_issues=Count( "assignee_id", - filter=Q(completed_at__isnull=True), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) ) .order_by("first_name", "last_name") @@ -206,17 +253,33 @@ class ModuleViewSet(BaseViewSet): .annotate(color=F("labels__color")) .annotate(label_id=F("labels__id")) .values("label_name", "color", "label_id") - .annotate(total_issues=Count("label_id")) + .annotate( + total_issues=Count( + "label_id", + filter=Q( + archived_at__isnull=True, + is_draft=False, + ), + ), + ) .annotate( completed_issues=Count( "label_id", - filter=Q(completed_at__isnull=False), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) ) .annotate( pending_issues=Count( "label_id", - filter=Q(completed_at__isnull=True), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) ) .order_by("label_name") @@ -279,7 +342,7 @@ class ModuleIssueViewSet(BaseViewSet): issue_id=str(self.kwargs.get("pk", None)), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return super().perform_destroy(instance) @@ -447,7 +510,7 @@ class ModuleIssueViewSet(BaseViewSet): ), } ), - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) return Response( @@ -494,7 +557,6 @@ class ModuleLinkViewSet(BaseViewSet): class ModuleFavoriteViewSet(BaseViewSet): - serializer_class = ModuleFavoriteSerializer model = ModuleFavorite diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 2d1ee8132..753fd861b 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1239,13 +1239,21 @@ class WorkspaceUserProfileEndpoint(BaseAPIView): .annotate( created_issues=Count( "project_issue", - filter=Q(project_issue__created_by_id=user_id), + filter=Q( + project_issue__created_by_id=user_id, + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), ) ) .annotate( assigned_issues=Count( "project_issue", - filter=Q(project_issue__assignees__in=[user_id]), + filter=Q( + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), ) ) .annotate( @@ -1254,6 +1262,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView): filter=Q( project_issue__completed_at__isnull=False, project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, ), ) ) @@ -1267,6 +1277,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView): "started", ], project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, ), ) ) @@ -1317,6 +1329,11 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): def get(self, request, slug, user_id): try: filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + order_by_param = request.GET.get("order_by", "-created_at") issue_queryset = ( Issue.issue_objects.filter( diff --git a/apiserver/plane/bgtasks/exporter_expired_task.py b/apiserver/plane/bgtasks/exporter_expired_task.py index a77d68b4b..45c53eaca 100644 --- a/apiserver/plane/bgtasks/exporter_expired_task.py +++ b/apiserver/plane/bgtasks/exporter_expired_task.py @@ -32,7 +32,7 @@ def delete_old_s3_link(): else: s3 = boto3.client( "s3", - region_name="ap-south-1", + region_name=settings.AWS_REGION, aws_access_key_id=settings.AWS_ACCESS_KEY_ID, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, config=Config(signature_version="s3v4"), diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 733defe69..6d33dfc4f 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -121,36 +121,20 @@ def track_priority( epoch ): if current_instance.get("priority") != requested_data.get("priority"): - if requested_data.get("priority") == None: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("priority"), - new_value=None, - field="priority", - project=project, - workspace=project.workspace, - comment=f"updated the priority to None", - epoch=epoch, - ) - ) - else: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("priority"), - new_value=requested_data.get("priority"), - field="priority", - project=project, - workspace=project.workspace, - comment=f"updated the priority to {requested_data.get('priority')}", - epoch=epoch, - ) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="updated", + old_value=current_instance.get("priority"), + new_value=requested_data.get("priority"), + field="priority", + project=project, + workspace=project.workspace, + comment=f"updated the priority to {requested_data.get('priority')}", + epoch=epoch, ) + ) # Track chnages in state of the issue diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index f7b06c625..a1b42073f 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -77,7 +77,7 @@ def archive_old_issues(): project_id=project_id, current_instance=None, subscriber=False, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) for issue in updated_issues ] @@ -149,7 +149,7 @@ def close_old_issues(): project_id=project_id, current_instance=None, subscriber=False, - epoch = int(timezone.now().timestamp()) + epoch=int(timezone.now().timestamp()) ) for issue in updated_issues ] diff --git a/apiserver/plane/db/migrations/0047_auto_20230921_0758.py b/apiserver/plane/db/migrations/0047_auto_20230921_0758.py new file mode 100644 index 000000000..4344963cd --- /dev/null +++ b/apiserver/plane/db/migrations/0047_auto_20230921_0758.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.3 on 2023-09-21 07:58 + + +from django.db import migrations + + +def update_priority_history(apps, schema_editor): + IssueActivity = apps.get_model("db", "IssueActivity") + updated_issue_activity = [] + for obj in IssueActivity.objects.all(): + if obj.field == "priority": + obj.new_value = obj.new_value or "none" + obj.old_value = obj.old_value or "none" + updated_issue_activity.append(obj) + IssueActivity.objects.bulk_update( + updated_issue_activity, ["new_value", "old_value"], batch_size=100 + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("db", "0046_auto_20230919_1421"), + ] + + operations = [ + migrations.RunPython(update_priority_history), + ] diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 4cd2134ac..08d825b59 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -26,7 +26,7 @@ ROLE_CHOICES = ( def get_default_props(): return { "filters": { - "priority": None, + "priority": "none", "state": None, "state_group": None, "assignees": None, diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index c85268435..e063d873a 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -17,7 +17,7 @@ ROLE_CHOICES = ( def get_default_props(): return { "filters": { - "priority": None, + "priority": "none", "state": None, "state_group": None, "assignees": None, diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index 60e751459..bffbb4c2a 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -74,10 +74,10 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None): sorted_data = grouped_data if temp_axis == "priority": - order = ["low", "medium", "high", "urgent", "None"] + order = ["low", "medium", "high", "urgent", "none"] sorted_data = {key: grouped_data[key] for key in order if key in grouped_data} else: - sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "None", x[0]))) + sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "none", x[0]))) return sorted_data diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 226d909cd..3a869113c 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -40,9 +40,6 @@ def filter_priority(params, filter, method): priorities = params.get("priority").split(",") if len(priorities) and "" not in priorities: filter["priority__in"] = priorities - else: - if params.get("priority", None) and len(params.get("priority")): - filter["priority__in"] = params.get("priority") return filter diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index 0014dfe86..498f37b84 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -1,113 +1,61 @@ version: "3.8" -x-api-and-worker-env: - &api-and-worker-env - DEBUG: ${DEBUG} - SENTRY_DSN: ${SENTRY_DSN} - DJANGO_SETTINGS_MODULE: plane.settings.selfhosted - DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} - REDIS_URL: redis://plane-redis:6379/ - EMAIL_HOST: ${EMAIL_HOST} - EMAIL_HOST_USER: ${EMAIL_HOST_USER} - EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} - EMAIL_PORT: ${EMAIL_PORT} - EMAIL_FROM: ${EMAIL_FROM} - EMAIL_USE_TLS: ${EMAIL_USE_TLS} - EMAIL_USE_SSL: ${EMAIL_USE_SSL} - AWS_REGION: ${AWS_REGION} - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} - FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} - WEB_URL: ${WEB_URL} - GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} - DISABLE_COLLECTSTATIC: 1 - DOCKERIZED: 1 - OPENAI_API_BASE: ${OPENAI_API_BASE} - OPENAI_API_KEY: ${OPENAI_API_KEY} - GPT_ENGINE: ${GPT_ENGINE} - SECRET_KEY: ${SECRET_KEY} - DEFAULT_EMAIL: ${DEFAULT_EMAIL} - DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} - USE_MINIO: ${USE_MINIO} - ENABLE_SIGNUP: ${ENABLE_SIGNUP} - services: - plane-web: - container_name: planefrontend + web: + container_name: web image: makeplane/plane-frontend:latest restart: always command: /usr/local/bin/start.sh web/server.js web env_file: - - .env - environment: - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} - NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL} - NEXT_PUBLIC_GOOGLE_CLIENTID: "0" - NEXT_PUBLIC_GITHUB_APP_NAME: "0" - NEXT_PUBLIC_GITHUB_ID: "0" - NEXT_PUBLIC_SENTRY_DSN: "0" - NEXT_PUBLIC_ENABLE_OAUTH: "0" - NEXT_PUBLIC_ENABLE_SENTRY: "0" - NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0" - NEXT_PUBLIC_TRACK_EVENTS: "0" + - ./web/.env depends_on: - - plane-api - - plane-worker + - api + - worker - plane-deploy: - container_name: planedeploy - image: makeplane/plane-deploy:latest + space: + container_name: space + image: makeplane/plane-space:latest restart: always command: /usr/local/bin/start.sh space/server.js space env_file: - - .env - environment: - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + - ./space/.env depends_on: - - plane-api - - plane-worker - - plane-web + - api + - worker + - web - plane-api: - container_name: planebackend + api: + container_name: api image: makeplane/plane-backend:latest restart: always command: ./bin/takeoff env_file: - - .env - environment: - <<: *api-and-worker-env + - ./apiserver/.env depends_on: - plane-db - plane-redis - plane-worker: - container_name: planebgworker + worker: + container_name: bgworker image: makeplane/plane-backend:latest restart: always command: ./bin/worker env_file: - - .env - environment: - <<: *api-and-worker-env + - ./apiserver/.env depends_on: - - plane-api + - api - plane-db - plane-redis - plane-beat-worker: - container_name: planebeatworker + beat-worker: + container_name: beatworker image: makeplane/plane-backend:latest restart: always command: ./bin/beat env_file: - - .env - environment: - <<: *api-and-worker-env + - ./apiserver/.env depends_on: - - plane-api + - api - plane-db - plane-redis @@ -157,8 +105,8 @@ services: - plane-minio # Comment this if you already have a reverse proxy running - plane-proxy: - container_name: planeproxy + proxy: + container_name: proxy image: makeplane/plane-proxy:latest ports: - ${NGINX_PORT}:80 @@ -168,8 +116,9 @@ services: FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} depends_on: - - plane-web - - plane-api + - web + - api + - space volumes: pgdata: diff --git a/docker-compose.yml b/docker-compose.yml index e3c1b37be..0895aa1ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: "3.8" services: - plane-web: - container_name: planefrontend + web: + container_name: web build: context: . dockerfile: ./web/Dockerfile.web @@ -11,11 +11,11 @@ services: restart: always command: /usr/local/bin/start.sh web/server.js web depends_on: - - plane-api - - plane-worker + - api + - worker - plane-deploy: - container_name: planedeploy + space: + container_name: space build: context: . dockerfile: ./space/Dockerfile.space @@ -24,12 +24,12 @@ services: restart: always command: /usr/local/bin/start.sh space/server.js space depends_on: - - plane-api - - plane-worker - - plane-web + - api + - worker + - web - plane-api: - container_name: planebackend + api: + container_name: api build: context: ./apiserver dockerfile: Dockerfile.api @@ -43,8 +43,8 @@ services: - plane-db - plane-redis - plane-worker: - container_name: planebgworker + worker: + container_name: bgworker build: context: ./apiserver dockerfile: Dockerfile.api @@ -55,12 +55,12 @@ services: env_file: - ./apiserver/.env depends_on: - - plane-api + - api - plane-db - plane-redis - plane-beat-worker: - container_name: planebeatworker + beat-worker: + container_name: beatworker build: context: ./apiserver dockerfile: Dockerfile.api @@ -71,7 +71,7 @@ services: env_file: - ./apiserver/.env depends_on: - - plane-api + - api - plane-db - plane-redis @@ -118,8 +118,8 @@ services: - plane-minio # Comment this if you already have a reverse proxy running - plane-proxy: - container_name: planeproxy + proxy: + container_name: proxy build: context: ./nginx dockerfile: Dockerfile @@ -130,8 +130,9 @@ services: FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} depends_on: - - plane-web - - plane-api + - web + - api + - space volumes: pgdata: diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template index 36a68fa55..af80b04fa 100644 --- a/nginx/nginx.conf.template +++ b/nginx/nginx.conf.template @@ -1,25 +1,26 @@ -events { } +events { +} http { sendfile on; server { - listen 80; - root /www/data/; + listen 80; + root /www/data/; access_log /var/log/nginx/access.log; client_max_body_size ${FILE_SIZE_LIMIT}; location / { - proxy_pass http://planefrontend:3000/; + proxy_pass http://web:3000/; } location /api/ { - proxy_pass http://planebackend:8000/api/; + proxy_pass http://api:8000/api/; } location /spaces/ { - proxy_pass http://planedeploy:3000/spaces/; + proxy_pass http://space:3000/spaces/; } location /${BUCKET_NAME}/ { diff --git a/space/.env.example b/space/.env.example index 56e9f1e95..7700ec946 100644 --- a/space/.env.example +++ b/space/.env.example @@ -1,4 +1,2 @@ -# Google Client ID for Google OAuth -NEXT_PUBLIC_GOOGLE_CLIENTID="" # Flag to toggle OAuth NEXT_PUBLIC_ENABLE_OAUTH=0 \ No newline at end of file diff --git a/web/.env.example b/web/.env.example index 88a2064c5..3868cd834 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,24 +1,4 @@ -# Extra image domains that need to be added for Next Image -NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS= -# Google Client ID for Google OAuth -NEXT_PUBLIC_GOOGLE_CLIENTID="" -# GitHub App ID for GitHub OAuth -NEXT_PUBLIC_GITHUB_ID="" -# GitHub App Name for GitHub Integration -NEXT_PUBLIC_GITHUB_APP_NAME="" -# Sentry DSN for error monitoring -NEXT_PUBLIC_SENTRY_DSN="" # Enable/Disable OAUTH - default 0 for selfhosted instance NEXT_PUBLIC_ENABLE_OAUTH=0 -# Enable/Disable Sentry -NEXT_PUBLIC_ENABLE_SENTRY=0 -# Enable/Disable session recording -NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0 -# Enable/Disable event tracking -NEXT_PUBLIC_TRACK_EVENTS=0 -# Slack Client ID for Slack Integration -NEXT_PUBLIC_SLACK_CLIENT_ID="" -# For Telemetry, set it to "app.plane.so" -NEXT_PUBLIC_PLAUSIBLE_DOMAIN="" # Public boards deploy URL -NEXT_PUBLIC_DEPLOY_URL="http://localhost:3000/spaces" \ No newline at end of file +NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces" \ No newline at end of file diff --git a/web/components/core/views/board-view/index.ts b/web/components/core/views/board-view/index.ts index 6e5cdf8bf..a5a6ee497 100644 --- a/web/components/core/views/board-view/index.ts +++ b/web/components/core/views/board-view/index.ts @@ -2,3 +2,4 @@ export * from "./all-boards"; export * from "./board-header"; export * from "./single-board"; export * from "./single-issue"; +export * from "./inline-create-issue-form"; diff --git a/web/components/core/views/board-view/inline-create-issue-form.tsx b/web/components/core/views/board-view/inline-create-issue-form.tsx new file mode 100644 index 000000000..f4810164d --- /dev/null +++ b/web/components/core/views/board-view/inline-create-issue-form.tsx @@ -0,0 +1,62 @@ +import { useEffect } from "react"; + +// react hook form +import { useFormContext } from "react-hook-form"; + +// components +import { InlineCreateIssueFormWrapper } from "components/core"; + +// hooks +import useProjectDetails from "hooks/use-project-details"; + +// types +import { IIssue } from "types"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + onSuccess?: (data: IIssue) => Promise | void; + prePopulatedData?: Partial; +}; + +const InlineInput = () => { + const { projectDetails } = useProjectDetails(); + + const { register, setFocus } = useFormContext(); + + useEffect(() => { + setFocus("name"); + }, [setFocus]); + + return ( +
+

+ {projectDetails?.identifier ?? "..."} +

+ +
+ ); +}; + +export const BoardInlineCreateIssueForm: React.FC = (props) => ( + <> + + + + {props.isOpen && ( +

+ Press {"'"}Enter{"'"} to add another issue +

+ )} + +); diff --git a/web/components/core/views/board-view/single-board.tsx b/web/components/core/views/board-view/single-board.tsx index 1981e1f7c..3e174ead2 100644 --- a/web/components/core/views/board-view/single-board.tsx +++ b/web/components/core/views/board-view/single-board.tsx @@ -6,7 +6,7 @@ import { useRouter } from "next/router"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import { Draggable } from "react-beautiful-dnd"; // components -import { BoardHeader, SingleBoardIssue } from "components/core"; +import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core"; // ui import { CustomMenu } from "components/ui"; // icons @@ -34,26 +34,30 @@ type Props = { viewProps: IIssueViewProps; }; -export const SingleBoard: React.FC = ({ - addIssueToGroup, - currentState, - groupTitle, - disableUserActions, - disableAddIssueOption = false, - dragDisabled, - handleIssueAction, - handleDraftIssueAction, - handleTrashBox, - openIssuesListModal, - handleMyIssueOpen, - removeIssue, - user, - userAuth, - viewProps, -}) => { +export const SingleBoard: React.FC = (props) => { + const { + addIssueToGroup, + currentState, + groupTitle, + disableUserActions, + disableAddIssueOption = false, + dragDisabled, + handleIssueAction, + handleDraftIssueAction, + handleTrashBox, + openIssuesListModal, + handleMyIssueOpen, + removeIssue, + user, + userAuth, + viewProps, + } = props; + // collapse/expand const [isCollapsed, setIsCollapsed] = useState(true); + const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false); + const { displayFilters, groupedIssues } = viewProps; const router = useRouter(); @@ -67,6 +71,24 @@ export const SingleBoard: React.FC = ({ const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions; + const onCreateClick = () => { + setIsInlineCreateIssueFormOpen(true); + + const boardListElement = document.getElementById(`board-list-${groupTitle}`); + + // timeout is needed because the animation + // takes time to complete & we can scroll only after that + const timeoutId = setTimeout(() => { + if (boardListElement) + boardListElement.scrollBy({ + top: boardListElement.scrollHeight, + left: 0, + behavior: "smooth", + }); + clearTimeout(timeoutId); + }, 10); + }; + return (
= ({ )}
= ({ > <>{provided.placeholder} + + setIsInlineCreateIssueFormOpen(false)} + prePopulatedData={{ + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + [displayFilters?.group_by! === "labels" + ? "labels_list" + : displayFilters?.group_by!]: + displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle, + }} + />
{displayFilters?.group_by !== "created_by" && (
@@ -177,7 +213,7 @@ export const SingleBoard: React.FC = ({ @@ -224,7 +230,9 @@ export const SingleList: React.FC = ({ position="right" noBorder > - Create new + setIsCreateIssueFormOpen(true)}> + Create new + {openIssuesListModal && ( Add an existing issue @@ -284,6 +292,29 @@ export const SingleList: React.FC = ({ ) : (
Loading...
)} + + setIsCreateIssueFormOpen(false)} + prePopulatedData={{ + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + [displayFilters?.group_by!]: groupTitle, + }} + /> + + {!isCreateIssueFormOpen && ( +
+ +
+ )}
diff --git a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx index 1076f30d0..0b4634b97 100644 --- a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx +++ b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx @@ -4,7 +4,7 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; // components -import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; +import { SpreadsheetColumns, SpreadsheetIssues, ListInlineCreateIssueForm } from "components/core"; import { CustomMenu, Spinner } from "components/ui"; import { IssuePeekOverview } from "components/issues"; // hooks @@ -33,6 +33,7 @@ export const SpreadsheetView: React.FC = ({ userAuth, }) => { const [expandedIssues, setExpandedIssues] = useState([]); + const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; @@ -88,53 +89,59 @@ export const SpreadsheetView: React.FC = ({ userAuth={userAuth} /> ))} + + setIsInlineCreateIssueFormOpen(false)} + prePopulatedData={{ + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + }} + /> +
- {type === "issue" ? ( - - ) : ( - !disableUserActions && ( - - - Add Issue - - } - position="left" - optionsClassName="left-5 !w-36" - noBorder - > - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} + {!isInlineCreateIssueFormOpen && ( + <> + {type === "issue" ? ( + + ) : ( + !disableUserActions && ( + + + Add Issue + + } + position="left" + optionsClassName="left-5 !w-36" + noBorder + > + setIsInlineCreateIssueFormOpen(true)}> + Create new + + {openIssuesListModal && ( + + Add an existing issue + + )} + + ) + )} + )}
diff --git a/web/components/gantt-chart/sidebar.tsx b/web/components/gantt-chart/sidebar.tsx index 92e7a603d..0d90ffdd0 100644 --- a/web/components/gantt-chart/sidebar.tsx +++ b/web/components/gantt-chart/sidebar.tsx @@ -1,3 +1,6 @@ +import { useState } from "react"; +// next +import { useRouter } from "next/router"; // react-beautiful-dnd import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; @@ -7,6 +10,9 @@ import { useChart } from "./hooks"; import { Loader } from "components/ui"; // icons import { EllipsisVerticalIcon } from "@heroicons/react/24/outline"; +import { PlusIcon } from "lucide-react"; +// components +import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form"; // types import { IBlockUpdateData, IGanttBlock } from "./types"; @@ -18,15 +24,16 @@ type Props = { enableReorder: boolean; }; -export const GanttSidebar: React.FC = ({ - title, - blockUpdateHandler, - blocks, - SidebarBlockRender, - enableReorder, -}) => { +export const GanttSidebar: React.FC = (props) => { + const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props; + + const router = useRouter(); + const { cycleId, moduleId } = router.query; + const { activeBlock, dispatch } = useChart(); + const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false); + // update the active block on hover const updateActiveBlock = (block: IGanttBlock | null) => { dispatch({ @@ -148,6 +155,28 @@ export const GanttSidebar: React.FC = ({ )} {droppableProvided.placeholder} + + setIsCreateIssueFormOpen(false)} + prePopulatedData={{ + start_date: new Date(Date.now()).toISOString().split("T")[0], + target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0], + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + }} + /> + + {!isCreateIssueFormOpen && ( + + )} )} diff --git a/web/components/issues/main-content.tsx b/web/components/issues/main-content.tsx index 5e14ba432..c4c9a780a 100644 --- a/web/components/issues/main-content.tsx +++ b/web/components/issues/main-content.tsx @@ -43,7 +43,7 @@ export const IssueMainContent: React.FC = ({ uneditable = false, }) => { const router = useRouter(); - const { workspaceSlug, projectId, issueId, archivedIssueId } = router.query; + const { workspaceSlug, projectId, issueId } = router.query; const { setToastAlert } = useToast(); @@ -206,7 +206,7 @@ export const IssueMainContent: React.FC = ({
- +
diff --git a/web/components/issues/sub-issues/issue.tsx b/web/components/issues/sub-issues/issue.tsx index 2e3d8acdb..1dba3add2 100644 --- a/web/components/issues/sub-issues/issue.tsx +++ b/web/components/issues/sub-issues/issue.tsx @@ -19,6 +19,7 @@ import { Tooltip, CustomMenu } from "components/ui"; // types import { ICurrentUserResponse, IIssue } from "types"; +import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; export interface ISubIssues { workspaceSlug: string; @@ -29,8 +30,8 @@ export interface ISubIssues { user: ICurrentUserResponse | undefined; editable: boolean; removeIssueFromSubIssues: (parentIssueId: string, issue: IIssue) => void; - issuesVisibility: string[]; - handleIssuesVisibility: (issueId: string) => void; + issuesLoader: ISubIssuesRootLoaders; + handleIssuesLoader: ({ key, issueId }: ISubIssuesRootLoadersHandler) => void; copyText: (text: string) => void; handleIssueCrudOperation: ( key: "create" | "existing" | "edit" | "delete", @@ -48,8 +49,8 @@ export const SubIssues: React.FC = ({ user, editable, removeIssueFromSubIssues, - issuesVisibility, - handleIssuesVisibility, + issuesLoader, + handleIssuesLoader, copyText, handleIssueCrudOperation, }) => ( @@ -62,19 +63,21 @@ export const SubIssues: React.FC = ({
{issue?.sub_issues_count > 0 && ( <> - {true ? ( + {issuesLoader.sub_issues.includes(issue?.id) ? ( +
+ +
+ ) : (
handleIssuesVisibility(issue?.id)} + onClick={() => handleIssuesLoader({ key: "visibility", issueId: issue?.id })} > - {issuesVisibility && issuesVisibility.includes(issue?.id) ? ( + {issuesLoader && issuesLoader.visibility.includes(issue?.id) ? ( ) : ( )}
- ) : ( - )} )} @@ -92,7 +95,7 @@ export const SubIssues: React.FC = ({ {issue.project_detail.identifier}-{issue?.sequence_id}
-
{issue?.name}
+
{issue?.name}
@@ -132,7 +135,11 @@ export const SubIssues: React.FC = ({ )} - + + copyText(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`) + } + >
Copy issue link @@ -142,17 +149,28 @@ export const SubIssues: React.FC = ({
{editable && ( -
removeIssueFromSubIssues(parentIssue?.id, issue)} - > - -
+ <> + {issuesLoader.delete.includes(issue?.id) ? ( +
+ +
+ ) : ( +
{ + handleIssuesLoader({ key: "delete", issueId: issue?.id }); + removeIssueFromSubIssues(parentIssue?.id, issue); + }} + > + +
+ )} + )}
)} - {issuesVisibility.includes(issue?.id) && issue?.sub_issues_count > 0 && ( + {issuesLoader.visibility.includes(issue?.id) && issue?.sub_issues_count > 0 && ( = ({ user={user} editable={editable} removeIssueFromSubIssues={removeIssueFromSubIssues} - issuesVisibility={issuesVisibility} - handleIssuesVisibility={handleIssuesVisibility} + issuesLoader={issuesLoader} + handleIssuesLoader={handleIssuesLoader} copyText={copyText} handleIssueCrudOperation={handleIssueCrudOperation} /> diff --git a/web/components/issues/sub-issues/issues-list.tsx b/web/components/issues/sub-issues/issues-list.tsx index 45c0b1882..c79e64e91 100644 --- a/web/components/issues/sub-issues/issues-list.tsx +++ b/web/components/issues/sub-issues/issues-list.tsx @@ -5,6 +5,8 @@ import useSWR from "swr"; import { SubIssues } from "./issue"; // types import { ICurrentUserResponse, IIssue } from "types"; + +import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; // services import issuesService from "services/issues.service"; // fetch keys @@ -18,8 +20,8 @@ export interface ISubIssuesRootList { user: ICurrentUserResponse | undefined; editable: boolean; removeIssueFromSubIssues: (parentIssueId: string, issue: IIssue) => void; - issuesVisibility: string[]; - handleIssuesVisibility: (issueId: string) => void; + issuesLoader: ISubIssuesRootLoaders; + handleIssuesLoader: ({ key, issueId }: ISubIssuesRootLoadersHandler) => void; copyText: (text: string) => void; handleIssueCrudOperation: ( key: "create" | "existing" | "edit" | "delete", @@ -36,8 +38,8 @@ export const SubIssuesRootList: React.FC = ({ user, editable, removeIssueFromSubIssues, - issuesVisibility, - handleIssuesVisibility, + issuesLoader, + handleIssuesLoader, copyText, handleIssueCrudOperation, }) => { @@ -50,6 +52,16 @@ export const SubIssuesRootList: React.FC = ({ : null ); + React.useEffect(() => { + if (isLoading) { + handleIssuesLoader({ key: "sub_issues", issueId: parentIssue?.id }); + } else { + if (issuesLoader.sub_issues.includes(parentIssue?.id)) { + handleIssuesLoader({ key: "sub_issues", issueId: parentIssue?.id }); + } + } + }, [isLoading]); + return (
{issues && @@ -66,8 +78,8 @@ export const SubIssuesRootList: React.FC = ({ user={user} editable={editable} removeIssueFromSubIssues={removeIssueFromSubIssues} - issuesVisibility={issuesVisibility} - handleIssuesVisibility={handleIssuesVisibility} + issuesLoader={issuesLoader} + handleIssuesLoader={handleIssuesLoader} copyText={copyText} handleIssueCrudOperation={handleIssueCrudOperation} /> diff --git a/web/components/issues/sub-issues/progressbar.tsx b/web/components/issues/sub-issues/progressbar.tsx index 368078a3d..dee91263b 100644 --- a/web/components/issues/sub-issues/progressbar.tsx +++ b/web/components/issues/sub-issues/progressbar.tsx @@ -14,7 +14,7 @@ export const ProgressBar = ({ total = 0, done = 0 }: IProgressBar) => {
diff --git a/web/components/issues/sub-issues/properties.tsx b/web/components/issues/sub-issues/properties.tsx index a899efdcf..4f2dca43b 100644 --- a/web/components/issues/sub-issues/properties.tsx +++ b/web/components/issues/sub-issues/properties.tsx @@ -86,12 +86,14 @@ export const IssueProperty: React.FC = ({ }; const handleAssigneeChange = (data: any) => { - const newData = issue.assignees ?? []; + let newData = issue.assignees ?? []; - if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); - else newData.push(data); + if (newData && newData.length > 0) { + if (newData.includes(data)) newData = newData.splice(newData.indexOf(data), 1); + else newData = [...newData, data]; + } else newData = [...newData, data]; - partialUpdateIssue({ assignees_list: data }); + partialUpdateIssue({ assignees_list: data, assignees: data }); trackEventServices.trackIssuePartialPropertyUpdateEvent( { diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index 75161e639..889ebfd2e 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -14,6 +14,9 @@ import { ProgressBar } from "./progressbar"; import { CustomMenu } from "components/ui"; // hooks import { useProjectMyMembership } from "contexts/project-member.context"; + +import useToast from "hooks/use-toast"; + // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types @@ -25,18 +28,28 @@ import { SUB_ISSUES } from "constants/fetch-keys"; export interface ISubIssuesRoot { parentIssue: IIssue; - user: ICurrentUserResponse | undefined; - editable: boolean; } -export const SubIssuesRoot: React.FC = ({ parentIssue, user, editable }) => { +export interface ISubIssuesRootLoaders { + visibility: string[]; + delete: string[]; + sub_issues: string[]; +} +export interface ISubIssuesRootLoadersHandler { + key: "visibility" | "delete" | "sub_issues"; + issueId: string; +} + +export const SubIssuesRoot: React.FC = ({ parentIssue, user }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; const { memberRole } = useProjectMyMembership(); - const { data: issues } = useSWR( + const { setToastAlert } = useToast(); + + const { data: issues, isLoading } = useSWR( workspaceSlug && projectId && parentIssue && parentIssue?.id ? SUB_ISSUES(parentIssue?.id) : null, @@ -45,13 +58,18 @@ export const SubIssuesRoot: React.FC = ({ parentIssue, user, edi : null ); - const [issuesVisibility, setIssuesVisibility] = React.useState([parentIssue?.id]); - const handleIssuesVisibility = (issueId: string) => { - if (issuesVisibility.includes(issueId)) { - setIssuesVisibility(issuesVisibility.filter((i: string) => i !== issueId)); - } else { - setIssuesVisibility([...issuesVisibility, issueId]); - } + const [issuesLoader, setIssuesLoader] = React.useState({ + visibility: [parentIssue?.id], + delete: [], + sub_issues: [], + }); + const handleIssuesLoader = ({ key, issueId }: ISubIssuesRootLoadersHandler) => { + setIssuesLoader((previousData: ISubIssuesRootLoaders) => ({ + ...previousData, + [key]: previousData[key].includes(issueId) + ? previousData[key].filter((i: string) => i !== issueId) + : [...previousData[key], issueId], + })); }; const [issueCrudOperation, setIssueCrudOperation] = React.useState<{ @@ -110,8 +128,22 @@ export const SubIssuesRoot: React.FC = ({ parentIssue, user, edi if (!workspaceSlug || !parentIssue || !issue?.id) return; issuesService .patchIssue(workspaceSlug, projectId, issue.id, { parent: null }, user) - .finally(() => { - if (parentIssueId) mutate(SUB_ISSUES(parentIssueId)); + .then(async () => { + if (parentIssueId) await mutate(SUB_ISSUES(parentIssueId)); + handleIssuesLoader({ key: "delete", issueId: issue?.id }); + setToastAlert({ + type: "success", + title: `Issue removed!`, + message: `Issue removed successfully.`, + }); + }) + .catch(() => { + handleIssuesLoader({ key: "delete", issueId: issue?.id }); + setToastAlert({ + type: "warning", + title: `Error!`, + message: `Error, Please try again later.`, + }); }); }; @@ -119,11 +151,11 @@ export const SubIssuesRoot: React.FC = ({ parentIssue, user, edi const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${text}`).then(() => { - // setToastAlert({ - // type: "success", - // title: "Link Copied!", - // message: "Issue link copied to clipboard.", - // }); + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Issue link copied to clipboard.", + }); }); }; @@ -135,148 +167,165 @@ export const SubIssuesRoot: React.FC = ({ parentIssue, user, edi return (
- {parentIssue && parentIssue?.sub_issues_count > 0 ? ( + {!issues && isLoading ? ( +
Loading...
+ ) : ( <> - {/* header */} -
-
handleIssuesVisibility(parentIssue?.id)} - > -
- {issuesVisibility.includes(parentIssue?.id) ? ( - - ) : ( - + {issues && issues?.sub_issues && issues?.sub_issues?.length > 0 ? ( + <> + {/* header */} +
+
+ handleIssuesLoader({ key: "visibility", issueId: parentIssue?.id }) + } + > +
+ {issuesLoader.visibility.includes(parentIssue?.id) ? ( + + ) : ( + + )} +
+
Sub-issues
+
({issues?.sub_issues?.length || 0})
+
+ +
+ +
+ + {isEditable && issuesLoader.visibility.includes(parentIssue?.id) && ( +
+
handleIssueCrudOperation("create", parentIssue?.id)} + > + Add sub-issue +
+
handleIssueCrudOperation("existing", parentIssue?.id)} + > + Add an existing issue +
+
)}
-
Sub-issues
-
({parentIssue?.sub_issues_count})
-
-
- -
- - {isEditable && issuesVisibility.includes(parentIssue?.id) && ( -
-
handleIssueCrudOperation("create", parentIssue?.id)} - > - Add sub-issue + {/* issues */} + {issuesLoader.visibility.includes(parentIssue?.id) && ( +
+
-
handleIssueCrudOperation("existing", parentIssue?.id)} - > - Add an existing issue -
-
- )} -
- - {/* issues */} - {issuesVisibility.includes(parentIssue?.id) && ( -
- -
- )} - - ) : ( - isEditable && ( -
-
No sub issues are available
- <> - - - Add sub-issue - - } - buttonClassName="whitespace-nowrap" - position="left" - noBorder - noChevron - > - handleIssueCrudOperation("create", parentIssue?.id)} - > - Create new - - handleIssueCrudOperation("existing", parentIssue?.id)} - > - Add an existing issue - - + )} -
- ) - )} - - {isEditable && issueCrudOperation?.create?.toggle && ( - handleIssueCrudOperation("create", null)} - /> - )} - - {isEditable && - issueCrudOperation?.existing?.toggle && - issueCrudOperation?.existing?.issueId && ( - handleIssueCrudOperation("existing", null)} - searchParams={{ sub_issue: true, issue_id: issueCrudOperation?.existing?.issueId }} - handleOnSubmit={addAsSubIssueFromExistingIssues} - workspaceLevelToggle - /> - )} - - {isEditable && issueCrudOperation?.edit?.toggle && issueCrudOperation?.edit?.issueId && ( - { - mutateSubIssues(issueCrudOperation?.edit?.issueId); - handleIssueCrudOperation("edit", null, null); - }} - data={issueCrudOperation?.edit?.issue} - /> - )} - - {isEditable && issueCrudOperation?.delete?.toggle && issueCrudOperation?.delete?.issueId && ( - { - mutateSubIssues(issueCrudOperation?.delete?.issueId); - handleIssueCrudOperation("delete", null, null); - }} - data={issueCrudOperation?.delete?.issue} - user={user} - redirection={false} - /> + ) : ( + isEditable && ( +
+
No sub issues are available
+ <> + + + Add sub-issue + + } + buttonClassName="whitespace-nowrap" + position="left" + noBorder + noChevron + > + { + mutateSubIssues(parentIssue?.id); + handleIssueCrudOperation("create", parentIssue?.id); + }} + > + Create new + + { + mutateSubIssues(parentIssue?.id); + handleIssueCrudOperation("existing", parentIssue?.id); + }} + > + Add an existing issue + + + +
+ ) + )} + {isEditable && issueCrudOperation?.create?.toggle && ( + { + mutateSubIssues(issueCrudOperation?.create?.issueId); + handleIssueCrudOperation("create", null); + }} + /> + )} + {isEditable && + issueCrudOperation?.existing?.toggle && + issueCrudOperation?.existing?.issueId && ( + handleIssueCrudOperation("existing", null)} + searchParams={{ sub_issue: true, issue_id: issueCrudOperation?.existing?.issueId }} + handleOnSubmit={addAsSubIssueFromExistingIssues} + workspaceLevelToggle + /> + )} + {isEditable && issueCrudOperation?.edit?.toggle && issueCrudOperation?.edit?.issueId && ( + <> + { + mutateSubIssues(issueCrudOperation?.edit?.issueId); + handleIssueCrudOperation("edit", null, null); + }} + data={issueCrudOperation?.edit?.issue} + /> + + )} + {isEditable && + issueCrudOperation?.delete?.toggle && + issueCrudOperation?.delete?.issueId && ( + { + mutateSubIssues(issueCrudOperation?.delete?.issueId); + handleIssueCrudOperation("delete", null, null); + }} + data={issueCrudOperation?.delete?.issue} + user={user} + redirection={false} + /> + )} + )}
); diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index 0fc84fda1..d1e0e98b7 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -1,3 +1,10 @@ +import { + CYCLE_ISSUES_WITH_PARAMS, + MODULE_ISSUES_WITH_PARAMS, + PROJECT_ISSUES_LIST_WITH_PARAMS, + VIEW_ISSUES, +} from "constants/fetch-keys"; + export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2"); export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); @@ -122,3 +129,65 @@ export const objToQueryParams = (obj: any) => { return params.toString(); }; + +export const getFetchKeysForIssueMutation = (options: { + cycleId?: string | string[]; + moduleId?: string | string[]; + viewId?: string | string[]; + projectId: string; + calendarParams: any; + spreadsheetParams: any; + viewGanttParams: any; + ganttParams: any; +}) => { + const { + cycleId, + moduleId, + viewId, + projectId, + calendarParams, + spreadsheetParams, + viewGanttParams, + ganttParams, + } = options; + + const calendarFetchKey = cycleId + ? { calendarFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) } + : moduleId + ? { calendarFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) } + : viewId + ? { calendarFetchKey: VIEW_ISSUES(viewId.toString(), calendarParams) } + : { + calendarFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS( + projectId?.toString() ?? "", + calendarParams + ), + }; + + const spreadsheetFetchKey = cycleId + ? { spreadsheetFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams) } + : moduleId + ? { spreadsheetFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams) } + : viewId + ? { spreadsheetFetchKey: VIEW_ISSUES(viewId.toString(), spreadsheetParams) } + : { + spreadsheetFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS( + projectId?.toString() ?? "", + spreadsheetParams + ), + }; + + const ganttFetchKey = cycleId + ? { ganttFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), ganttParams) } + : moduleId + ? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) } + : viewId + ? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) } + : { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) }; + + return { + ...calendarFetchKey, + ...spreadsheetFetchKey, + ...ganttFetchKey, + }; +}; diff --git a/web/hooks/gantt-chart/issue-view.tsx b/web/hooks/gantt-chart/issue-view.tsx index 8b24a566c..c2f6972fa 100644 --- a/web/hooks/gantt-chart/issue-view.tsx +++ b/web/hooks/gantt-chart/issue-view.tsx @@ -36,6 +36,7 @@ const useGanttChartIssues = (workspaceSlug: string | undefined, projectId: strin return { ganttIssues, mutateGanttIssues, + params, }; }; diff --git a/web/hooks/use-keypress.tsx b/web/hooks/use-keypress.tsx new file mode 100644 index 000000000..d04cd1445 --- /dev/null +++ b/web/hooks/use-keypress.tsx @@ -0,0 +1,19 @@ +import { useEffect } from "react"; + +const useKeypress = (key: string, callback: () => void) => { + useEffect(() => { + const handleKeydown = (event: KeyboardEvent) => { + if (event.key === key) { + callback(); + } + }; + + document.addEventListener("keydown", handleKeydown); + + return () => { + document.removeEventListener("keydown", handleKeydown); + }; + }); +}; + +export default useKeypress;