mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' into packaging-tiptap
This commit is contained in:
commit
0f9aae6ff9
@ -80,7 +80,7 @@ class CycleViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
@ -102,48 +102,84 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.select_related("owned_by")
|
.select_related("owned_by")
|
||||||
.annotate(is_favorite=Exists(subquery))
|
.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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"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(
|
.annotate(
|
||||||
cancelled_issues=Count(
|
cancelled_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"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(
|
.annotate(
|
||||||
started_issues=Count(
|
started_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"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(
|
.annotate(
|
||||||
unstarted_issues=Count(
|
unstarted_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"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(
|
.annotate(
|
||||||
backlog_issues=Count(
|
backlog_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"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(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_estimates=Sum(
|
completed_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"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(
|
.annotate(
|
||||||
started_estimates=Sum(
|
started_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"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(
|
.prefetch_related(
|
||||||
@ -196,17 +232,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.values("display_name", "assignee_id", "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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"assignee_id",
|
"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")
|
.order_by("display_name")
|
||||||
@ -222,17 +271,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.annotate(color=F("labels__color"))
|
.annotate(color=F("labels__color"))
|
||||||
.annotate(label_id=F("labels__id"))
|
.annotate(label_id=F("labels__id"))
|
||||||
.values("label_name", "color", "label_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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"label_id",
|
"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")
|
.order_by("label_name")
|
||||||
@ -385,17 +447,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.values(
|
.values(
|
||||||
"first_name", "last_name", "assignee_id", "avatar", "display_name"
|
"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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"assignee_id",
|
"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")
|
.order_by("first_name", "last_name")
|
||||||
@ -412,17 +487,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.annotate(color=F("labels__color"))
|
.annotate(color=F("labels__color"))
|
||||||
.annotate(label_id=F("labels__id"))
|
.annotate(label_id=F("labels__id"))
|
||||||
.values("label_name", "color", "label_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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"label_id",
|
"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")
|
.order_by("label_name")
|
||||||
@ -488,7 +576,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
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
|
# Return all Cycle Issues
|
||||||
|
@ -384,7 +384,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
|||||||
sort_order=largest_sort_order,
|
sort_order=largest_sort_order,
|
||||||
start_date=issue_data.get("start_date", None),
|
start_date=issue_data.get("start_date", None),
|
||||||
target_date=issue_data.get("target_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,
|
created_by=request.user,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -173,12 +173,12 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check for valid priority
|
# 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",
|
"low",
|
||||||
"medium",
|
"medium",
|
||||||
"high",
|
"high",
|
||||||
"urgent",
|
"urgent",
|
||||||
None,
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -213,7 +213,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -278,7 +278,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -480,12 +480,12 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check for valid priority
|
# 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",
|
"low",
|
||||||
"medium",
|
"medium",
|
||||||
"high",
|
"high",
|
||||||
"urgent",
|
"urgent",
|
||||||
None,
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -520,7 +520,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -585,7 +585,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
||||||
|
@ -130,7 +130,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -151,7 +151,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -318,7 +318,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=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.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
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")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@ -596,7 +596,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(current_instance).data,
|
IssueCommentSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -618,7 +618,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(current_instance).data,
|
IssueCommentSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -902,7 +902,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@ -921,7 +921,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
IssueLinkSerializer(current_instance).data,
|
IssueLinkSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -943,7 +943,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
IssueLinkSerializer(current_instance).data,
|
IssueLinkSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -1022,7 +1022,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
|||||||
serializer.data,
|
serializer.data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
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)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1248,7 +1248,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
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)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=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):
|
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||||
@ -1477,7 +1477,7 @@ class IssueReactionViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_reaction.id),
|
"identifier": str(issue_reaction.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_reaction.delete()
|
issue_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1526,7 +1526,7 @@ class CommentReactionViewSet(BaseViewSet):
|
|||||||
issue_id=None,
|
issue_id=None,
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=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):
|
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||||
@ -1551,7 +1551,7 @@ class CommentReactionViewSet(BaseViewSet):
|
|||||||
"comment_id": str(comment_id),
|
"comment_id": str(comment_id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment_reaction.delete()
|
comment_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1648,7 +1648,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@ -1698,7 +1698,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(comment).data,
|
IssueCommentSerializer(comment).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1732,7 +1732,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(comment).data,
|
IssueCommentSerializer(comment).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment.delete()
|
comment.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1807,7 +1807,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=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.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1852,7 +1852,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_reaction.id),
|
"identifier": str(issue_reaction.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_reaction.delete()
|
issue_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1926,7 +1926,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
|||||||
issue_id=None,
|
issue_id=None,
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=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.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1978,7 +1978,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
|||||||
"comment_id": str(comment_id),
|
"comment_id": str(comment_id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment_reaction.delete()
|
comment_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -2042,7 +2042,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
serializer = IssueVoteSerializer(issue_vote)
|
serializer = IssueVoteSerializer(issue_vote)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@ -2077,7 +2077,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_vote.id),
|
"identifier": str(issue_vote.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_vote.delete()
|
issue_vote.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -2111,7 +2111,7 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
IssueRelationSerializer(current_instance).data,
|
IssueRelationSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -2145,7 +2145,7 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
if relation == "blocking":
|
if relation == "blocking":
|
||||||
@ -2417,7 +2417,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -2439,6 +2439,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -2602,7 +2603,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=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.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
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.issue_filters import issue_filters
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
|
|
||||||
class ModuleViewSet(BaseViewSet):
|
class ModuleViewSet(BaseViewSet):
|
||||||
model = Module
|
model = Module
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -78,35 +79,63 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"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(
|
.annotate(
|
||||||
cancelled_issues=Count(
|
cancelled_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"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(
|
.annotate(
|
||||||
started_issues=Count(
|
started_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"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(
|
.annotate(
|
||||||
unstarted_issues=Count(
|
unstarted_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"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(
|
.annotate(
|
||||||
backlog_issues=Count(
|
backlog_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"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")
|
.order_by(order_by, "name")
|
||||||
@ -130,7 +159,7 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
@ -179,18 +208,36 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(display_name=F("assignees__display_name"))
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
.values(
|
||||||
.annotate(total_issues=Count("assignee_id"))
|
"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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"assignee_id",
|
"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")
|
.order_by("first_name", "last_name")
|
||||||
@ -206,17 +253,33 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
.annotate(color=F("labels__color"))
|
.annotate(color=F("labels__color"))
|
||||||
.annotate(label_id=F("labels__id"))
|
.annotate(label_id=F("labels__id"))
|
||||||
.values("label_name", "color", "label_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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"label_id",
|
"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")
|
.order_by("label_name")
|
||||||
@ -279,7 +342,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -447,7 +510,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
@ -494,7 +557,6 @@ class ModuleLinkViewSet(BaseViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleFavoriteViewSet(BaseViewSet):
|
class ModuleFavoriteViewSet(BaseViewSet):
|
||||||
|
|
||||||
serializer_class = ModuleFavoriteSerializer
|
serializer_class = ModuleFavoriteSerializer
|
||||||
model = ModuleFavorite
|
model = ModuleFavorite
|
||||||
|
|
||||||
|
@ -1239,13 +1239,21 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
|||||||
.annotate(
|
.annotate(
|
||||||
created_issues=Count(
|
created_issues=Count(
|
||||||
"project_issue",
|
"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(
|
.annotate(
|
||||||
assigned_issues=Count(
|
assigned_issues=Count(
|
||||||
"project_issue",
|
"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(
|
.annotate(
|
||||||
@ -1254,6 +1262,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
|||||||
filter=Q(
|
filter=Q(
|
||||||
project_issue__completed_at__isnull=False,
|
project_issue__completed_at__isnull=False,
|
||||||
project_issue__assignees__in=[user_id],
|
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",
|
"started",
|
||||||
],
|
],
|
||||||
project_issue__assignees__in=[user_id],
|
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):
|
def get(self, request, slug, user_id):
|
||||||
try:
|
try:
|
||||||
filters = issue_filters(request.query_params, "GET")
|
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")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
|
@ -32,7 +32,7 @@ def delete_old_s3_link():
|
|||||||
else:
|
else:
|
||||||
s3 = boto3.client(
|
s3 = boto3.client(
|
||||||
"s3",
|
"s3",
|
||||||
region_name="ap-south-1",
|
region_name=settings.AWS_REGION,
|
||||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
config=Config(signature_version="s3v4"),
|
config=Config(signature_version="s3v4"),
|
||||||
|
@ -121,36 +121,20 @@ def track_priority(
|
|||||||
epoch
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("priority") != requested_data.get("priority"):
|
if current_instance.get("priority") != requested_data.get("priority"):
|
||||||
if requested_data.get("priority") == None:
|
issue_activities.append(
|
||||||
issue_activities.append(
|
IssueActivity(
|
||||||
IssueActivity(
|
issue_id=issue_id,
|
||||||
issue_id=issue_id,
|
actor=actor,
|
||||||
actor=actor,
|
verb="updated",
|
||||||
verb="updated",
|
old_value=current_instance.get("priority"),
|
||||||
old_value=current_instance.get("priority"),
|
new_value=requested_data.get("priority"),
|
||||||
new_value=None,
|
field="priority",
|
||||||
field="priority",
|
project=project,
|
||||||
project=project,
|
workspace=project.workspace,
|
||||||
workspace=project.workspace,
|
comment=f"updated the priority to {requested_data.get('priority')}",
|
||||||
comment=f"updated the priority to None",
|
epoch=epoch,
|
||||||
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,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Track chnages in state of the issue
|
# Track chnages in state of the issue
|
||||||
|
@ -77,7 +77,7 @@ def archive_old_issues():
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
subscriber=False,
|
subscriber=False,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
for issue in updated_issues
|
for issue in updated_issues
|
||||||
]
|
]
|
||||||
@ -149,7 +149,7 @@ def close_old_issues():
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
subscriber=False,
|
subscriber=False,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
for issue in updated_issues
|
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),
|
||||||
|
]
|
@ -26,7 +26,7 @@ ROLE_CHOICES = (
|
|||||||
def get_default_props():
|
def get_default_props():
|
||||||
return {
|
return {
|
||||||
"filters": {
|
"filters": {
|
||||||
"priority": None,
|
"priority": "none",
|
||||||
"state": None,
|
"state": None,
|
||||||
"state_group": None,
|
"state_group": None,
|
||||||
"assignees": None,
|
"assignees": None,
|
||||||
|
@ -17,7 +17,7 @@ ROLE_CHOICES = (
|
|||||||
def get_default_props():
|
def get_default_props():
|
||||||
return {
|
return {
|
||||||
"filters": {
|
"filters": {
|
||||||
"priority": None,
|
"priority": "none",
|
||||||
"state": None,
|
"state": None,
|
||||||
"state_group": None,
|
"state_group": None,
|
||||||
"assignees": None,
|
"assignees": None,
|
||||||
|
@ -74,10 +74,10 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
|||||||
|
|
||||||
sorted_data = grouped_data
|
sorted_data = grouped_data
|
||||||
if temp_axis == "priority":
|
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}
|
sorted_data = {key: grouped_data[key] for key in order if key in grouped_data}
|
||||||
else:
|
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
|
return sorted_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,9 +40,6 @@ def filter_priority(params, filter, method):
|
|||||||
priorities = params.get("priority").split(",")
|
priorities = params.get("priority").split(",")
|
||||||
if len(priorities) and "" not in priorities:
|
if len(priorities) and "" not in priorities:
|
||||||
filter["priority__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
|
return filter
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,113 +1,61 @@
|
|||||||
version: "3.8"
|
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:
|
services:
|
||||||
plane-web:
|
web:
|
||||||
container_name: planefrontend
|
container_name: web
|
||||||
image: makeplane/plane-frontend:latest
|
image: makeplane/plane-frontend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh web/server.js web
|
command: /usr/local/bin/start.sh web/server.js web
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./web/.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"
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
|
|
||||||
plane-deploy:
|
space:
|
||||||
container_name: planedeploy
|
container_name: space
|
||||||
image: makeplane/plane-deploy:latest
|
image: makeplane/plane-space:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh space/server.js space
|
command: /usr/local/bin/start.sh space/server.js space
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./space/.env
|
||||||
environment:
|
|
||||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
- plane-web
|
- web
|
||||||
|
|
||||||
plane-api:
|
api:
|
||||||
container_name: planebackend
|
container_name: api
|
||||||
image: makeplane/plane-backend:latest
|
image: makeplane/plane-backend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: ./bin/takeoff
|
command: ./bin/takeoff
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-worker:
|
worker:
|
||||||
container_name: planebgworker
|
container_name: bgworker
|
||||||
image: makeplane/plane-backend:latest
|
image: makeplane/plane-backend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: ./bin/worker
|
command: ./bin/worker
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-beat-worker:
|
beat-worker:
|
||||||
container_name: planebeatworker
|
container_name: beatworker
|
||||||
image: makeplane/plane-backend:latest
|
image: makeplane/plane-backend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: ./bin/beat
|
command: ./bin/beat
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
@ -157,8 +105,8 @@ services:
|
|||||||
- plane-minio
|
- plane-minio
|
||||||
|
|
||||||
# Comment this if you already have a reverse proxy running
|
# Comment this if you already have a reverse proxy running
|
||||||
plane-proxy:
|
proxy:
|
||||||
container_name: planeproxy
|
container_name: proxy
|
||||||
image: makeplane/plane-proxy:latest
|
image: makeplane/plane-proxy:latest
|
||||||
ports:
|
ports:
|
||||||
- ${NGINX_PORT}:80
|
- ${NGINX_PORT}:80
|
||||||
@ -168,8 +116,9 @@ services:
|
|||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-web
|
- web
|
||||||
- plane-api
|
- api
|
||||||
|
- space
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
plane-web:
|
web:
|
||||||
container_name: planefrontend
|
container_name: web
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./web/Dockerfile.web
|
dockerfile: ./web/Dockerfile.web
|
||||||
@ -11,11 +11,11 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh web/server.js web
|
command: /usr/local/bin/start.sh web/server.js web
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
|
|
||||||
plane-deploy:
|
space:
|
||||||
container_name: planedeploy
|
container_name: space
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./space/Dockerfile.space
|
dockerfile: ./space/Dockerfile.space
|
||||||
@ -24,12 +24,12 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh space/server.js space
|
command: /usr/local/bin/start.sh space/server.js space
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
- plane-web
|
- web
|
||||||
|
|
||||||
plane-api:
|
api:
|
||||||
container_name: planebackend
|
container_name: api
|
||||||
build:
|
build:
|
||||||
context: ./apiserver
|
context: ./apiserver
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
@ -43,8 +43,8 @@ services:
|
|||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-worker:
|
worker:
|
||||||
container_name: planebgworker
|
container_name: bgworker
|
||||||
build:
|
build:
|
||||||
context: ./apiserver
|
context: ./apiserver
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
@ -55,12 +55,12 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- ./apiserver/.env
|
- ./apiserver/.env
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-beat-worker:
|
beat-worker:
|
||||||
container_name: planebeatworker
|
container_name: beatworker
|
||||||
build:
|
build:
|
||||||
context: ./apiserver
|
context: ./apiserver
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
@ -71,7 +71,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- ./apiserver/.env
|
- ./apiserver/.env
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
@ -118,8 +118,8 @@ services:
|
|||||||
- plane-minio
|
- plane-minio
|
||||||
|
|
||||||
# Comment this if you already have a reverse proxy running
|
# Comment this if you already have a reverse proxy running
|
||||||
plane-proxy:
|
proxy:
|
||||||
container_name: planeproxy
|
container_name: proxy
|
||||||
build:
|
build:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@ -130,8 +130,9 @@ services:
|
|||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-web
|
- web
|
||||||
- plane-api
|
- api
|
||||||
|
- space
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
events { }
|
events {
|
||||||
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
sendfile on;
|
sendfile on;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
root /www/data/;
|
root /www/data/;
|
||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log;
|
||||||
|
|
||||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://planefrontend:3000/;
|
proxy_pass http://web:3000/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://planebackend:8000/api/;
|
proxy_pass http://api:8000/api/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /spaces/ {
|
location /spaces/ {
|
||||||
proxy_pass http://planedeploy:3000/spaces/;
|
proxy_pass http://space:3000/spaces/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /${BUCKET_NAME}/ {
|
location /${BUCKET_NAME}/ {
|
||||||
|
@ -1,4 +1,2 @@
|
|||||||
# Google Client ID for Google OAuth
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
|
||||||
# Flag to toggle OAuth
|
# Flag to toggle OAuth
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
@ -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
|
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
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
|
# Public boards deploy URL
|
||||||
NEXT_PUBLIC_DEPLOY_URL="http://localhost:3000/spaces"
|
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
@ -2,3 +2,4 @@ export * from "./all-boards";
|
|||||||
export * from "./board-header";
|
export * from "./board-header";
|
||||||
export * from "./single-board";
|
export * from "./single-board";
|
||||||
export * from "./single-issue";
|
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 StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
// components
|
// components
|
||||||
import { BoardHeader, SingleBoardIssue } from "components/core";
|
import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -34,26 +34,30 @@ type Props = {
|
|||||||
viewProps: IIssueViewProps;
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleBoard: React.FC<Props> = ({
|
export const SingleBoard: React.FC<Props> = (props) => {
|
||||||
addIssueToGroup,
|
const {
|
||||||
currentState,
|
addIssueToGroup,
|
||||||
groupTitle,
|
currentState,
|
||||||
disableUserActions,
|
groupTitle,
|
||||||
disableAddIssueOption = false,
|
disableUserActions,
|
||||||
dragDisabled,
|
disableAddIssueOption = false,
|
||||||
handleIssueAction,
|
dragDisabled,
|
||||||
handleDraftIssueAction,
|
handleIssueAction,
|
||||||
handleTrashBox,
|
handleDraftIssueAction,
|
||||||
openIssuesListModal,
|
handleTrashBox,
|
||||||
handleMyIssueOpen,
|
openIssuesListModal,
|
||||||
removeIssue,
|
handleMyIssueOpen,
|
||||||
user,
|
removeIssue,
|
||||||
userAuth,
|
user,
|
||||||
viewProps,
|
userAuth,
|
||||||
}) => {
|
viewProps,
|
||||||
|
} = props;
|
||||||
|
|
||||||
// collapse/expand
|
// collapse/expand
|
||||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
|
||||||
|
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
const { displayFilters, groupedIssues } = viewProps;
|
const { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -67,6 +71,24 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
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 (
|
return (
|
||||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||||
<BoardHeader
|
<BoardHeader
|
||||||
@ -115,6 +137,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
id={`board-list-${groupTitle}`}
|
||||||
className={`pt-3 ${
|
className={`pt-3 ${
|
||||||
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
||||||
} `}
|
} `}
|
||||||
@ -169,6 +192,19 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<>{provided.placeholder}</>
|
<>{provided.placeholder}</>
|
||||||
</span>
|
</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>
|
</div>
|
||||||
{displayFilters?.group_by !== "created_by" && (
|
{displayFilters?.group_by !== "created_by" && (
|
||||||
<div>
|
<div>
|
||||||
@ -177,7 +213,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
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" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
Add Issue
|
||||||
@ -197,7 +233,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
position="left"
|
position="left"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>
|
<CustomMenu.MenuItem onClick={() => onCreateClick()}>
|
||||||
Create new
|
Create new
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
||||||
|
@ -2,3 +2,4 @@ export * from "./calendar-header";
|
|||||||
export * from "./calendar";
|
export * from "./calendar";
|
||||||
export * from "./single-date";
|
export * from "./single-date";
|
||||||
export * from "./single-issue";
|
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";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// react-beautiful-dnd
|
// react-beautiful-dnd
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
// component
|
// component
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { SingleCalendarIssue } from "./single-issue";
|
import { SingleCalendarIssue } from "./single-issue";
|
||||||
|
import { CalendarInlineCreateIssueForm } from "./inline-create-issue-form";
|
||||||
// icons
|
// icons
|
||||||
import { PlusSmallIcon } from "@heroicons/react/24/outline";
|
import { PlusSmallIcon } from "@heroicons/react/24/outline";
|
||||||
// helper
|
// helper
|
||||||
@ -26,17 +30,14 @@ type Props = {
|
|||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleCalendarDate: React.FC<Props> = ({
|
export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||||
handleIssueAction,
|
const { handleIssueAction, date, index, isMonthlyView, showWeekEnds, user, isNotAllowed } = props;
|
||||||
date,
|
|
||||||
index,
|
const router = useRouter();
|
||||||
addIssueToDate,
|
const { cycleId, moduleId } = router.query;
|
||||||
isMonthlyView,
|
|
||||||
showWeekEnds,
|
|
||||||
user,
|
|
||||||
isNotAllowed,
|
|
||||||
}) => {
|
|
||||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||||
|
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
const totalIssues = date.issues.length;
|
const totalIssues = date.issues.length;
|
||||||
|
|
||||||
@ -78,6 +79,17 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<CalendarInlineCreateIssueForm
|
||||||
|
isOpen={isCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||||
|
prePopulatedData={{
|
||||||
|
target_date: date.date,
|
||||||
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{totalIssues > 4 && (
|
{totalIssues > 4 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -93,7 +105,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="flex items-center justify-center gap-1 text-center"
|
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" />
|
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Add issue
|
Add issue
|
||||||
|
@ -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 "./spreadsheet-view";
|
||||||
export * from "./all-views";
|
export * from "./all-views";
|
||||||
export * from "./issues-view";
|
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 "./all-lists";
|
||||||
export * from "./single-issue";
|
export * from "./single-issue";
|
||||||
export * from "./single-list";
|
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>
|
||||||
|
);
|
@ -1,3 +1,6 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -10,7 +13,7 @@ import projectService from "services/project.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useProjects from "hooks/use-projects";
|
import useProjects from "hooks/use-projects";
|
||||||
// components
|
// components
|
||||||
import { SingleListIssue } from "components/core";
|
import { SingleListIssue, ListInlineCreateIssueForm } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, CustomMenu } from "components/ui";
|
import { Avatar, CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -51,24 +54,27 @@ type Props = {
|
|||||||
viewProps: IIssueViewProps;
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleList: React.FC<Props> = ({
|
export const SingleList: React.FC<Props> = (props) => {
|
||||||
currentState,
|
const {
|
||||||
groupTitle,
|
currentState,
|
||||||
addIssueToGroup,
|
groupTitle,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
handleDraftIssueAction,
|
handleDraftIssueAction,
|
||||||
handleMyIssueOpen,
|
handleMyIssueOpen,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
disableAddIssueOption = false,
|
disableAddIssueOption = false,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
viewProps,
|
viewProps,
|
||||||
}) => {
|
} = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||||
@ -207,7 +213,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
onClick={addIssueToGroup}
|
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -224,7 +230,9 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
position="right"
|
position="right"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
|
<CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>
|
||||||
|
Create new
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
||||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||||
Add an existing issue
|
Add an existing issue
|
||||||
@ -284,6 +292,29 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
<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>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@ import React, { useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
import { SpreadsheetColumns, SpreadsheetIssues, ListInlineCreateIssueForm } from "components/core";
|
||||||
import { CustomMenu, Spinner } from "components/ui";
|
import { CustomMenu, Spinner } from "components/ui";
|
||||||
import { IssuePeekOverview } from "components/issues";
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// hooks
|
// hooks
|
||||||
@ -33,6 +33,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||||
|
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
@ -88,53 +89,59 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<ListInlineCreateIssueForm
|
||||||
|
isOpen={isInlineCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||||
|
prePopulatedData={{
|
||||||
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<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"
|
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 }}
|
style={{ gridTemplateColumns }}
|
||||||
>
|
>
|
||||||
{type === "issue" ? (
|
{!isInlineCreateIssueFormOpen && (
|
||||||
<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"
|
{type === "issue" ? (
|
||||||
onClick={() => {
|
<button
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
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"
|
||||||
document.dispatchEvent(e);
|
onClick={() => setIsInlineCreateIssueFormOpen(true)}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
!disableUserActions && (
|
|
||||||
<CustomMenu
|
|
||||||
className="sticky left-0 z-[1]"
|
|
||||||
customButton={
|
|
||||||
<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"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
position="left"
|
|
||||||
optionsClassName="left-5 !w-36"
|
|
||||||
noBorder
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Create new
|
<PlusIcon className="h-4 w-4" />
|
||||||
</CustomMenu.MenuItem>
|
Add Issue
|
||||||
{openIssuesListModal && (
|
</button>
|
||||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
) : (
|
||||||
Add an existing issue
|
!disableUserActions && (
|
||||||
</CustomMenu.MenuItem>
|
<CustomMenu
|
||||||
)}
|
className="sticky left-0 z-[1]"
|
||||||
</CustomMenu>
|
customButton={
|
||||||
)
|
<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"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
Add Issue
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
position="left"
|
||||||
|
optionsClassName="left-5 !w-36"
|
||||||
|
noBorder
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
|
||||||
|
Create new
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
{openIssuesListModal && (
|
||||||
|
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||||
|
Add an existing issue
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
// react-beautiful-dnd
|
// react-beautiful-dnd
|
||||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
@ -7,6 +10,9 @@ import { useChart } from "./hooks";
|
|||||||
import { Loader } from "components/ui";
|
import { Loader } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
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
|
// types
|
||||||
import { IBlockUpdateData, IGanttBlock } from "./types";
|
import { IBlockUpdateData, IGanttBlock } from "./types";
|
||||||
|
|
||||||
@ -18,15 +24,16 @@ type Props = {
|
|||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GanttSidebar: React.FC<Props> = ({
|
export const GanttSidebar: React.FC<Props> = (props) => {
|
||||||
title,
|
const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props;
|
||||||
blockUpdateHandler,
|
|
||||||
blocks,
|
const router = useRouter();
|
||||||
SidebarBlockRender,
|
const { cycleId, moduleId } = router.query;
|
||||||
enableReorder,
|
|
||||||
}) => {
|
|
||||||
const { activeBlock, dispatch } = useChart();
|
const { activeBlock, dispatch } = useChart();
|
||||||
|
|
||||||
|
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
// update the active block on hover
|
// update the active block on hover
|
||||||
const updateActiveBlock = (block: IGanttBlock | null) => {
|
const updateActiveBlock = (block: IGanttBlock | null) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -148,6 +155,28 @@ export const GanttSidebar: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{droppableProvided.placeholder}
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</StrictModeDroppable>
|
</StrictModeDroppable>
|
||||||
|
@ -43,7 +43,7 @@ export const IssueMainContent: React.FC<Props> = ({
|
|||||||
uneditable = false,
|
uneditable = false,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId, archivedIssueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ export const IssueMainContent: React.FC<Props> = ({
|
|||||||
<IssueReaction workspaceSlug={workspaceSlug} issueId={issueId} projectId={projectId} />
|
<IssueReaction workspaceSlug={workspaceSlug} issueId={issueId} projectId={projectId} />
|
||||||
|
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<SubIssuesRoot parentIssue={issueDetails} user={user} editable={uneditable} />
|
<SubIssuesRoot parentIssue={issueDetails} user={user} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 py-3">
|
<div className="flex flex-col gap-3 py-3">
|
||||||
|
@ -19,6 +19,7 @@ import { Tooltip, CustomMenu } from "components/ui";
|
|||||||
|
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue } from "types";
|
import { ICurrentUserResponse, IIssue } from "types";
|
||||||
|
import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
||||||
|
|
||||||
export interface ISubIssues {
|
export interface ISubIssues {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -29,8 +30,8 @@ export interface ISubIssues {
|
|||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
removeIssueFromSubIssues: (parentIssueId: string, issue: IIssue) => void;
|
removeIssueFromSubIssues: (parentIssueId: string, issue: IIssue) => void;
|
||||||
issuesVisibility: string[];
|
issuesLoader: ISubIssuesRootLoaders;
|
||||||
handleIssuesVisibility: (issueId: string) => void;
|
handleIssuesLoader: ({ key, issueId }: ISubIssuesRootLoadersHandler) => void;
|
||||||
copyText: (text: string) => void;
|
copyText: (text: string) => void;
|
||||||
handleIssueCrudOperation: (
|
handleIssueCrudOperation: (
|
||||||
key: "create" | "existing" | "edit" | "delete",
|
key: "create" | "existing" | "edit" | "delete",
|
||||||
@ -48,8 +49,8 @@ export const SubIssues: React.FC<ISubIssues> = ({
|
|||||||
user,
|
user,
|
||||||
editable,
|
editable,
|
||||||
removeIssueFromSubIssues,
|
removeIssueFromSubIssues,
|
||||||
issuesVisibility,
|
issuesLoader,
|
||||||
handleIssuesVisibility,
|
handleIssuesLoader,
|
||||||
copyText,
|
copyText,
|
||||||
handleIssueCrudOperation,
|
handleIssueCrudOperation,
|
||||||
}) => (
|
}) => (
|
||||||
@ -62,19 +63,21 @@ export const SubIssues: React.FC<ISubIssues> = ({
|
|||||||
<div className="flex-shrink-0 w-[22px] h-[22px]">
|
<div className="flex-shrink-0 w-[22px] h-[22px]">
|
||||||
{issue?.sub_issues_count > 0 && (
|
{issue?.sub_issues_count > 0 && (
|
||||||
<>
|
<>
|
||||||
{true ? (
|
{issuesLoader.sub_issues.includes(issue?.id) ? (
|
||||||
|
<div className="w-full h-full flex justify-center items-center rounded-sm bg-custom-background-80 transition-all cursor-not-allowed">
|
||||||
|
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className="w-full h-full flex justify-center items-center rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer"
|
className="w-full h-full flex justify-center items-center rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer"
|
||||||
onClick={() => handleIssuesVisibility(issue?.id)}
|
onClick={() => handleIssuesLoader({ key: "visibility", issueId: issue?.id })}
|
||||||
>
|
>
|
||||||
{issuesVisibility && issuesVisibility.includes(issue?.id) ? (
|
{issuesLoader && issuesLoader.visibility.includes(issue?.id) ? (
|
||||||
<ChevronDown width={14} strokeWidth={2} />
|
<ChevronDown width={14} strokeWidth={2} />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight width={14} strokeWidth={2} />
|
<ChevronRight width={14} strokeWidth={2} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -92,7 +95,7 @@ export const SubIssues: React.FC<ISubIssues> = ({
|
|||||||
{issue.project_detail.identifier}-{issue?.sequence_id}
|
{issue.project_detail.identifier}-{issue?.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={`${issue?.name}`}>
|
<Tooltip tooltipHeading="Title" tooltipContent={`${issue?.name}`}>
|
||||||
<div className="line-clamp-1 text-xs text-custom-text-100">{issue?.name}</div>
|
<div className="line-clamp-1 text-xs text-custom-text-100 pr-2">{issue?.name}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -132,7 +135,11 @@ export const SubIssues: React.FC<ISubIssues> = ({
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CustomMenu.MenuItem onClick={copyText}>
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() =>
|
||||||
|
copyText(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<LinkIcon width={14} strokeWidth={2} />
|
<LinkIcon width={14} strokeWidth={2} />
|
||||||
<span>Copy issue link</span>
|
<span>Copy issue link</span>
|
||||||
@ -142,17 +149,28 @@ export const SubIssues: React.FC<ISubIssues> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{editable && (
|
{editable && (
|
||||||
<div
|
<>
|
||||||
className="flex-shrink-0 invisible group-hover:visible w-[22px] h-[22px] flex justify-center items-center rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer overflow-hidden"
|
{issuesLoader.delete.includes(issue?.id) ? (
|
||||||
onClick={() => removeIssueFromSubIssues(parentIssue?.id, issue)}
|
<div className="flex-shrink-0 w-[22px] h-[22px] rounded-sm bg-red-200/10 text-red-500 transition-all cursor-not-allowed overflow-hidden flex justify-center items-center">
|
||||||
>
|
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
||||||
<X width={14} strokeWidth={2} />
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 invisible group-hover:visible w-[22px] h-[22px] rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer overflow-hidden flex justify-center items-center"
|
||||||
|
onClick={() => {
|
||||||
|
handleIssuesLoader({ key: "delete", issueId: issue?.id });
|
||||||
|
removeIssueFromSubIssues(parentIssue?.id, issue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X width={14} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{issuesVisibility.includes(issue?.id) && issue?.sub_issues_count > 0 && (
|
{issuesLoader.visibility.includes(issue?.id) && issue?.sub_issues_count > 0 && (
|
||||||
<SubIssuesRootList
|
<SubIssuesRootList
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -161,8 +179,8 @@ export const SubIssues: React.FC<ISubIssues> = ({
|
|||||||
user={user}
|
user={user}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
||||||
issuesVisibility={issuesVisibility}
|
issuesLoader={issuesLoader}
|
||||||
handleIssuesVisibility={handleIssuesVisibility}
|
handleIssuesLoader={handleIssuesLoader}
|
||||||
copyText={copyText}
|
copyText={copyText}
|
||||||
handleIssueCrudOperation={handleIssueCrudOperation}
|
handleIssueCrudOperation={handleIssueCrudOperation}
|
||||||
/>
|
/>
|
||||||
|
@ -5,6 +5,8 @@ import useSWR from "swr";
|
|||||||
import { SubIssues } from "./issue";
|
import { SubIssues } from "./issue";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue } from "types";
|
import { ICurrentUserResponse, IIssue } from "types";
|
||||||
|
|
||||||
|
import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
@ -18,8 +20,8 @@ export interface ISubIssuesRootList {
|
|||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
removeIssueFromSubIssues: (parentIssueId: string, issue: IIssue) => void;
|
removeIssueFromSubIssues: (parentIssueId: string, issue: IIssue) => void;
|
||||||
issuesVisibility: string[];
|
issuesLoader: ISubIssuesRootLoaders;
|
||||||
handleIssuesVisibility: (issueId: string) => void;
|
handleIssuesLoader: ({ key, issueId }: ISubIssuesRootLoadersHandler) => void;
|
||||||
copyText: (text: string) => void;
|
copyText: (text: string) => void;
|
||||||
handleIssueCrudOperation: (
|
handleIssueCrudOperation: (
|
||||||
key: "create" | "existing" | "edit" | "delete",
|
key: "create" | "existing" | "edit" | "delete",
|
||||||
@ -36,8 +38,8 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
|||||||
user,
|
user,
|
||||||
editable,
|
editable,
|
||||||
removeIssueFromSubIssues,
|
removeIssueFromSubIssues,
|
||||||
issuesVisibility,
|
issuesLoader,
|
||||||
handleIssuesVisibility,
|
handleIssuesLoader,
|
||||||
copyText,
|
copyText,
|
||||||
handleIssueCrudOperation,
|
handleIssueCrudOperation,
|
||||||
}) => {
|
}) => {
|
||||||
@ -50,6 +52,16 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
|||||||
: null
|
: 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 (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{issues &&
|
{issues &&
|
||||||
@ -66,8 +78,8 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
|||||||
user={user}
|
user={user}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
||||||
issuesVisibility={issuesVisibility}
|
issuesLoader={issuesLoader}
|
||||||
handleIssuesVisibility={handleIssuesVisibility}
|
handleIssuesLoader={handleIssuesLoader}
|
||||||
copyText={copyText}
|
copyText={copyText}
|
||||||
handleIssueCrudOperation={handleIssueCrudOperation}
|
handleIssueCrudOperation={handleIssueCrudOperation}
|
||||||
/>
|
/>
|
||||||
|
@ -14,7 +14,7 @@ export const ProgressBar = ({ total = 0, done = 0 }: IProgressBar) => {
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="w-full rounded-full bg-custom-background-80 overflow-hidden shadow">
|
<div className="w-full rounded-full bg-custom-background-80 overflow-hidden shadow">
|
||||||
<div
|
<div
|
||||||
className="bg-green-500 h-[6px] rounded-full"
|
className="bg-green-500 h-[6px] rounded-full transition-all"
|
||||||
style={{ width: `${calPercentage(done, total)}%` }}
|
style={{ width: `${calPercentage(done, total)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,12 +86,14 @@ export const IssueProperty: React.FC<IIssueProperty> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAssigneeChange = (data: any) => {
|
const handleAssigneeChange = (data: any) => {
|
||||||
const newData = issue.assignees ?? [];
|
let newData = issue.assignees ?? [];
|
||||||
|
|
||||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
if (newData && newData.length > 0) {
|
||||||
else newData.push(data);
|
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(
|
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||||
{
|
{
|
||||||
|
@ -14,6 +14,9 @@ import { ProgressBar } from "./progressbar";
|
|||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||||
|
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
@ -25,18 +28,28 @@ import { SUB_ISSUES } from "constants/fetch-keys";
|
|||||||
|
|
||||||
export interface ISubIssuesRoot {
|
export interface ISubIssuesRoot {
|
||||||
parentIssue: IIssue;
|
parentIssue: IIssue;
|
||||||
|
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
editable: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ 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<ISubIssuesRoot> = ({ parentIssue, user }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||||
|
|
||||||
const { memberRole } = useProjectMyMembership();
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
|
||||||
const { data: issues } = useSWR(
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const { data: issues, isLoading } = useSWR(
|
||||||
workspaceSlug && projectId && parentIssue && parentIssue?.id
|
workspaceSlug && projectId && parentIssue && parentIssue?.id
|
||||||
? SUB_ISSUES(parentIssue?.id)
|
? SUB_ISSUES(parentIssue?.id)
|
||||||
: null,
|
: null,
|
||||||
@ -45,13 +58,18 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user, edi
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const [issuesVisibility, setIssuesVisibility] = React.useState<string[]>([parentIssue?.id]);
|
const [issuesLoader, setIssuesLoader] = React.useState<ISubIssuesRootLoaders>({
|
||||||
const handleIssuesVisibility = (issueId: string) => {
|
visibility: [parentIssue?.id],
|
||||||
if (issuesVisibility.includes(issueId)) {
|
delete: [],
|
||||||
setIssuesVisibility(issuesVisibility.filter((i: string) => i !== issueId));
|
sub_issues: [],
|
||||||
} else {
|
});
|
||||||
setIssuesVisibility([...issuesVisibility, issueId]);
|
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<{
|
const [issueCrudOperation, setIssueCrudOperation] = React.useState<{
|
||||||
@ -110,8 +128,22 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user, edi
|
|||||||
if (!workspaceSlug || !parentIssue || !issue?.id) return;
|
if (!workspaceSlug || !parentIssue || !issue?.id) return;
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug, projectId, issue.id, { parent: null }, user)
|
.patchIssue(workspaceSlug, projectId, issue.id, { parent: null }, user)
|
||||||
.finally(() => {
|
.then(async () => {
|
||||||
if (parentIssueId) mutate(SUB_ISSUES(parentIssueId));
|
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<ISubIssuesRoot> = ({ parentIssue, user, edi
|
|||||||
const originURL =
|
const originURL =
|
||||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
copyTextToClipboard(`${originURL}/${text}`).then(() => {
|
copyTextToClipboard(`${originURL}/${text}`).then(() => {
|
||||||
// setToastAlert({
|
setToastAlert({
|
||||||
// type: "success",
|
type: "success",
|
||||||
// title: "Link Copied!",
|
title: "Link Copied!",
|
||||||
// message: "Issue link copied to clipboard.",
|
message: "Issue link copied to clipboard.",
|
||||||
// });
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -135,148 +167,165 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user, edi
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full space-y-2">
|
<div className="w-full h-full space-y-2">
|
||||||
{parentIssue && parentIssue?.sub_issues_count > 0 ? (
|
{!issues && isLoading ? (
|
||||||
|
<div className="py-3 text-center text-sm text-custom-text-300 font-medium">Loading...</div>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* header */}
|
{issues && issues?.sub_issues && issues?.sub_issues?.length > 0 ? (
|
||||||
<div className="relative flex items-center gap-4 text-xs">
|
<>
|
||||||
<div
|
{/* header */}
|
||||||
className="rounded border border-custom-border-100 shadow p-1.5 px-2 flex items-center gap-1 hover:bg-custom-background-80 transition-all cursor-pointer select-none"
|
<div className="relative flex items-center gap-4 text-xs">
|
||||||
onClick={() => handleIssuesVisibility(parentIssue?.id)}
|
<div
|
||||||
>
|
className="rounded border border-custom-border-100 shadow p-1.5 px-2 flex items-center gap-1 hover:bg-custom-background-80 transition-all cursor-pointer select-none"
|
||||||
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
|
onClick={() =>
|
||||||
{issuesVisibility.includes(parentIssue?.id) ? (
|
handleIssuesLoader({ key: "visibility", issueId: parentIssue?.id })
|
||||||
<ChevronDown width={16} strokeWidth={2} />
|
}
|
||||||
) : (
|
>
|
||||||
<ChevronRight width={14} strokeWidth={2} />
|
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
|
||||||
|
{issuesLoader.visibility.includes(parentIssue?.id) ? (
|
||||||
|
<ChevronDown width={16} strokeWidth={2} />
|
||||||
|
) : (
|
||||||
|
<ChevronRight width={14} strokeWidth={2} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>Sub-issues</div>
|
||||||
|
<div>({issues?.sub_issues?.length || 0})</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-[250px] select-none">
|
||||||
|
<ProgressBar
|
||||||
|
total={issues?.sub_issues?.length || 0}
|
||||||
|
done={
|
||||||
|
(issues?.state_distribution?.cancelled || 0) +
|
||||||
|
(issues?.state_distribution?.completed || 0)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditable && issuesLoader.visibility.includes(parentIssue?.id) && (
|
||||||
|
<div className="ml-auto flex-shrink-0 flex items-center gap-2 select-none">
|
||||||
|
<div
|
||||||
|
className="hover:bg-custom-background-80 transition-all cursor-pointer p-1.5 px-2 rounded border border-custom-border-100 shadow"
|
||||||
|
onClick={() => handleIssueCrudOperation("create", parentIssue?.id)}
|
||||||
|
>
|
||||||
|
Add sub-issue
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="hover:bg-custom-background-80 transition-all cursor-pointer p-1.5 px-2 rounded border border-custom-border-100 shadow"
|
||||||
|
onClick={() => handleIssueCrudOperation("existing", parentIssue?.id)}
|
||||||
|
>
|
||||||
|
Add an existing issue
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>Sub-issues</div>
|
|
||||||
<div>({parentIssue?.sub_issues_count})</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full max-w-[250px] select-none">
|
{/* issues */}
|
||||||
<ProgressBar
|
{issuesLoader.visibility.includes(parentIssue?.id) && (
|
||||||
total={parentIssue?.sub_issues_count}
|
<div className="border border-b-0 border-custom-border-100">
|
||||||
done={
|
<SubIssuesRootList
|
||||||
(issues?.state_distribution?.cancelled || 0) +
|
workspaceSlug={workspaceSlug}
|
||||||
(issues?.state_distribution?.completed || 0)
|
projectId={projectId}
|
||||||
}
|
parentIssue={parentIssue}
|
||||||
/>
|
user={undefined}
|
||||||
</div>
|
editable={isEditable}
|
||||||
|
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
||||||
{isEditable && issuesVisibility.includes(parentIssue?.id) && (
|
issuesLoader={issuesLoader}
|
||||||
<div className="ml-auto flex-shrink-0 flex items-center gap-2 select-none">
|
handleIssuesLoader={handleIssuesLoader}
|
||||||
<div
|
copyText={copyText}
|
||||||
className="hover:bg-custom-background-80 transition-all cursor-pointer p-1.5 px-2 rounded border border-custom-border-100 shadow"
|
handleIssueCrudOperation={handleIssueCrudOperation}
|
||||||
onClick={() => handleIssueCrudOperation("create", parentIssue?.id)}
|
/>
|
||||||
>
|
|
||||||
Add sub-issue
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
)}
|
||||||
className="hover:bg-custom-background-80 transition-all cursor-pointer p-1.5 px-2 rounded border border-custom-border-100 shadow"
|
|
||||||
onClick={() => handleIssueCrudOperation("existing", parentIssue?.id)}
|
|
||||||
>
|
|
||||||
Add an existing issue
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* issues */}
|
|
||||||
{issuesVisibility.includes(parentIssue?.id) && (
|
|
||||||
<div className="border border-b-0 border-custom-border-100">
|
|
||||||
<SubIssuesRootList
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
parentIssue={parentIssue}
|
|
||||||
user={undefined}
|
|
||||||
editable={isEditable}
|
|
||||||
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
|
||||||
issuesVisibility={issuesVisibility}
|
|
||||||
handleIssuesVisibility={handleIssuesVisibility}
|
|
||||||
copyText={copyText}
|
|
||||||
handleIssueCrudOperation={handleIssueCrudOperation}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
isEditable && (
|
|
||||||
<div className="text-xs py-2 text-custom-text-300 font-medium">
|
|
||||||
<div className="py-3 text-center">No sub issues are available</div>
|
|
||||||
<>
|
|
||||||
<CustomMenu
|
|
||||||
label={
|
|
||||||
<>
|
|
||||||
<Plus className="h-3 w-3" />
|
|
||||||
Add sub-issue
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
buttonClassName="whitespace-nowrap"
|
|
||||||
position="left"
|
|
||||||
noBorder
|
|
||||||
noChevron
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => handleIssueCrudOperation("create", parentIssue?.id)}
|
|
||||||
>
|
|
||||||
Create new
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => handleIssueCrudOperation("existing", parentIssue?.id)}
|
|
||||||
>
|
|
||||||
Add an existing issue
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</>
|
</>
|
||||||
</div>
|
) : (
|
||||||
)
|
isEditable && (
|
||||||
)}
|
<div className="text-xs py-2 text-custom-text-300 font-medium">
|
||||||
|
<div className="py-3 text-center">No sub issues are available</div>
|
||||||
{isEditable && issueCrudOperation?.create?.toggle && (
|
<>
|
||||||
<CreateUpdateIssueModal
|
<CustomMenu
|
||||||
isOpen={issueCrudOperation?.create?.toggle}
|
label={
|
||||||
prePopulateData={{
|
<>
|
||||||
parent: issueCrudOperation?.create?.issueId,
|
<Plus className="h-3 w-3" />
|
||||||
}}
|
Add sub-issue
|
||||||
handleClose={() => handleIssueCrudOperation("create", null)}
|
</>
|
||||||
/>
|
}
|
||||||
)}
|
buttonClassName="whitespace-nowrap"
|
||||||
|
position="left"
|
||||||
{isEditable &&
|
noBorder
|
||||||
issueCrudOperation?.existing?.toggle &&
|
noChevron
|
||||||
issueCrudOperation?.existing?.issueId && (
|
>
|
||||||
<ExistingIssuesListModal
|
<CustomMenu.MenuItem
|
||||||
isOpen={issueCrudOperation?.existing?.toggle}
|
onClick={() => {
|
||||||
handleClose={() => handleIssueCrudOperation("existing", null)}
|
mutateSubIssues(parentIssue?.id);
|
||||||
searchParams={{ sub_issue: true, issue_id: issueCrudOperation?.existing?.issueId }}
|
handleIssueCrudOperation("create", parentIssue?.id);
|
||||||
handleOnSubmit={addAsSubIssueFromExistingIssues}
|
}}
|
||||||
workspaceLevelToggle
|
>
|
||||||
/>
|
Create new
|
||||||
)}
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
{isEditable && issueCrudOperation?.edit?.toggle && issueCrudOperation?.edit?.issueId && (
|
onClick={() => {
|
||||||
<CreateUpdateIssueModal
|
mutateSubIssues(parentIssue?.id);
|
||||||
isOpen={issueCrudOperation?.edit?.toggle}
|
handleIssueCrudOperation("existing", parentIssue?.id);
|
||||||
handleClose={() => {
|
}}
|
||||||
mutateSubIssues(issueCrudOperation?.edit?.issueId);
|
>
|
||||||
handleIssueCrudOperation("edit", null, null);
|
Add an existing issue
|
||||||
}}
|
</CustomMenu.MenuItem>
|
||||||
data={issueCrudOperation?.edit?.issue}
|
</CustomMenu>
|
||||||
/>
|
</>
|
||||||
)}
|
</div>
|
||||||
|
)
|
||||||
{isEditable && issueCrudOperation?.delete?.toggle && issueCrudOperation?.delete?.issueId && (
|
)}
|
||||||
<DeleteIssueModal
|
{isEditable && issueCrudOperation?.create?.toggle && (
|
||||||
isOpen={issueCrudOperation?.delete?.toggle}
|
<CreateUpdateIssueModal
|
||||||
handleClose={() => {
|
isOpen={issueCrudOperation?.create?.toggle}
|
||||||
mutateSubIssues(issueCrudOperation?.delete?.issueId);
|
prePopulateData={{
|
||||||
handleIssueCrudOperation("delete", null, null);
|
parent: issueCrudOperation?.create?.issueId,
|
||||||
}}
|
}}
|
||||||
data={issueCrudOperation?.delete?.issue}
|
handleClose={() => {
|
||||||
user={user}
|
mutateSubIssues(issueCrudOperation?.create?.issueId);
|
||||||
redirection={false}
|
handleIssueCrudOperation("create", null);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isEditable &&
|
||||||
|
issueCrudOperation?.existing?.toggle &&
|
||||||
|
issueCrudOperation?.existing?.issueId && (
|
||||||
|
<ExistingIssuesListModal
|
||||||
|
isOpen={issueCrudOperation?.existing?.toggle}
|
||||||
|
handleClose={() => handleIssueCrudOperation("existing", null)}
|
||||||
|
searchParams={{ sub_issue: true, issue_id: issueCrudOperation?.existing?.issueId }}
|
||||||
|
handleOnSubmit={addAsSubIssueFromExistingIssues}
|
||||||
|
workspaceLevelToggle
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isEditable && issueCrudOperation?.edit?.toggle && issueCrudOperation?.edit?.issueId && (
|
||||||
|
<>
|
||||||
|
<CreateUpdateIssueModal
|
||||||
|
isOpen={issueCrudOperation?.edit?.toggle}
|
||||||
|
handleClose={() => {
|
||||||
|
mutateSubIssues(issueCrudOperation?.edit?.issueId);
|
||||||
|
handleIssueCrudOperation("edit", null, null);
|
||||||
|
}}
|
||||||
|
data={issueCrudOperation?.edit?.issue}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isEditable &&
|
||||||
|
issueCrudOperation?.delete?.toggle &&
|
||||||
|
issueCrudOperation?.delete?.issueId && (
|
||||||
|
<DeleteIssueModal
|
||||||
|
isOpen={issueCrudOperation?.delete?.toggle}
|
||||||
|
handleClose={() => {
|
||||||
|
mutateSubIssues(issueCrudOperation?.delete?.issueId);
|
||||||
|
handleIssueCrudOperation("delete", null, null);
|
||||||
|
}}
|
||||||
|
data={issueCrudOperation?.delete?.issue}
|
||||||
|
user={user}
|
||||||
|
redirection={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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 addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2");
|
||||||
|
|
||||||
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
|
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
|
||||||
@ -122,3 +129,65 @@ export const objToQueryParams = (obj: any) => {
|
|||||||
|
|
||||||
return params.toString();
|
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 {
|
return {
|
||||||
ganttIssues,
|
ganttIssues,
|
||||||
mutateGanttIssues,
|
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;
|
Loading…
Reference in New Issue
Block a user