forked from github/plane
Merge branch 'develop' of github.com:makeplane/plane into dev/private_bucket_for_attachments
This commit is contained in:
commit
ad9ff684e9
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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(
|
||||
|
@ -121,22 +121,6 @@ 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,
|
||||
|
@ -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
|
||||
]
|
||||
|
27
apiserver/plane/db/migrations/0047_auto_20230921_0758.py
Normal file
27
apiserver/plane/db/migrations/0047_auto_20230921_0758.py
Normal file
@ -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),
|
||||
]
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -15,17 +15,19 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
<div>
|
||||
<h6 className="px-3 text-base font-medium">Pending issues</h6>
|
||||
{defaultAnalytics.pending_issue_user.length > 0 ? (
|
||||
{defaultAnalytics.pending_issue_user && defaultAnalytics.pending_issue_user.length > 0 ? (
|
||||
<BarGraph
|
||||
data={defaultAnalytics.pending_issue_user}
|
||||
indexBy="assignees__display_name"
|
||||
indexBy="assignees__id"
|
||||
keys={["count"]}
|
||||
height="250px"
|
||||
colors={() => `#f97316`}
|
||||
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
|
||||
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) =>
|
||||
d.count > 0 ? d.count : 50
|
||||
)}
|
||||
tooltip={(datum) => {
|
||||
const assignee = defaultAnalytics.pending_issue_user.find(
|
||||
(a) => a.assignees__display_name === `${datum.indexValue}`
|
||||
(a) => a.assignees__id === `${datum.indexValue}`
|
||||
);
|
||||
|
||||
return (
|
||||
@ -39,10 +41,9 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
}}
|
||||
axisBottom={{
|
||||
renderTick: (datum) => {
|
||||
const avatar =
|
||||
defaultAnalytics.pending_issue_user[datum.tickIndex]?.assignees__avatar ?? "";
|
||||
const assignee = defaultAnalytics.pending_issue_user[datum.tickIndex] ?? "";
|
||||
|
||||
if (avatar && avatar !== "")
|
||||
if (assignee && assignee?.assignees__avatar && assignee?.assignees__avatar !== "")
|
||||
return (
|
||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||
<image
|
||||
@ -50,7 +51,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
y={10}
|
||||
width={16}
|
||||
height={16}
|
||||
xlinkHref={avatar}
|
||||
xlinkHref={assignee?.assignees__avatar}
|
||||
style={{ clipPath: "circle(50%)" }}
|
||||
/>
|
||||
</g>
|
||||
@ -60,7 +61,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||
<circle cy={18} r={8} fill="#374151" />
|
||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
||||
{datum.value ? `${datum.value}`.toUpperCase()[0] : "?"}
|
||||
{datum.value ? `${assignee.assignees__display_name}`.toUpperCase()[0] : "?"}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
|
@ -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";
|
||||
|
@ -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> | void;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
};
|
||||
|
||||
const InlineInput = () => {
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const { register, setFocus } = useFormContext();
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium leading-5 text-custom-text-300">
|
||||
{projectDetails?.identifier ?? "..."}
|
||||
</h4>
|
||||
<input
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 pl-0 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BoardInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||
<>
|
||||
<InlineCreateIssueFormWrapper
|
||||
className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow"
|
||||
{...props}
|
||||
>
|
||||
<InlineInput />
|
||||
</InlineCreateIssueFormWrapper>
|
||||
{props.isOpen && (
|
||||
<p className="text-xs ml-3 italic text-custom-text-200">
|
||||
Press {"'"}Enter{"'"} to add another issue
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
@ -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,7 +34,8 @@ type Props = {
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleBoard: React.FC<Props> = ({
|
||||
export const SingleBoard: React.FC<Props> = (props) => {
|
||||
const {
|
||||
addIssueToGroup,
|
||||
currentState,
|
||||
groupTitle,
|
||||
@ -50,10 +51,13 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
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<Props> = ({
|
||||
|
||||
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 (
|
||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||
<BoardHeader
|
||||
@ -115,6 +137,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
id={`board-list-${groupTitle}`}
|
||||
className={`pt-3 ${
|
||||
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
||||
} `}
|
||||
@ -134,6 +157,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
type={type}
|
||||
index={index}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
groupTitle={groupTitle}
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||
@ -169,6 +193,19 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
>
|
||||
<>{provided.placeholder}</>
|
||||
</span>
|
||||
|
||||
<BoardInlineCreateIssueForm
|
||||
isOpen={isInlineCreateIssueFormOpen}
|
||||
handleClose={() => 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,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{displayFilters?.group_by !== "created_by" && (
|
||||
<div>
|
||||
@ -177,7 +214,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
||||
onClick={addIssueToGroup}
|
||||
onClick={() => onCreateClick()}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
@ -197,7 +234,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
position="left"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>
|
||||
<CustomMenu.MenuItem onClick={() => onCreateClick()}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
|
@ -56,6 +56,7 @@ type Props = {
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
groupTitle?: string;
|
||||
index: number;
|
||||
editIssue: () => void;
|
||||
@ -77,6 +78,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
provided,
|
||||
snapshot,
|
||||
issue,
|
||||
projectId,
|
||||
index,
|
||||
editIssue,
|
||||
makeIssueCopy,
|
||||
@ -104,7 +106,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
const { displayFilters, properties, mutateIssues } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId } = router.query;
|
||||
|
||||
const isDraftIssue = router.pathname.includes("draft-issues");
|
||||
|
||||
@ -452,6 +454,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
projectId={projectId}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
@ -479,6 +482,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
@ -489,6 +493,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
|
@ -2,3 +2,4 @@ export * from "./calendar-header";
|
||||
export * from "./calendar";
|
||||
export * from "./single-date";
|
||||
export * from "./single-issue";
|
||||
export * from "./inline-create-issue-form";
|
||||
|
@ -0,0 +1,91 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
// react hook form
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
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> | void;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
};
|
||||
|
||||
const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>) => {
|
||||
const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const { right } = ref.current.getBoundingClientRect();
|
||||
|
||||
const width = right + 250;
|
||||
|
||||
if (width > window.innerWidth) setIsThereSpaceOnRight(false);
|
||||
else setIsThereSpaceOnRight(true);
|
||||
}, [ref]);
|
||||
|
||||
return isThereSpaceOnRight;
|
||||
};
|
||||
|
||||
const InlineInput = () => {
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const { register, setFocus } = useFormContext();
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
|
||||
{projectDetails?.identifier ?? "..."}
|
||||
</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
|
||||
const { isOpen } = props;
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`absolute -translate-x-1 top-5 transition-all z-20 ${
|
||||
isOpen ? "opacity-100 scale-100" : "opacity-0 pointer-events-none scale-95"
|
||||
} ${isSpaceOnRight ? "left-full" : "right-0"}`}
|
||||
>
|
||||
<InlineCreateIssueFormWrapper
|
||||
{...props}
|
||||
className="flex w-60 p-1 px-1.5 rounded items-center gap-x-3 bg-custom-background-100 shadow-custom-shadow-md transition-opacity"
|
||||
>
|
||||
<InlineInput />
|
||||
</InlineCreateIssueFormWrapper>
|
||||
</div>
|
||||
{/* Added to make any other element as outside click. This will make input also to be outside. */}
|
||||
{isOpen && <div className="w-screen h-screen fixed inset-0 z-10" />}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,10 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
// component
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { SingleCalendarIssue } from "./single-issue";
|
||||
import { CalendarInlineCreateIssueForm } from "./inline-create-issue-form";
|
||||
// icons
|
||||
import { PlusSmallIcon } from "@heroicons/react/24/outline";
|
||||
// helper
|
||||
@ -26,17 +30,14 @@ type Props = {
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SingleCalendarDate: React.FC<Props> = ({
|
||||
handleIssueAction,
|
||||
date,
|
||||
index,
|
||||
addIssueToDate,
|
||||
isMonthlyView,
|
||||
showWeekEnds,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||
const { handleIssueAction, date, index, isMonthlyView, showWeekEnds, user, isNotAllowed } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||
|
||||
const totalIssues = date.issues.length;
|
||||
|
||||
@ -70,6 +71,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
user={user}
|
||||
@ -78,6 +80,17 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
|
||||
<CalendarInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
target_date: date.date,
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
}}
|
||||
/>
|
||||
|
||||
{totalIssues > 4 && (
|
||||
<button
|
||||
type="button"
|
||||
@ -93,7 +106,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
>
|
||||
<button
|
||||
className="flex items-center justify-center gap-1 text-center"
|
||||
onClick={() => addIssueToDate(date.date)}
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Add issue
|
||||
|
@ -41,6 +41,7 @@ type Props = {
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
@ -52,11 +53,12 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
provided,
|
||||
snapshot,
|
||||
issue,
|
||||
projectId,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -310,6 +312,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
{properties.state && (
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={projectId}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
@ -334,6 +337,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
@ -345,6 +349,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
|
@ -0,0 +1,62 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
// react hook form
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
// hooks
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
|
||||
// components
|
||||
import { InlineCreateIssueFormWrapper } from "components/core";
|
||||
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
};
|
||||
|
||||
const InlineInput = () => {
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const { register, setFocus } = useFormContext();
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-[14px] h-[14px] rounded-full border border-custom-border-1000 flex-shrink-0" />
|
||||
<h4 className="text-sm text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const GanttInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||
<>
|
||||
<InlineCreateIssueFormWrapper
|
||||
className="flex py-3 px-4 mr-2.5 items-center rounded gap-x-2 border bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
{...props}
|
||||
>
|
||||
<InlineInput />
|
||||
</InlineCreateIssueFormWrapper>
|
||||
{props.isOpen && (
|
||||
<p className="text-xs ml-3 mt-3 italic text-custom-text-200">
|
||||
Press {"'"}Enter{"'"} to add another issue
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
@ -5,3 +5,4 @@ export * from "./list-view";
|
||||
export * from "./spreadsheet-view";
|
||||
export * from "./all-views";
|
||||
export * from "./issues-view";
|
||||
export * from "./inline-issue-create-wrapper";
|
||||
|
270
web/components/core/views/inline-issue-create-wrapper.tsx
Normal file
270
web/components/core/views/inline-issue-create-wrapper.tsx
Normal file
@ -0,0 +1,270 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react hook form
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
|
||||
// headless ui
|
||||
import { Transition } from "@headlessui/react";
|
||||
|
||||
// services
|
||||
import modulesService from "services/modules.service";
|
||||
import issuesService from "services/issues.service";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUser from "hooks/use-user";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||
import useGanttChartIssues from "hooks/gantt-chart/issue-view";
|
||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
|
||||
// helpers
|
||||
import { getFetchKeysForIssueMutation } from "helpers/string.helper";
|
||||
|
||||
// fetch-keys
|
||||
import {
|
||||
USER_ISSUE,
|
||||
SUB_ISSUES,
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
CYCLE_DETAILS,
|
||||
MODULE_DETAILS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const addIssueToCycle = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
cycleId: string,
|
||||
user: any,
|
||||
params: any
|
||||
) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
await issuesService
|
||||
.addIssueToCycle(
|
||||
workspaceSlug as string,
|
||||
projectId.toString(),
|
||||
cycleId,
|
||||
{
|
||||
issues: [issueId],
|
||||
},
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
if (cycleId) {
|
||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params));
|
||||
mutate(CYCLE_DETAILS(cycleId as string));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const addIssueToModule = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
moduleId: string,
|
||||
user: any,
|
||||
params: any
|
||||
) => {
|
||||
await modulesService
|
||||
.addIssuesToModule(
|
||||
workspaceSlug as string,
|
||||
projectId.toString(),
|
||||
moduleId as string,
|
||||
{
|
||||
issues: [issueId],
|
||||
},
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
if (moduleId) {
|
||||
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
|
||||
mutate(MODULE_DETAILS(moduleId as string));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
|
||||
const { isOpen, handleClose, onSuccess, prePopulatedData, children, className } = props;
|
||||
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { displayFilters, params } = useIssuesView();
|
||||
const { params: calendarParams } = useCalendarIssuesView();
|
||||
const { ...viewGanttParams } = params;
|
||||
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||
const { params: ganttParams } = useGanttChartIssues(
|
||||
workspaceSlug?.toString(),
|
||||
projectId?.toString()
|
||||
);
|
||||
|
||||
const method = useForm<IIssue>({ defaultValues });
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
getValues,
|
||||
formState: { errors, isSubmitting },
|
||||
} = method;
|
||||
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
useKeypress("Escape", handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
const values = getValues();
|
||||
|
||||
if (prePopulatedData) reset({ ...defaultValues, ...values, ...prePopulatedData });
|
||||
}, [reset, prePopulatedData, getValues]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting)
|
||||
setToastAlert({
|
||||
type: "info",
|
||||
title: "Creating issue...",
|
||||
message: "Please wait while we create your issue.",
|
||||
});
|
||||
}, [isSubmitting, setToastAlert]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!errors) return;
|
||||
|
||||
Object.keys(errors).forEach((key) => {
|
||||
const error = errors[key as keyof IIssue];
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}, [errors, setToastAlert]);
|
||||
|
||||
const { calendarFetchKey, ganttFetchKey, spreadsheetFetchKey } = getFetchKeysForIssueMutation({
|
||||
cycleId: cycleId,
|
||||
moduleId: moduleId,
|
||||
viewId: viewId,
|
||||
projectId: projectId?.toString() ?? "",
|
||||
calendarParams,
|
||||
spreadsheetParams,
|
||||
viewGanttParams,
|
||||
ganttParams,
|
||||
});
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (!workspaceSlug || !projectId || !user || isSubmitting) return;
|
||||
|
||||
reset({ ...defaultValues });
|
||||
|
||||
await issuesService
|
||||
.createIssues(workspaceSlug.toString(), projectId.toString(), formData, user)
|
||||
.then(async (res) => {
|
||||
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params));
|
||||
if (formData.cycle && formData.cycle !== "")
|
||||
await addIssueToCycle(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
res.id,
|
||||
formData.cycle,
|
||||
user,
|
||||
params
|
||||
);
|
||||
if (formData.module && formData.module !== "")
|
||||
await addIssueToModule(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
res.id,
|
||||
formData.module,
|
||||
user,
|
||||
params
|
||||
);
|
||||
|
||||
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||
if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
|
||||
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||
if (groupedIssues) mutateMyIssues();
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
|
||||
if (onSuccess) await onSuccess(res);
|
||||
|
||||
if (formData.assignees_list?.some((assignee) => assignee === user?.id))
|
||||
mutate(USER_ISSUE(workspaceSlug as string));
|
||||
|
||||
if (formData.parent && formData.parent !== "") mutate(SUB_ISSUES(formData.parent));
|
||||
})
|
||||
.catch((err) => {
|
||||
Object.keys(err || {}).forEach((key) => {
|
||||
const error = err?.[key];
|
||||
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorTitle || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition ease-in-out duration-200 transform"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-in-out duration-200 transform"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<FormProvider {...method}>
|
||||
<form ref={ref} className={className} onSubmit={handleSubmit(onSubmitHandler)}>
|
||||
{children}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
export * from "./all-lists";
|
||||
export * from "./single-issue";
|
||||
export * from "./single-list";
|
||||
export * from "./inline-create-issue-form";
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { useEffect } from "react";
|
||||
// react hook form
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
// hooks
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
|
||||
// components
|
||||
import { InlineCreateIssueFormWrapper } from "../inline-issue-create-wrapper";
|
||||
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
};
|
||||
|
||||
const InlineInput = () => {
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const { register, setFocus } = useFormContext();
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
|
||||
{projectDetails?.identifier ?? "..."}
|
||||
</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||
<InlineCreateIssueFormWrapper
|
||||
className="flex py-3 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-md"
|
||||
{...props}
|
||||
>
|
||||
<InlineInput />
|
||||
</InlineCreateIssueFormWrapper>
|
||||
);
|
@ -51,6 +51,7 @@ import {
|
||||
type Props = {
|
||||
type?: string;
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
groupTitle?: string;
|
||||
editIssue: () => void;
|
||||
index: number;
|
||||
@ -69,6 +70,7 @@ type Props = {
|
||||
export const SingleListIssue: React.FC<Props> = ({
|
||||
type,
|
||||
issue,
|
||||
projectId,
|
||||
editIssue,
|
||||
index,
|
||||
makeIssueCopy,
|
||||
@ -88,7 +90,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId, userId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||
|
||||
@ -376,6 +378,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
{properties.state && (
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={projectId}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
@ -400,6 +403,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
{properties.labels && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
@ -411,6 +415,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
@ -10,7 +13,7 @@ import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
// components
|
||||
import { SingleListIssue } from "components/core";
|
||||
import { SingleListIssue, ListInlineCreateIssueForm } from "components/core";
|
||||
// ui
|
||||
import { Avatar, CustomMenu } from "components/ui";
|
||||
// icons
|
||||
@ -51,10 +54,10 @@ type Props = {
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleList: React.FC<Props> = ({
|
||||
export const SingleList: React.FC<Props> = (props) => {
|
||||
const {
|
||||
currentState,
|
||||
groupTitle,
|
||||
addIssueToGroup,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
handleDraftIssueAction,
|
||||
@ -65,10 +68,13 @@ export const SingleList: React.FC<Props> = ({
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||
@ -207,7 +213,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={addIssueToGroup}
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
@ -224,7 +230,9 @@ export const SingleList: React.FC<Props> = ({
|
||||
position="right"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
@ -250,6 +258,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
key={issue.id}
|
||||
type={type}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
groupTitle={groupTitle}
|
||||
index={index}
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
@ -284,6 +293,29 @@ export const SingleList: React.FC<Props> = ({
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||
)}
|
||||
|
||||
<ListInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
[displayFilters?.group_by!]: groupTitle,
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isCreateIssueFormOpen && (
|
||||
<div className="w-full bg-custom-background-100 px-6 py-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
|
@ -49,6 +49,7 @@ import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
index: number;
|
||||
expanded: boolean;
|
||||
handleToggleExpand: (issueId: string) => void;
|
||||
@ -64,6 +65,7 @@ type Props = {
|
||||
|
||||
export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
index,
|
||||
expanded,
|
||||
handleToggleExpand,
|
||||
@ -80,7 +82,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { params } = useSpreadsheetIssuesView();
|
||||
|
||||
@ -96,7 +98,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), params)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId, params);
|
||||
|
||||
if (issue.parent)
|
||||
mutate<ISubIssueResponse>(
|
||||
@ -136,13 +138,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
issuesService
|
||||
.patchIssue(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issue.id as string,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.patchIssue(workspaceSlug as string, projectId, issue.id as string, formData, user)
|
||||
.then(() => {
|
||||
if (issue.parent) {
|
||||
mutate(SUB_ISSUES(issue.parent as string));
|
||||
@ -368,6 +364,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={projectId}
|
||||
onChange={handleStateChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
@ -390,6 +387,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
@ -402,6 +400,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
|
@ -55,6 +55,7 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
<div>
|
||||
<SingleSpreadsheetIssue
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
index={index}
|
||||
expanded={isExpanded}
|
||||
handleToggleExpand={handleToggleExpand}
|
||||
|
@ -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<Props> = ({
|
||||
userAuth,
|
||||
}) => {
|
||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
@ -88,17 +89,26 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
))}
|
||||
|
||||
<ListInlineCreateIssueForm
|
||||
isOpen={isInlineCreateIssueFormOpen}
|
||||
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
||||
style={{ gridTemplateColumns }}
|
||||
>
|
||||
{!isInlineCreateIssueFormOpen && (
|
||||
<>
|
||||
{type === "issue" ? (
|
||||
<button
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
onClick={() => setIsInlineCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
@ -120,12 +130,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
optionsClassName="left-5 !w-36"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
@ -136,6 +141,8 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
</CustomMenu>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
@ -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<Props> = ({
|
||||
title,
|
||||
blockUpdateHandler,
|
||||
blocks,
|
||||
SidebarBlockRender,
|
||||
enableReorder,
|
||||
}) => {
|
||||
export const GanttSidebar: React.FC<Props> = (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<Props> = ({
|
||||
)}
|
||||
{droppableProvided.placeholder}
|
||||
</>
|
||||
|
||||
<GanttInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
handleClose={() => 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 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md mt-3"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
|
@ -24,6 +24,7 @@ import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
value: string[];
|
||||
projectId: string;
|
||||
onChange: (data: any) => void;
|
||||
labelsDetails: any[];
|
||||
className?: string;
|
||||
@ -37,6 +38,7 @@ type Props = {
|
||||
|
||||
export const LabelSelect: React.FC<Props> = ({
|
||||
value,
|
||||
projectId,
|
||||
onChange,
|
||||
labelsDetails,
|
||||
className = "",
|
||||
@ -54,15 +56,15 @@ export const LabelSelect: React.FC<Props> = ({
|
||||
const [labelModal, setLabelModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
||||
projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId) : null,
|
||||
workspaceSlug && projectId && fetchStates
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId)
|
||||
: null
|
||||
);
|
||||
|
||||
@ -150,7 +152,7 @@ export const LabelSelect: React.FC<Props> = ({
|
||||
<CreateLabelModal
|
||||
isOpen={labelModal}
|
||||
handleClose={() => setLabelModal(false)}
|
||||
projectId={projectId.toString()}
|
||||
projectId={projectId}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
|
@ -18,6 +18,7 @@ import { IUser } from "types";
|
||||
|
||||
type Props = {
|
||||
value: string | string[];
|
||||
projectId: string;
|
||||
onChange: (data: any) => void;
|
||||
membersDetails: IUser[];
|
||||
renderWorkspaceMembers?: boolean;
|
||||
@ -30,6 +31,7 @@ type Props = {
|
||||
|
||||
export const MembersSelect: React.FC<Props> = ({
|
||||
value,
|
||||
projectId,
|
||||
onChange,
|
||||
membersDetails,
|
||||
renderWorkspaceMembers = false,
|
||||
@ -44,14 +46,14 @@ export const MembersSelect: React.FC<Props> = ({
|
||||
const [fetchStates, setFetchStates] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
|
||||
const { members } = useProjectMembers(
|
||||
workspaceSlug?.toString(),
|
||||
projectId?.toString(),
|
||||
projectId,
|
||||
fetchStates && !renderWorkspaceMembers
|
||||
);
|
||||
|
||||
|
@ -149,7 +149,7 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
|
||||
</button>
|
||||
) : (
|
||||
<span className="cursor-default rounded bg-green-600 px-2 py-1 text-xs">
|
||||
Member
|
||||
Joined
|
||||
</span>
|
||||
)}
|
||||
{project.is_favorite && (
|
||||
|
@ -25,6 +25,7 @@ import { getStatesList } from "helpers/state.helper";
|
||||
type Props = {
|
||||
value: IState;
|
||||
onChange: (data: any, states: IState[] | undefined) => void;
|
||||
projectId: string;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
@ -35,6 +36,7 @@ type Props = {
|
||||
export const StateSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
projectId,
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
@ -50,12 +52,12 @@ export const StateSelect: React.FC<Props> = ({
|
||||
const [fetchStates, setFetchStates] = useState<boolean>(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId && fetchStates ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId && fetchStates ? STATES_LIST(projectId) : null,
|
||||
workspaceSlug && projectId && fetchStates
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId)
|
||||
: null
|
||||
);
|
||||
|
||||
|
@ -35,7 +35,7 @@ export const ToggleSwitch: React.FC<Props> = (props) => {
|
||||
: size === "md"
|
||||
? "translate-x-4"
|
||||
: "translate-x-5") + " bg-white"
|
||||
: "translate-x-1 bg-custom-background-90"
|
||||
: "translate-x-0.5 bg-custom-background-90"
|
||||
}`}
|
||||
/>
|
||||
</Switch>
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -36,6 +36,7 @@ const useGanttChartIssues = (workspaceSlug: string | undefined, projectId: strin
|
||||
return {
|
||||
ganttIssues,
|
||||
mutateGanttIssues,
|
||||
params,
|
||||
};
|
||||
};
|
||||
|
||||
|
19
web/hooks/use-keypress.tsx
Normal file
19
web/hooks/use-keypress.tsx
Normal file
@ -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;
|
@ -127,9 +127,21 @@ const WorkspacePage: NextPage = () => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-8 space-y-8">
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">
|
||||
Good {greeting}, {user?.first_name} {user?.last_name}
|
||||
</h3>
|
||||
<h6 className="text-custom-text-400 font-medium flex items-center gap-2">
|
||||
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
|
||||
<div>
|
||||
{DAYS[today.getDay()]}, {renderShortDate(today)} {render12HourFormatTime(today)}
|
||||
</div>
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
{projects ? (
|
||||
projects.length > 0 ? (
|
||||
<div className="p-8">
|
||||
<div className="flex flex-col gap-8">
|
||||
<IssuesStats data={workspaceDashboardData} />
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
@ -143,17 +155,8 @@ const WorkspacePage: NextPage = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8">
|
||||
<h3 className="text-2xl font-semibold">
|
||||
Good {greeting}, {user?.first_name} {user?.last_name}
|
||||
</h3>
|
||||
<h6 className="text-custom-text-400 font-medium">
|
||||
{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}
|
||||
{DAYS[today.getDay()]}, {renderShortDate(today)} {render12HourFormatTime(today)}
|
||||
</h6>
|
||||
<div className="mt-7 bg-custom-primary-100/5 flex justify-between gap-5 md:gap-8">
|
||||
<div className="bg-custom-primary-100/5 flex justify-between gap-5 md:gap-8">
|
||||
<div className="p-5 md:p-8 pr-0">
|
||||
<h5 className="text-xl font-semibold">Create a project</h5>
|
||||
<p className="mt-2 mb-5">
|
||||
@ -174,9 +177,9 @@ const WorkspacePage: NextPage = () => {
|
||||
<Image src={emptyDashboard} alt="Empty Dashboard" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
|
@ -46,7 +46,7 @@ const ProfileActivity = () => {
|
||||
{userActivity ? (
|
||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||
<h3 className="text-xl font-medium">Acitivity</h3>
|
||||
<h3 className="text-xl font-medium">Activity</h3>
|
||||
</div>
|
||||
<div className={`flex flex-col gap-2 py-4 w-full`}>
|
||||
<ul role="list" className="-mb-4">
|
||||
|
@ -66,7 +66,7 @@ const ProfilePreferences = observer(() => {
|
||||
|
||||
<div className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||
<h3 className="text-xl font-medium">Acitivity</h3>
|
||||
<h3 className="text-xl font-medium">Preferences</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
|
||||
<div className="col-span-12 sm:col-span-6">
|
||||
|
@ -50,7 +50,7 @@ const BillingSettings: NextPage = () => {
|
||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<div>
|
||||
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||
<h3 className="text-xl font-medium">Billing & Plan</h3>
|
||||
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-6">
|
||||
|
Loading…
Reference in New Issue
Block a user