mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: merge conflicts resolved from preview
This commit is contained in:
commit
7f10cee40d
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "plane-api",
|
"name": "plane-api",
|
||||||
"version": "0.18.0"
|
"version": "0.19.0"
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
# Module improts
|
# Module improts
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
|
from .issue import IssueExpandSerializer
|
||||||
from plane.db.models import InboxIssue
|
from plane.db.models import InboxIssue
|
||||||
|
|
||||||
|
|
||||||
class InboxIssueSerializer(BaseSerializer):
|
class InboxIssueSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InboxIssue
|
model = InboxIssue
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
@ -19,7 +19,6 @@ from rest_framework.views import APIView
|
|||||||
# Module imports
|
# Module imports
|
||||||
from plane.api.middleware.api_authentication import APIKeyAuthentication
|
from plane.api.middleware.api_authentication import APIKeyAuthentication
|
||||||
from plane.api.rate_limit import ApiKeyRateThrottle
|
from plane.api.rate_limit import ApiKeyRateThrottle
|
||||||
from plane.bgtasks.webhook_task import send_webhook
|
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
|
||||||
@ -38,40 +37,6 @@ class TimezoneMixin:
|
|||||||
timezone.deactivate()
|
timezone.deactivate()
|
||||||
|
|
||||||
|
|
||||||
class WebhookMixin:
|
|
||||||
webhook_event = None
|
|
||||||
bulk = False
|
|
||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
|
||||||
response = super().finalize_response(
|
|
||||||
request, response, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for the case should webhook be sent
|
|
||||||
if (
|
|
||||||
self.webhook_event
|
|
||||||
and self.request.method in ["POST", "PATCH", "DELETE"]
|
|
||||||
and response.status_code in [200, 201, 204]
|
|
||||||
):
|
|
||||||
url = request.build_absolute_uri()
|
|
||||||
parsed_url = urlparse(url)
|
|
||||||
# Extract the scheme and netloc
|
|
||||||
scheme = parsed_url.scheme
|
|
||||||
netloc = parsed_url.netloc
|
|
||||||
# Push the object to delay
|
|
||||||
send_webhook.delay(
|
|
||||||
event=self.webhook_event,
|
|
||||||
payload=response.data,
|
|
||||||
kw=self.kwargs,
|
|
||||||
action=self.request.method,
|
|
||||||
slug=self.workspace_slug,
|
|
||||||
bulk=self.bulk,
|
|
||||||
current_site=f"{scheme}://{netloc}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||||
authentication_classes = [
|
authentication_classes = [
|
||||||
APIKeyAuthentication,
|
APIKeyAuthentication,
|
||||||
|
@ -5,6 +5,7 @@ import json
|
|||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.db.models import Count, F, Func, OuterRef, Q, Sum
|
from django.db.models import Count, F, Func, OuterRef, Q, Sum
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -26,10 +27,11 @@ from plane.db.models import (
|
|||||||
)
|
)
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
|
|
||||||
|
|
||||||
class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
class CycleAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
`update` and `destroy` actions related to cycle.
|
`update` and `destroy` actions related to cycle.
|
||||||
@ -277,6 +279,16 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
owned_by=request.user,
|
owned_by=request.user,
|
||||||
)
|
)
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="cycle",
|
||||||
|
model_id=str(serializer.data["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.data, status=status.HTTP_201_CREATED
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
)
|
)
|
||||||
@ -295,6 +307,11 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_instance = json.dumps(
|
||||||
|
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
|
||||||
if cycle.archived_at:
|
if cycle.archived_at:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Archived cycle cannot be edited"},
|
{"error": "Archived cycle cannot be edited"},
|
||||||
@ -344,6 +361,17 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_409_CONFLICT,
|
status=status.HTTP_409_CONFLICT,
|
||||||
)
|
)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="cycle",
|
||||||
|
model_id=str(serializer.data["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
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)
|
||||||
|
|
||||||
@ -515,7 +543,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
class CycleIssueAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`,
|
This viewset automatically provides `list`, `create`,
|
||||||
and `destroy` actions related to cycle issues.
|
and `destroy` actions related to cycle issues.
|
||||||
|
@ -154,6 +154,13 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
state=state,
|
state=state,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# create an inbox issue
|
||||||
|
inbox_issue = InboxIssue.objects.create(
|
||||||
|
inbox_id=inbox.id,
|
||||||
|
project_id=project_id,
|
||||||
|
issue=issue,
|
||||||
|
source=request.data.get("source", "in-app"),
|
||||||
|
)
|
||||||
# Create an Issue Activity
|
# Create an Issue Activity
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.created",
|
type="issue.activity.created",
|
||||||
@ -163,14 +170,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
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()),
|
||||||
)
|
inbox=str(inbox_issue.id),
|
||||||
|
|
||||||
# create an inbox issue
|
|
||||||
inbox_issue = InboxIssue.objects.create(
|
|
||||||
inbox_id=inbox.id,
|
|
||||||
project_id=project_id,
|
|
||||||
issue=issue,
|
|
||||||
source=request.data.get("source", "in-app"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = InboxIssueSerializer(inbox_issue)
|
serializer = InboxIssueSerializer(inbox_issue)
|
||||||
@ -260,6 +260,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
inbox=(inbox_issue.id),
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -327,6 +328,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
notification=False,
|
notification=False,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
inbox=str(inbox_issue.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
@ -48,11 +48,10 @@ from plane.db.models import (
|
|||||||
ProjectMember,
|
ProjectMember,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceIssueAPIEndpoint(BaseAPIView):
|
||||||
class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|
||||||
"""
|
"""
|
||||||
This viewset provides `retrieveByIssueId` on workspace level
|
This viewset provides `retrieveByIssueId` on workspace level
|
||||||
|
|
||||||
@ -60,12 +59,9 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
model = Issue
|
model = Issue
|
||||||
webhook_event = "issue"
|
webhook_event = "issue"
|
||||||
permission_classes = [
|
permission_classes = [ProjectEntityPermission]
|
||||||
ProjectEntityPermission
|
|
||||||
]
|
|
||||||
serializer_class = IssueSerializer
|
serializer_class = IssueSerializer
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project__identifier(self):
|
def project__identifier(self):
|
||||||
return self.kwargs.get("project__identifier", None)
|
return self.kwargs.get("project__identifier", None)
|
||||||
@ -91,7 +87,9 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
def get(self, request, slug, project__identifier=None, issue__identifier=None):
|
def get(
|
||||||
|
self, request, slug, project__identifier=None, issue__identifier=None
|
||||||
|
):
|
||||||
if issue__identifier and project__identifier:
|
if issue__identifier and project__identifier:
|
||||||
issue = Issue.issue_objects.annotate(
|
issue = Issue.issue_objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
@ -100,7 +98,11 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
).get(workspace__slug=slug, project__identifier=project__identifier, sequence_id=issue__identifier)
|
).get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__identifier=project__identifier,
|
||||||
|
sequence_id=issue__identifier,
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
IssueSerializer(
|
IssueSerializer(
|
||||||
issue,
|
issue,
|
||||||
@ -110,7 +112,8 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|
||||||
|
class IssueAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
`update` and `destroy` actions related to issue.
|
`update` and `destroy` actions related to issue.
|
||||||
@ -652,7 +655,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
class IssueCommentAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
`update` and `destroy` actions related to comments of the particular issue.
|
`update` and `destroy` actions related to comments of the particular issue.
|
||||||
|
@ -5,6 +5,7 @@ import json
|
|||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
|
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -28,10 +29,11 @@ from plane.db.models import (
|
|||||||
Project,
|
Project,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
|
|
||||||
|
|
||||||
class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
class ModuleAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
`update` and `destroy` actions related to module.
|
`update` and `destroy` actions related to module.
|
||||||
@ -163,6 +165,16 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_409_CONFLICT,
|
status=status.HTTP_409_CONFLICT,
|
||||||
)
|
)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="module",
|
||||||
|
model_id=str(serializer.data["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
module = Module.objects.get(pk=serializer.data["id"])
|
module = Module.objects.get(pk=serializer.data["id"])
|
||||||
serializer = ModuleSerializer(module)
|
serializer = ModuleSerializer(module)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@ -172,6 +184,11 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
module = Module.objects.get(
|
module = Module.objects.get(
|
||||||
pk=pk, project_id=project_id, workspace__slug=slug
|
pk=pk, project_id=project_id, workspace__slug=slug
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_instance = json.dumps(
|
||||||
|
ModuleSerializer(module).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
|
||||||
if module.archived_at:
|
if module.archived_at:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Archived module cannot be edited"},
|
{"error": "Archived module cannot be edited"},
|
||||||
@ -204,6 +221,18 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_409_CONFLICT,
|
status=status.HTTP_409_CONFLICT,
|
||||||
)
|
)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="module",
|
||||||
|
model_id=str(serializer.data["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@ -260,7 +289,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
class ModuleIssueAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
`update` and `destroy` actions related to module issues.
|
`update` and `destroy` actions related to module issues.
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
|
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -23,11 +27,11 @@ from plane.db.models import (
|
|||||||
State,
|
State,
|
||||||
Workspace,
|
Workspace,
|
||||||
)
|
)
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
class ProjectAPIEndpoint(BaseAPIView):
|
||||||
"""Project Endpoints to create, update, list, retrieve and delete endpoint"""
|
"""Project Endpoints to create, update, list, retrieve and delete endpoint"""
|
||||||
|
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
@ -236,6 +240,17 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.filter(pk=serializer.data["id"])
|
.filter(pk=serializer.data["id"])
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
# Model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="project",
|
||||||
|
model_id=str(project.id),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
serializer = ProjectSerializer(project)
|
serializer = ProjectSerializer(project)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.data, status=status.HTTP_201_CREATED
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
@ -265,7 +280,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
try:
|
try:
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
project = Project.objects.get(pk=pk)
|
project = Project.objects.get(pk=pk)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
if project.archived_at:
|
if project.archived_at:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Archived project cannot be updated"},
|
{"error": "Archived project cannot be updated"},
|
||||||
@ -303,6 +320,17 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.filter(pk=serializer.data["id"])
|
.filter(pk=serializer.data["id"])
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="project",
|
||||||
|
model_id=str(project.id),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
serializer = ProjectSerializer(project)
|
serializer = ProjectSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -442,7 +442,7 @@ class IssueLinkSerializer(BaseSerializer):
|
|||||||
raise serializers.ValidationError("Invalid URL format.")
|
raise serializers.ValidationError("Invalid URL format.")
|
||||||
|
|
||||||
# Check URL scheme
|
# Check URL scheme
|
||||||
if not value.startswith(('http://', 'https://')):
|
if not value.startswith(("http://", "https://")):
|
||||||
raise serializers.ValidationError("Invalid URL scheme.")
|
raise serializers.ValidationError("Invalid URL scheme.")
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
@ -29,7 +29,7 @@ from .user.base import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from .base import BaseAPIView, BaseViewSet, WebhookMixin
|
from .base import BaseAPIView, BaseViewSet
|
||||||
|
|
||||||
from .workspace.base import (
|
from .workspace.base import (
|
||||||
WorkSpaceViewSet,
|
WorkSpaceViewSet,
|
||||||
|
@ -19,8 +19,6 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.authentication.session import BaseSessionAuthentication
|
|
||||||
from plane.bgtasks.webhook_task import send_webhook
|
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
|
||||||
@ -39,35 +37,6 @@ class TimezoneMixin:
|
|||||||
timezone.deactivate()
|
timezone.deactivate()
|
||||||
|
|
||||||
|
|
||||||
class WebhookMixin:
|
|
||||||
webhook_event = None
|
|
||||||
bulk = False
|
|
||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
|
||||||
response = super().finalize_response(
|
|
||||||
request, response, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for the case should webhook be sent
|
|
||||||
if (
|
|
||||||
self.webhook_event
|
|
||||||
and self.request.method in ["POST", "PATCH", "DELETE"]
|
|
||||||
and response.status_code in [200, 201, 204]
|
|
||||||
):
|
|
||||||
# Push the object to delay
|
|
||||||
send_webhook.delay(
|
|
||||||
event=self.webhook_event,
|
|
||||||
payload=response.data,
|
|
||||||
kw=self.kwargs,
|
|
||||||
action=self.request.method,
|
|
||||||
slug=self.workspace_slug,
|
|
||||||
bulk=self.bulk,
|
|
||||||
current_site=request.META.get("HTTP_ORIGIN"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -47,10 +48,11 @@ from plane.db.models import (
|
|||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseAPIView, BaseViewSet, WebhookMixin
|
from .. import BaseAPIView, BaseViewSet
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
|
|
||||||
|
|
||||||
class CycleViewSet(WebhookMixin, BaseViewSet):
|
class CycleViewSet(BaseViewSet):
|
||||||
serializer_class = CycleSerializer
|
serializer_class = CycleSerializer
|
||||||
model = Cycle
|
model = Cycle
|
||||||
webhook_event = "cycle"
|
webhook_event = "cycle"
|
||||||
@ -412,6 +414,17 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="cycle",
|
||||||
|
model_id=str(cycle["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
return Response(cycle, status=status.HTTP_201_CREATED)
|
return Response(cycle, status=status.HTTP_201_CREATED)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -434,6 +447,11 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
{"error": "Archived cycle cannot be updated"},
|
{"error": "Archived cycle cannot be updated"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_instance = json.dumps(
|
||||||
|
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
|
||||||
request_data = request.data
|
request_data = request.data
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -487,6 +505,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"status",
|
"status",
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="cycle",
|
||||||
|
model_id=str(cycle["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
return Response(cycle, status=status.HTTP_200_OK)
|
return Response(cycle, 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)
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet, WebhookMixin
|
from .. import BaseViewSet
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
@ -38,9 +38,9 @@ from plane.db.models import (
|
|||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
|
class CycleIssueViewSet(BaseViewSet):
|
||||||
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|
||||||
serializer_class = CycleIssueSerializer
|
serializer_class = CycleIssueSerializer
|
||||||
model = CycleIssue
|
model = CycleIssue
|
||||||
|
|
||||||
@ -191,6 +191,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"is_draft",
|
"is_draft",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
issues = user_timezone_converter(
|
||||||
|
issues, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def create(self, request, slug, project_id, cycle_id):
|
def create(self, request, slug, project_id, cycle_id):
|
||||||
@ -249,6 +254,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
update_cycle_issue_activity = []
|
update_cycle_issue_activity = []
|
||||||
# Iterate over each cycle_issue in cycle_issues
|
# Iterate over each cycle_issue in cycle_issues
|
||||||
for cycle_issue in cycle_issues:
|
for cycle_issue in cycle_issues:
|
||||||
|
old_cycle_id = cycle_issue.cycle_id
|
||||||
# Update the cycle_issue's cycle_id
|
# Update the cycle_issue's cycle_id
|
||||||
cycle_issue.cycle_id = cycle_id
|
cycle_issue.cycle_id = cycle_id
|
||||||
# Add the modified cycle_issue to the records_to_update list
|
# Add the modified cycle_issue to the records_to_update list
|
||||||
@ -256,7 +262,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
# Record the update activity
|
# Record the update activity
|
||||||
update_cycle_issue_activity.append(
|
update_cycle_issue_activity.append(
|
||||||
{
|
{
|
||||||
"old_cycle_id": str(cycle_issue.cycle_id),
|
"old_cycle_id": str(old_cycle_id),
|
||||||
"new_cycle_id": str(cycle_id),
|
"new_cycle_id": str(cycle_id),
|
||||||
"issue_id": str(cycle_issue.issue_id),
|
"issue_id": str(cycle_issue.issue_id),
|
||||||
}
|
}
|
||||||
|
@ -251,6 +251,16 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
inbox_id = Inbox.objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
).first()
|
||||||
|
# create an inbox issue
|
||||||
|
inbox_issue = InboxIssue.objects.create(
|
||||||
|
inbox_id=inbox_id.id,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=serializer.data["id"],
|
||||||
|
source=request.data.get("source", "in-app"),
|
||||||
|
)
|
||||||
# Create an Issue Activity
|
# Create an Issue Activity
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.created",
|
type="issue.activity.created",
|
||||||
@ -262,16 +272,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
notification=True,
|
notification=True,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
)
|
inbox=str(inbox_issue.id),
|
||||||
inbox_id = Inbox.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id
|
|
||||||
).first()
|
|
||||||
# create an inbox issue
|
|
||||||
inbox_issue = InboxIssue.objects.create(
|
|
||||||
inbox_id=inbox_id.id,
|
|
||||||
project_id=project_id,
|
|
||||||
issue_id=serializer.data["id"],
|
|
||||||
source=request.data.get("source", "in-app"),
|
|
||||||
)
|
)
|
||||||
inbox_issue = (
|
inbox_issue = (
|
||||||
InboxIssue.objects.select_related("issue")
|
InboxIssue.objects.select_related("issue")
|
||||||
@ -339,7 +340,24 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
# Get issue data
|
# Get issue data
|
||||||
issue_data = request.data.pop("issue", False)
|
issue_data = request.data.pop("issue", False)
|
||||||
if bool(issue_data):
|
if bool(issue_data):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
).get(
|
||||||
pk=inbox_issue.issue_id,
|
pk=inbox_issue.issue_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@ -379,6 +397,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
notification=True,
|
notification=True,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
inbox=str(inbox_issue.id),
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -444,6 +463,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
notification=False,
|
notification=False,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
inbox=(inbox_issue.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
inbox_issue = (
|
inbox_issue = (
|
||||||
@ -480,7 +500,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
output_field=ArrayField(UUIDField()),
|
output_field=ArrayField(UUIDField()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).first()
|
)
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
serializer = InboxIssueDetailSerializer(inbox_issue).data
|
serializer = InboxIssueDetailSerializer(inbox_issue).data
|
||||||
return Response(serializer, status=status.HTTP_200_OK)
|
return Response(serializer, status=status.HTTP_200_OK)
|
||||||
|
@ -47,7 +47,7 @@ from plane.db.models import (
|
|||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
class IssueArchiveViewSet(BaseViewSet):
|
class IssueArchiveViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -239,6 +239,11 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
"is_draft",
|
"is_draft",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
issues = user_timezone_converter(
|
||||||
|
issue_queryset, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
|
@ -50,9 +50,10 @@ from plane.db.models import (
|
|||||||
Project,
|
Project,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseAPIView, BaseViewSet, WebhookMixin
|
from .. import BaseAPIView, BaseViewSet
|
||||||
|
|
||||||
|
|
||||||
class IssueListEndpoint(BaseAPIView):
|
class IssueListEndpoint(BaseAPIView):
|
||||||
@ -241,10 +242,14 @@ class IssueListEndpoint(BaseAPIView):
|
|||||||
"is_draft",
|
"is_draft",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
issues = user_timezone_converter(
|
||||||
|
issues, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSet(WebhookMixin, BaseViewSet):
|
class IssueViewSet(BaseViewSet):
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
return (
|
return (
|
||||||
IssueCreateSerializer
|
IssueCreateSerializer
|
||||||
@ -440,6 +445,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"is_draft",
|
"is_draft",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
issues = user_timezone_converter(
|
||||||
|
issue_queryset, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
@ -503,6 +512,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
issue = user_timezone_converter(
|
||||||
|
issue, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
return Response(issue, status=status.HTTP_201_CREATED)
|
return Response(issue, 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)
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet, WebhookMixin
|
from .. import BaseViewSet
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
@ -25,7 +25,7 @@ from plane.db.models import (
|
|||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
class IssueCommentViewSet(BaseViewSet):
|
||||||
serializer_class = IssueCommentSerializer
|
serializer_class = IssueCommentSerializer
|
||||||
model = IssueComment
|
model = IssueComment
|
||||||
webhook_event = "issue_comment"
|
webhook_event = "issue_comment"
|
||||||
|
@ -45,6 +45,7 @@ from plane.db.models import (
|
|||||||
Project,
|
Project,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet
|
from .. import BaseViewSet
|
||||||
@ -229,6 +230,10 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
"is_draft",
|
"is_draft",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
issues = user_timezone_converter(
|
||||||
|
issue_queryset, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
|
@ -31,6 +31,7 @@ from plane.db.models import (
|
|||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
@ -132,6 +133,10 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||||||
"is_draft",
|
"is_draft",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
sub_issues = user_timezone_converter(
|
||||||
|
sub_issues, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"sub_issues": sub_issues,
|
"sub_issues": sub_issues,
|
||||||
|
@ -32,6 +32,8 @@ from plane.db.models import (
|
|||||||
ModuleLink,
|
ModuleLink,
|
||||||
)
|
)
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseAPIView
|
from .. import BaseAPIView
|
||||||
@ -199,6 +201,10 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
modules = user_timezone_converter(
|
||||||
|
modules, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
return Response(modules, status=status.HTTP_200_OK)
|
return Response(modules, status=status.HTTP_200_OK)
|
||||||
else:
|
else:
|
||||||
queryset = (
|
queryset = (
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
# Django Imports
|
||||||
from django.contrib.postgres.aggregates import ArrayAgg
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
@ -17,14 +18,14 @@ from django.db.models import (
|
|||||||
Value,
|
Value,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
# Django Imports
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
@ -48,12 +49,12 @@ from plane.db.models import (
|
|||||||
Project,
|
Project,
|
||||||
)
|
)
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
# Module imports
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
from .. import BaseAPIView, BaseViewSet, WebhookMixin
|
from .. import BaseAPIView, BaseViewSet
|
||||||
|
|
||||||
|
|
||||||
class ModuleViewSet(WebhookMixin, BaseViewSet):
|
class ModuleViewSet(BaseViewSet):
|
||||||
model = Module
|
model = Module
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
@ -236,6 +237,20 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="module",
|
||||||
|
model_id=str(module["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
module = user_timezone_converter(
|
||||||
|
module, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
return Response(module, status=status.HTTP_201_CREATED)
|
return Response(module, 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)
|
||||||
|
|
||||||
@ -277,6 +292,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
modules = user_timezone_converter(
|
||||||
|
modules, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
return Response(modules, status=status.HTTP_200_OK)
|
return Response(modules, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
@ -412,6 +431,9 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
module = self.get_queryset().filter(pk=pk)
|
module = self.get_queryset().filter(pk=pk)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
ModuleSerializer(module).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
|
||||||
if module.first().archived_at:
|
if module.first().archived_at:
|
||||||
return Response(
|
return Response(
|
||||||
@ -454,6 +476,22 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="module",
|
||||||
|
model_id=str(module["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
module = user_timezone_converter(
|
||||||
|
module, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
return Response(module, status=status.HTTP_200_OK)
|
return Response(module, 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)
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet, WebhookMixin
|
from .. import BaseViewSet
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
@ -31,9 +31,9 @@ from plane.db.models import (
|
|||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
|
class ModuleIssueViewSet(BaseViewSet):
|
||||||
class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|
||||||
serializer_class = ModuleIssueSerializer
|
serializer_class = ModuleIssueSerializer
|
||||||
model = ModuleIssue
|
model = ModuleIssue
|
||||||
webhook_event = "module_issue"
|
webhook_event = "module_issue"
|
||||||
@ -150,6 +150,11 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"is_draft",
|
"is_draft",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
issues = user_timezone_converter(
|
||||||
|
issues, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
# create multiple issues inside a module
|
# create multiple issues inside a module
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import boto3
|
import boto3
|
||||||
|
import json
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
@ -14,6 +15,7 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -22,7 +24,7 @@ from rest_framework import serializers
|
|||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.views.base import BaseViewSet, BaseAPIView, WebhookMixin
|
from plane.app.views.base import BaseViewSet, BaseAPIView
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ProjectSerializer,
|
ProjectSerializer,
|
||||||
ProjectListSerializer,
|
ProjectListSerializer,
|
||||||
@ -50,9 +52,10 @@ from plane.db.models import (
|
|||||||
Issue,
|
Issue,
|
||||||
)
|
)
|
||||||
from plane.utils.cache import cache_response
|
from plane.utils.cache import cache_response
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
|
|
||||||
|
|
||||||
class ProjectViewSet(WebhookMixin, BaseViewSet):
|
class ProjectViewSet(BaseViewSet):
|
||||||
serializer_class = ProjectListSerializer
|
serializer_class = ProjectListSerializer
|
||||||
model = Project
|
model = Project
|
||||||
webhook_event = "project"
|
webhook_event = "project"
|
||||||
@ -185,7 +188,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.annotate(
|
.annotate(
|
||||||
total_issues=Issue.issue_objects.filter(
|
total_issues=Issue.issue_objects.filter(
|
||||||
project_id=self.kwargs.get("pk"),
|
project_id=self.kwargs.get("pk"),
|
||||||
parent__isnull=True,
|
|
||||||
)
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
@ -204,7 +206,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
archived_issues=Issue.objects.filter(
|
archived_issues=Issue.objects.filter(
|
||||||
project_id=self.kwargs.get("pk"),
|
project_id=self.kwargs.get("pk"),
|
||||||
archived_at__isnull=False,
|
archived_at__isnull=False,
|
||||||
parent__isnull=True,
|
|
||||||
)
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
@ -224,7 +225,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
draft_issues=Issue.objects.filter(
|
draft_issues=Issue.objects.filter(
|
||||||
project_id=self.kwargs.get("pk"),
|
project_id=self.kwargs.get("pk"),
|
||||||
is_draft=True,
|
is_draft=True,
|
||||||
parent__isnull=True,
|
|
||||||
)
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
@ -337,6 +337,17 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.filter(pk=serializer.data["id"])
|
.filter(pk=serializer.data["id"])
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="project",
|
||||||
|
model_id=str(project.id),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
serializer = ProjectListSerializer(project)
|
serializer = ProjectListSerializer(project)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.data, status=status.HTTP_201_CREATED
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
@ -367,7 +378,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
project = Project.objects.get(pk=pk)
|
project = Project.objects.get(pk=pk)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
if project.archived_at:
|
if project.archived_at:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Archived projects cannot be updated"},
|
{"error": "Archived projects cannot be updated"},
|
||||||
@ -405,6 +418,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.filter(pk=serializer.data["id"])
|
.filter(pk=serializer.data["id"])
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="project",
|
||||||
|
model_id=str(project.id),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
serializer = ProjectListSerializer(project)
|
serializer = ProjectListSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -42,7 +42,7 @@ from plane.db.models import (
|
|||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
class GlobalViewViewSet(BaseViewSet):
|
class GlobalViewViewSet(BaseViewSet):
|
||||||
serializer_class = IssueViewSerializer
|
serializer_class = IssueViewSerializer
|
||||||
@ -255,6 +255,10 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
"is_draft",
|
"is_draft",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
)
|
)
|
||||||
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
|
issues = user_timezone_converter(
|
||||||
|
issues, datetime_fields, request.user.user_timezone
|
||||||
|
)
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
|
|||||||
.select_related("project")
|
.select_related("project")
|
||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.select_related("owned_by")
|
.select_related("owned_by")
|
||||||
.filter(archived_at__isnull=False)
|
.filter(archived_at__isnull=True)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_issues=Count(
|
total_issues=Count(
|
||||||
"issue_cycle",
|
"issue_cycle",
|
||||||
|
@ -30,7 +30,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
|
|||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.select_related("lead")
|
.select_related("lead")
|
||||||
.prefetch_related("members")
|
.prefetch_related("members")
|
||||||
.filter(archived_at__isnull=False)
|
.filter(archived_at__isnull=True)
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"link_module",
|
"link_module",
|
||||||
|
@ -31,6 +31,7 @@ from plane.db.models import (
|
|||||||
)
|
)
|
||||||
from plane.settings.redis import redis_instance
|
from plane.settings.redis import redis_instance
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
|
from plane.bgtasks.webhook_task import webhook_activity
|
||||||
|
|
||||||
|
|
||||||
# Track Changes in name
|
# Track Changes in name
|
||||||
@ -1296,7 +1297,7 @@ def create_issue_vote_activity(
|
|||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
actor_id=actor_id,
|
actor_id=actor_id,
|
||||||
verb="created",
|
verb="updated",
|
||||||
old_value=None,
|
old_value=None,
|
||||||
new_value=requested_data.get("vote"),
|
new_value=requested_data.get("vote"),
|
||||||
field="vote",
|
field="vote",
|
||||||
@ -1365,7 +1366,7 @@ def create_issue_relation_activity(
|
|||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
actor_id=actor_id,
|
actor_id=actor_id,
|
||||||
verb="created",
|
verb="updated",
|
||||||
old_value="",
|
old_value="",
|
||||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||||
field=requested_data.get("relation_type"),
|
field=requested_data.get("relation_type"),
|
||||||
@ -1380,7 +1381,7 @@ def create_issue_relation_activity(
|
|||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=related_issue,
|
issue_id=related_issue,
|
||||||
actor_id=actor_id,
|
actor_id=actor_id,
|
||||||
verb="created",
|
verb="updated",
|
||||||
old_value="",
|
old_value="",
|
||||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||||
field=(
|
field=(
|
||||||
@ -1606,6 +1607,7 @@ def issue_activity(
|
|||||||
subscriber=True,
|
subscriber=True,
|
||||||
notification=False,
|
notification=False,
|
||||||
origin=None,
|
origin=None,
|
||||||
|
inbox=None,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
issue_activities = []
|
issue_activities = []
|
||||||
@ -1692,6 +1694,41 @@ def issue_activity(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_exception(e)
|
log_exception(e)
|
||||||
|
|
||||||
|
for activity in issue_activities_created:
|
||||||
|
webhook_activity.delay(
|
||||||
|
event=(
|
||||||
|
"issue_comment"
|
||||||
|
if activity.field == "comment"
|
||||||
|
else "inbox_issue" if inbox else "issue"
|
||||||
|
),
|
||||||
|
event_id=(
|
||||||
|
activity.issue_comment_id
|
||||||
|
if activity.field == "comment"
|
||||||
|
else inbox if inbox else activity.issue_id
|
||||||
|
),
|
||||||
|
verb=activity.verb,
|
||||||
|
field=(
|
||||||
|
"description"
|
||||||
|
if activity.field == "comment"
|
||||||
|
else activity.field
|
||||||
|
),
|
||||||
|
old_value=(
|
||||||
|
activity.old_value
|
||||||
|
if activity.old_value != ""
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
new_value=(
|
||||||
|
activity.new_value
|
||||||
|
if activity.new_value != ""
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
actor_id=activity.actor_id,
|
||||||
|
current_site=origin,
|
||||||
|
slug=activity.workspace.slug,
|
||||||
|
old_identifier=activity.old_identifier,
|
||||||
|
new_identifier=activity.new_identifier,
|
||||||
|
)
|
||||||
|
|
||||||
if notification:
|
if notification:
|
||||||
notifications.delay(
|
notifications.delay(
|
||||||
type=type,
|
type=type,
|
||||||
|
@ -25,6 +25,8 @@ from plane.api.serializers import (
|
|||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
ModuleSerializer,
|
ModuleSerializer,
|
||||||
ProjectSerializer,
|
ProjectSerializer,
|
||||||
|
UserLiteSerializer,
|
||||||
|
InboxIssueSerializer,
|
||||||
)
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Cycle,
|
Cycle,
|
||||||
@ -37,6 +39,7 @@ from plane.db.models import (
|
|||||||
User,
|
User,
|
||||||
Webhook,
|
Webhook,
|
||||||
WebhookLog,
|
WebhookLog,
|
||||||
|
InboxIssue,
|
||||||
)
|
)
|
||||||
from plane.license.utils.instance_value import get_email_configuration
|
from plane.license.utils.instance_value import get_email_configuration
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
@ -49,6 +52,8 @@ SERIALIZER_MAPPER = {
|
|||||||
"cycle_issue": CycleIssueSerializer,
|
"cycle_issue": CycleIssueSerializer,
|
||||||
"module_issue": ModuleIssueSerializer,
|
"module_issue": ModuleIssueSerializer,
|
||||||
"issue_comment": IssueCommentSerializer,
|
"issue_comment": IssueCommentSerializer,
|
||||||
|
"user": UserLiteSerializer,
|
||||||
|
"inbox_issue": InboxIssueSerializer,
|
||||||
}
|
}
|
||||||
|
|
||||||
MODEL_MAPPER = {
|
MODEL_MAPPER = {
|
||||||
@ -59,6 +64,8 @@ MODEL_MAPPER = {
|
|||||||
"cycle_issue": CycleIssue,
|
"cycle_issue": CycleIssue,
|
||||||
"module_issue": ModuleIssue,
|
"module_issue": ModuleIssue,
|
||||||
"issue_comment": IssueComment,
|
"issue_comment": IssueComment,
|
||||||
|
"user": User,
|
||||||
|
"inbox_issue": InboxIssue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -179,64 +186,6 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@shared_task()
|
|
||||||
def send_webhook(event, payload, kw, action, slug, bulk, current_site):
|
|
||||||
try:
|
|
||||||
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
|
|
||||||
|
|
||||||
if event == "project":
|
|
||||||
webhooks = webhooks.filter(project=True)
|
|
||||||
|
|
||||||
if event == "issue":
|
|
||||||
webhooks = webhooks.filter(issue=True)
|
|
||||||
|
|
||||||
if event == "module" or event == "module_issue":
|
|
||||||
webhooks = webhooks.filter(module=True)
|
|
||||||
|
|
||||||
if event == "cycle" or event == "cycle_issue":
|
|
||||||
webhooks = webhooks.filter(cycle=True)
|
|
||||||
|
|
||||||
if event == "issue_comment":
|
|
||||||
webhooks = webhooks.filter(issue_comment=True)
|
|
||||||
|
|
||||||
if webhooks:
|
|
||||||
if action in ["POST", "PATCH"]:
|
|
||||||
if bulk and event in ["cycle_issue", "module_issue"]:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
event_data = [
|
|
||||||
get_model_data(
|
|
||||||
event=event,
|
|
||||||
event_id=(
|
|
||||||
payload.get("id")
|
|
||||||
if isinstance(payload, dict)
|
|
||||||
else kw.get("pk")
|
|
||||||
),
|
|
||||||
many=False,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
if action == "DELETE":
|
|
||||||
event_data = [{"id": kw.get("pk")}]
|
|
||||||
|
|
||||||
for webhook in webhooks:
|
|
||||||
for data in event_data:
|
|
||||||
webhook_task.delay(
|
|
||||||
webhook=webhook.id,
|
|
||||||
slug=slug,
|
|
||||||
event=event,
|
|
||||||
event_data=data,
|
|
||||||
action=action,
|
|
||||||
current_site=current_site,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if settings.DEBUG:
|
|
||||||
print(e)
|
|
||||||
log_exception(e)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def send_webhook_deactivation_email(
|
def send_webhook_deactivation_email(
|
||||||
webhook_id, receiver_id, current_site, reason
|
webhook_id, receiver_id, current_site, reason
|
||||||
@ -294,3 +243,240 @@ def send_webhook_deactivation_email(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_exception(e)
|
log_exception(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(
|
||||||
|
bind=True,
|
||||||
|
autoretry_for=(requests.RequestException,),
|
||||||
|
retry_backoff=600,
|
||||||
|
max_retries=5,
|
||||||
|
retry_jitter=True,
|
||||||
|
)
|
||||||
|
def webhook_send_task(
|
||||||
|
self,
|
||||||
|
webhook,
|
||||||
|
slug,
|
||||||
|
event,
|
||||||
|
event_data,
|
||||||
|
action,
|
||||||
|
current_site,
|
||||||
|
activity,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "Autopilot",
|
||||||
|
"X-Plane-Delivery": str(uuid.uuid4()),
|
||||||
|
"X-Plane-Event": event,
|
||||||
|
}
|
||||||
|
|
||||||
|
# # Your secret key
|
||||||
|
event_data = (
|
||||||
|
json.loads(json.dumps(event_data, cls=DjangoJSONEncoder))
|
||||||
|
if event_data is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
activity = (
|
||||||
|
json.loads(json.dumps(activity, cls=DjangoJSONEncoder))
|
||||||
|
if activity is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
action = {
|
||||||
|
"POST": "create",
|
||||||
|
"PATCH": "update",
|
||||||
|
"PUT": "update",
|
||||||
|
"DELETE": "delete",
|
||||||
|
}.get(action, action)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"event": event,
|
||||||
|
"action": action,
|
||||||
|
"webhook_id": str(webhook.id),
|
||||||
|
"workspace_id": str(webhook.workspace_id),
|
||||||
|
"data": event_data,
|
||||||
|
"activity": activity,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use HMAC for generating signature
|
||||||
|
if webhook.secret_key:
|
||||||
|
hmac_signature = hmac.new(
|
||||||
|
webhook.secret_key.encode("utf-8"),
|
||||||
|
json.dumps(payload).encode("utf-8"),
|
||||||
|
hashlib.sha256,
|
||||||
|
)
|
||||||
|
signature = hmac_signature.hexdigest()
|
||||||
|
headers["X-Plane-Signature"] = signature
|
||||||
|
|
||||||
|
# Send the webhook event
|
||||||
|
response = requests.post(
|
||||||
|
webhook.url,
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log the webhook request
|
||||||
|
WebhookLog.objects.create(
|
||||||
|
workspace_id=str(webhook.workspace_id),
|
||||||
|
webhook_id=str(webhook.id),
|
||||||
|
event_type=str(event),
|
||||||
|
request_method=str(action),
|
||||||
|
request_headers=str(headers),
|
||||||
|
request_body=str(payload),
|
||||||
|
response_status=str(response.status_code),
|
||||||
|
response_headers=str(response.headers),
|
||||||
|
response_body=str(response.text),
|
||||||
|
retry_count=str(self.request.retries),
|
||||||
|
)
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
# Log the failed webhook request
|
||||||
|
WebhookLog.objects.create(
|
||||||
|
workspace_id=str(webhook.workspace_id),
|
||||||
|
webhook_id=str(webhook.id),
|
||||||
|
event_type=str(event),
|
||||||
|
request_method=str(action),
|
||||||
|
request_headers=str(headers),
|
||||||
|
request_body=str(payload),
|
||||||
|
response_status=500,
|
||||||
|
response_headers="",
|
||||||
|
response_body=str(e),
|
||||||
|
retry_count=str(self.request.retries),
|
||||||
|
)
|
||||||
|
# Retry logic
|
||||||
|
if self.request.retries >= self.max_retries:
|
||||||
|
Webhook.objects.filter(pk=webhook.id).update(is_active=False)
|
||||||
|
if webhook:
|
||||||
|
# send email for the deactivation of the webhook
|
||||||
|
send_webhook_deactivation_email(
|
||||||
|
webhook_id=webhook.id,
|
||||||
|
receiver_id=webhook.created_by_id,
|
||||||
|
reason=str(e),
|
||||||
|
current_site=current_site,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
raise requests.RequestException()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
|
log_exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def webhook_activity(
|
||||||
|
event,
|
||||||
|
verb,
|
||||||
|
field,
|
||||||
|
old_value,
|
||||||
|
new_value,
|
||||||
|
actor_id,
|
||||||
|
slug,
|
||||||
|
current_site,
|
||||||
|
event_id,
|
||||||
|
old_identifier,
|
||||||
|
new_identifier,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
|
||||||
|
|
||||||
|
if event == "project":
|
||||||
|
webhooks = webhooks.filter(project=True)
|
||||||
|
|
||||||
|
if event == "issue":
|
||||||
|
webhooks = webhooks.filter(issue=True)
|
||||||
|
|
||||||
|
if event == "module" or event == "module_issue":
|
||||||
|
webhooks = webhooks.filter(module=True)
|
||||||
|
|
||||||
|
if event == "cycle" or event == "cycle_issue":
|
||||||
|
webhooks = webhooks.filter(cycle=True)
|
||||||
|
|
||||||
|
if event == "issue_comment":
|
||||||
|
webhooks = webhooks.filter(issue_comment=True)
|
||||||
|
|
||||||
|
for webhook in webhooks:
|
||||||
|
webhook_send_task.delay(
|
||||||
|
webhook=webhook.id,
|
||||||
|
slug=slug,
|
||||||
|
event=event,
|
||||||
|
event_data=get_model_data(
|
||||||
|
event=event,
|
||||||
|
event_id=event_id,
|
||||||
|
),
|
||||||
|
action=verb,
|
||||||
|
current_site=current_site,
|
||||||
|
activity={
|
||||||
|
"field": field,
|
||||||
|
"new_value": new_value,
|
||||||
|
"old_value": old_value,
|
||||||
|
"actor": get_model_data(event="user", event_id=actor_id),
|
||||||
|
"old_identifier": old_identifier,
|
||||||
|
"new_identifier": new_identifier,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
|
log_exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def model_activity(
|
||||||
|
model_name,
|
||||||
|
model_id,
|
||||||
|
requested_data,
|
||||||
|
current_instance,
|
||||||
|
actor_id,
|
||||||
|
slug,
|
||||||
|
origin=None,
|
||||||
|
):
|
||||||
|
"""Function takes in two json and computes differences between keys of both the json"""
|
||||||
|
if current_instance is None:
|
||||||
|
webhook_activity.delay(
|
||||||
|
event=model_name,
|
||||||
|
verb="created",
|
||||||
|
field=None,
|
||||||
|
old_value=None,
|
||||||
|
new_value=None,
|
||||||
|
actor_id=actor_id,
|
||||||
|
slug=slug,
|
||||||
|
current_site=origin,
|
||||||
|
event_id=model_id,
|
||||||
|
old_identifier=None,
|
||||||
|
new_identifier=None,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load the current instance
|
||||||
|
current_instance = (
|
||||||
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Loop through all keys in requested data and check the current value and requested value
|
||||||
|
for key in requested_data:
|
||||||
|
current_value = current_instance.get(key, None)
|
||||||
|
requested_value = requested_data.get(key, None)
|
||||||
|
if current_value != requested_value:
|
||||||
|
webhook_activity.delay(
|
||||||
|
event=model_name,
|
||||||
|
verb="updated",
|
||||||
|
field=key,
|
||||||
|
old_value=current_value,
|
||||||
|
new_value=requested_value,
|
||||||
|
actor_id=actor_id,
|
||||||
|
slug=slug,
|
||||||
|
current_site=origin,
|
||||||
|
event_id=model_id,
|
||||||
|
old_identifier=None,
|
||||||
|
new_identifier=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
@ -46,7 +46,7 @@ class Command(BaseCommand):
|
|||||||
}
|
}
|
||||||
|
|
||||||
instance = Instance.objects.create(
|
instance = Instance.objects.create(
|
||||||
instance_name="Plane Free",
|
instance_name="Plane Community Edition",
|
||||||
instance_id=secrets.token_hex(12),
|
instance_id=secrets.token_hex(12),
|
||||||
license_key=None,
|
license_key=None,
|
||||||
api_key=secrets.token_hex(8),
|
api_key=secrets.token_hex(8),
|
||||||
|
25
apiserver/plane/utils/user_timezone_converter.py
Normal file
25
apiserver/plane/utils/user_timezone_converter.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import pytz
|
||||||
|
|
||||||
|
def user_timezone_converter(queryset, datetime_fields, user_timezone):
|
||||||
|
# Create a timezone object for the user's timezone
|
||||||
|
user_tz = pytz.timezone(user_timezone)
|
||||||
|
|
||||||
|
# Check if queryset is a dictionary (single item) or a list of dictionaries
|
||||||
|
if isinstance(queryset, dict):
|
||||||
|
queryset_values = [queryset]
|
||||||
|
else:
|
||||||
|
queryset_values = list(queryset.values())
|
||||||
|
|
||||||
|
# Iterate over the dictionaries in the list
|
||||||
|
for item in queryset_values:
|
||||||
|
# Iterate over the datetime fields
|
||||||
|
for field in datetime_fields:
|
||||||
|
# Convert the datetime field to the user's timezone
|
||||||
|
if item[field]:
|
||||||
|
item[field] = item[field].astimezone(user_tz)
|
||||||
|
|
||||||
|
# If queryset was a single item, return a single item
|
||||||
|
if isinstance(queryset, dict):
|
||||||
|
return queryset_values[0]
|
||||||
|
else:
|
||||||
|
return queryset_values
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"repository": "https://github.com/makeplane/plane.git",
|
"repository": "https://github.com/makeplane/plane.git",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/editor-core",
|
"name": "@plane/editor-core",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"description": "Core Editor that powers Plane",
|
"description": "Core Editor that powers Plane",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./dist/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
|
@ -1,3 +1,14 @@
|
|||||||
|
.ProseMirror {
|
||||||
|
--font-size-h1: 1.5rem;
|
||||||
|
--font-size-h2: 1.3125rem;
|
||||||
|
--font-size-h3: 1.125rem;
|
||||||
|
--font-size-h4: 0.9375rem;
|
||||||
|
--font-size-h5: 0.8125rem;
|
||||||
|
--font-size-h6: 0.75rem;
|
||||||
|
--font-size-regular: 0.9375rem;
|
||||||
|
--font-size-list: var(--font-size-regular);
|
||||||
|
}
|
||||||
|
|
||||||
.ProseMirror p.is-editor-empty:first-child::before {
|
.ProseMirror p.is-editor-empty:first-child::before {
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
float: left;
|
float: left;
|
||||||
@ -56,7 +67,7 @@
|
|||||||
|
|
||||||
/* to-do list */
|
/* to-do list */
|
||||||
ul[data-type="taskList"] li {
|
ul[data-type="taskList"] li {
|
||||||
font-size: 1rem;
|
font-size: var(--font-size-list);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +173,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
|||||||
cursor: text;
|
cursor: text;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 14px;
|
font-size: var(--font-size-regular);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -310,15 +321,15 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
font-size: 1.875rem;
|
font-size: var(--font-size-h1);
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 1.4rem;
|
margin-top: 1.4rem;
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
font-size: 1.5rem;
|
font-size: var(--font-size-h2);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
@ -326,21 +337,23 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
font-size: 1.25rem;
|
font-size: var(--font-size-h3);
|
||||||
|
font-weight: 600;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
font-size: 1rem;
|
font-size: var(--font-size-h4);
|
||||||
|
font-weight: 600;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
font-size: 0.9rem;
|
font-size: var(--font-size-h5);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
@ -348,7 +361,7 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
font-size: 0.83rem;
|
font-size: var(--font-size-h6);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
@ -356,14 +369,14 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
padding: 3px 2px;
|
padding: 3px 0;
|
||||||
font-size: 1rem;
|
font-size: var(--font-size-regular);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p,
|
.prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p,
|
||||||
.prose :where(ul):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p {
|
.prose :where(ul):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p {
|
||||||
font-size: 1rem;
|
font-size: var(--font-size-list);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/document-editor",
|
"name": "@plane/document-editor",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"description": "Package that powers Plane's Pages Editor",
|
"description": "Package that powers Plane's Pages Editor",
|
||||||
"main": "./dist/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.mjs",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/editor-extensions",
|
"name": "@plane/editor-extensions",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"description": "Package that powers Plane's Editor with extensions",
|
"description": "Package that powers Plane's Editor with extensions",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./dist/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/lite-text-editor",
|
"name": "@plane/lite-text-editor",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"description": "Package that powers Plane's Comment Editor",
|
"description": "Package that powers Plane's Comment Editor",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./dist/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
|
@ -33,6 +33,7 @@ export interface ILiteTextEditor {
|
|||||||
};
|
};
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LiteTextEditor = (props: ILiteTextEditor) => {
|
const LiteTextEditor = (props: ILiteTextEditor) => {
|
||||||
@ -48,12 +49,14 @@ const LiteTextEditor = (props: ILiteTextEditor) => {
|
|||||||
tabIndex,
|
tabIndex,
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
placeholder = "Add comment...",
|
placeholder = "Add comment...",
|
||||||
|
id = "",
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
onChange,
|
onChange,
|
||||||
initialValue,
|
initialValue,
|
||||||
value,
|
value,
|
||||||
|
id,
|
||||||
editorClassName,
|
editorClassName,
|
||||||
restoreFile: fileHandler.restore,
|
restoreFile: fileHandler.restore,
|
||||||
uploadFile: fileHandler.upload,
|
uploadFile: fileHandler.upload,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/rich-text-editor",
|
"name": "@plane/rich-text-editor",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"description": "Rich Text Editor that powers Plane",
|
"description": "Rich Text Editor that powers Plane",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./dist/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "eslint-config-custom",
|
"name": "eslint-config-custom",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {},
|
"devDependencies": {},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tailwind-config-custom",
|
"name": "tailwind-config-custom",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"description": "common tailwind configuration across monorepo",
|
"description": "common tailwind configuration across monorepo",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tsconfig",
|
"name": "tsconfig",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"files": [
|
"files": [
|
||||||
"base.json",
|
"base.json",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/types",
|
"name": "@plane/types",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./src/index.d.ts"
|
"main": "./src/index.d.ts"
|
||||||
}
|
}
|
||||||
|
2
packages/types/src/inbox.d.ts
vendored
2
packages/types/src/inbox.d.ts
vendored
@ -20,7 +20,7 @@ export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
|
|||||||
export type TInboxIssueStatus = EInboxIssueStatus;
|
export type TInboxIssueStatus = EInboxIssueStatus;
|
||||||
|
|
||||||
// filters
|
// filters
|
||||||
export type TInboxIssueFilterMemberKeys = "assignee" | "created_by";
|
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
|
||||||
|
|
||||||
export type TInboxIssueFilterDateKeys = "created_at" | "updated_at";
|
export type TInboxIssueFilterDateKeys = "created_at" | "updated_at";
|
||||||
|
|
||||||
|
5
packages/types/src/pages.d.ts
vendored
5
packages/types/src/pages.d.ts
vendored
@ -16,14 +16,9 @@ export type TPage = {
|
|||||||
project: string | undefined;
|
project: string | undefined;
|
||||||
updated_at: Date | undefined;
|
updated_at: Date | undefined;
|
||||||
updated_by: string | undefined;
|
updated_by: string | undefined;
|
||||||
view_props: TPageViewProps | undefined;
|
|
||||||
workspace: string | undefined;
|
workspace: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPageViewProps = {
|
|
||||||
full_width?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// page filters
|
// page filters
|
||||||
export type TPageNavigationTabs = "public" | "private" | "archived";
|
export type TPageNavigationTabs = "public" | "private" | "archived";
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "@plane/ui",
|
"name": "@plane/ui",
|
||||||
"description": "UI components shared across multiple apps internally",
|
"description": "UI components shared across multiple apps internally",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.mjs",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
@ -18,7 +18,7 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
|
|||||||
mentionHandler={{ highlights: mentionHighlights }}
|
mentionHandler={{ highlights: mentionHighlights }}
|
||||||
{...props}
|
{...props}
|
||||||
// overriding the customClassName to add relative class passed
|
// overriding the customClassName to add relative class passed
|
||||||
containerClassName={cn(props.containerClassName, "relative border border-custom-border-200 p-3")}
|
containerClassName={cn("relative p-0 border-none", props.containerClassName)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "space",
|
"name": "space",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo run develop",
|
"dev": "turbo run develop",
|
||||||
|
@ -85,7 +85,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
|||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
|
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, workspaceSlug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -180,6 +180,7 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
|||||||
},
|
},
|
||||||
item.className
|
item.className
|
||||||
)}
|
)}
|
||||||
|
disabled={item.disabled}
|
||||||
>
|
>
|
||||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||||
<div>
|
<div>
|
||||||
|
@ -33,7 +33,7 @@ const CollaboratorListItem: React.FC<CollaboratorListItemProps> = observer((prop
|
|||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={userDetails.avatar}
|
src={userDetails.avatar}
|
||||||
name={isCurrentUser ? "You" : userDetails.display_name}
|
name={userDetails.display_name}
|
||||||
size={69}
|
size={69}
|
||||||
className="!text-3xl !font-medium"
|
className="!text-3xl !font-medium"
|
||||||
showTooltip={false}
|
showTooltip={false}
|
||||||
|
@ -51,7 +51,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
|||||||
suggestions: mentionSuggestions,
|
suggestions: mentionSuggestions,
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
containerClassName={cn(containerClassName, "relative min-h-[150px] border border-custom-border-200 p-3")}
|
containerClassName={cn("relative pl-3", containerClassName)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,7 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
// overriding the containerClassName to add relative class passed
|
// overriding the containerClassName to add relative class passed
|
||||||
containerClassName={cn(props.containerClassName, "relative border border-custom-border-200 p-3")}
|
containerClassName={cn(props.containerClassName, "relative pl-3")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -161,7 +161,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("relative flex flex-col h-full select-none rounded-sm bg-custom-background-100 shadow", {
|
className={cn("relative flex flex-col h-full select-none rounded-sm bg-custom-background-100 shadow", {
|
||||||
"fixed inset-0 z-[999999] bg-custom-background-100": fullScreenMode,
|
"fixed inset-0 z-20 bg-custom-background-100": fullScreenMode,
|
||||||
"border-[0.5px] border-custom-border-200": border,
|
"border-[0.5px] border-custom-border-200": border,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -145,8 +145,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
const issueCount = cycleDetails
|
const issueCount = cycleDetails
|
||||||
? issueFilters?.displayFilters?.sub_issue && cycleDetails?.sub_issues
|
? !issueFilters?.displayFilters?.sub_issue && cycleDetails?.sub_issues
|
||||||
? cycleDetails.total_issues + cycleDetails?.sub_issues
|
? cycleDetails.total_issues - cycleDetails?.sub_issues
|
||||||
: cycleDetails.total_issues
|
: cycleDetails.total_issues
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@ -225,9 +225,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
className="ml-1.5 flex-shrink-0 truncate"
|
className="ml-1.5 flex-shrink-0 truncate"
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
>
|
>
|
||||||
{currentProjectCycleIds?.map((cycleId) => (
|
{currentProjectCycleIds?.map((cycleId) => <CycleDropdownOption key={cycleId} cycleId={cycleId} />)}
|
||||||
<CycleDropdownOption key={cycleId} cycleId={cycleId} />
|
|
||||||
))}
|
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -144,8 +144,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
const issueCount = moduleDetails
|
const issueCount = moduleDetails
|
||||||
? issueFilters?.displayFilters?.sub_issue && moduleDetails.sub_issues
|
? !issueFilters?.displayFilters?.sub_issue && moduleDetails.sub_issues
|
||||||
? moduleDetails.total_issues + moduleDetails.sub_issues
|
? moduleDetails.total_issues - moduleDetails.sub_issues
|
||||||
: moduleDetails.total_issues
|
: moduleDetails.total_issues
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@ -224,9 +224,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
className="ml-1.5 flex-shrink-0"
|
className="ml-1.5 flex-shrink-0"
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
>
|
>
|
||||||
{projectModuleIds?.map((moduleId) => (
|
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)}
|
||||||
<ModuleDropdownOption key={moduleId} moduleId={moduleId} />
|
|
||||||
))}
|
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -27,8 +27,8 @@ export const ProjectArchivesHeader: FC = observer(() => {
|
|||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
const issueCount = currentProjectDetails
|
const issueCount = currentProjectDetails
|
||||||
? issueFilters?.displayFilters?.sub_issue
|
? !issueFilters?.displayFilters?.sub_issue && currentProjectDetails.archived_sub_issues
|
||||||
? currentProjectDetails.archived_issues + currentProjectDetails.archived_sub_issues
|
? currentProjectDetails.archived_issues - currentProjectDetails.archived_sub_issues
|
||||||
: currentProjectDetails.archived_issues
|
: currentProjectDetails.archived_issues
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
@ -78,8 +78,8 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const issueCount = currentProjectDetails
|
const issueCount = currentProjectDetails
|
||||||
? issueFilters?.displayFilters?.sub_issue
|
? !issueFilters?.displayFilters?.sub_issue && currentProjectDetails.draft_sub_issues
|
||||||
? currentProjectDetails.draft_issues + currentProjectDetails.draft_sub_issues
|
? currentProjectDetails.draft_issues - currentProjectDetails.draft_sub_issues
|
||||||
: currentProjectDetails.draft_issues
|
: currentProjectDetails.draft_issues
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
@ -102,8 +102,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
const issueCount = currentProjectDetails
|
const issueCount = currentProjectDetails
|
||||||
? issueFilters?.displayFilters?.sub_issue
|
? !issueFilters?.displayFilters?.sub_issue && currentProjectDetails?.sub_issues
|
||||||
? currentProjectDetails?.total_issues + currentProjectDetails?.sub_issues
|
? currentProjectDetails?.total_issues - currentProjectDetails?.sub_issues
|
||||||
: currentProjectDetails?.total_issues
|
: currentProjectDetails?.total_issues
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
@ -293,32 +293,36 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
|||||||
</ControlLink>
|
</ControlLink>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<CustomMenu verticalEllipsis placement="bottom-start">
|
<>
|
||||||
{canMarkAsAccepted && (
|
{isAllowed && (
|
||||||
<CustomMenu.MenuItem onClick={() => setIsSnoozeDateModalOpen(true)}>
|
<CustomMenu verticalEllipsis placement="bottom-start">
|
||||||
<div className="flex items-center gap-2">
|
{canMarkAsAccepted && (
|
||||||
<Clock size={14} strokeWidth={2} />
|
<CustomMenu.MenuItem onClick={() => setIsSnoozeDateModalOpen(true)}>
|
||||||
Snooze
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<Clock size={14} strokeWidth={2} />
|
||||||
</CustomMenu.MenuItem>
|
Snooze
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
{canMarkAsDuplicate && (
|
||||||
|
<CustomMenu.MenuItem onClick={() => setSelectDuplicateIssue(true)}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileStack size={14} strokeWidth={2} />
|
||||||
|
Mark as duplicate
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
{canDelete && (
|
||||||
|
<CustomMenu.MenuItem onClick={() => setDeleteIssueModal(true)}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Trash2 size={14} strokeWidth={2} />
|
||||||
|
Delete
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
{canMarkAsDuplicate && (
|
</>
|
||||||
<CustomMenu.MenuItem onClick={() => setSelectDuplicateIssue(true)}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FileStack size={14} strokeWidth={2} />
|
|
||||||
Mark as duplicate
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
{canDelete && (
|
|
||||||
<CustomMenu.MenuItem onClick={() => setDeleteIssueModal(true)}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Trash2 size={14} strokeWidth={2} />
|
|
||||||
Delete
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,9 +31,10 @@ export const InboxIssueContentProperties: React.FC<Props> = observer((props) =>
|
|||||||
const minDate = issue.start_date ? getDate(issue.start_date) : null;
|
const minDate = issue.start_date ? getDate(issue.start_date) : null;
|
||||||
minDate?.setDate(minDate.getDate());
|
minDate?.setDate(minDate.getDate());
|
||||||
if (!issue || !issue?.id) return <></>;
|
if (!issue || !issue?.id) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-min w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
<div className="flex h-min w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
||||||
<div className="h-min w-full overflow-y-auto px-5">
|
<div className="h-min w-full overflow-y-auto px-3">
|
||||||
<h5 className="text-sm font-medium my-4">Properties</h5>
|
<h5 className="text-sm font-medium my-4">Properties</h5>
|
||||||
<div className={`divide-y-2 divide-custom-border-200 ${!isEditable ? "opacity-60" : ""}`}>
|
<div className={`divide-y-2 divide-custom-border-200 ${!isEditable ? "opacity-60" : ""}`}>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
@ -114,7 +114,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg space-y-4">
|
<div className="rounded-lg space-y-4 pl-3">
|
||||||
<IssueTitleInput
|
<IssueTitleInput
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={issue.project_id}
|
projectId={issue.project_id}
|
||||||
@ -124,6 +124,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={!isEditable}
|
disabled={!isEditable}
|
||||||
value={issue.name}
|
value={issue.name}
|
||||||
|
containerClassName="-ml-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{loader === "issue-loading" ? (
|
{loader === "issue-loading" ? (
|
||||||
@ -135,11 +136,12 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={issue.project_id}
|
projectId={issue.project_id}
|
||||||
issueId={issue.id}
|
issueId={issue.id}
|
||||||
swrIssueDescription={null}
|
swrIssueDescription={issue.description_html ?? "<p></p>"}
|
||||||
initialValue={issue.description_html ?? "<p></p>"}
|
initialValue={issue.description_html ?? "<p></p>"}
|
||||||
disabled={!isEditable}
|
disabled={!isEditable}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
|
containerClassName="-ml-3 !mb-6 border-none"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -152,12 +154,15 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<IssueAttachmentRoot
|
|
||||||
workspaceSlug={workspaceSlug}
|
<div className="pl-3">
|
||||||
projectId={projectId}
|
<IssueAttachmentRoot
|
||||||
issueId={issue.id}
|
workspaceSlug={workspaceSlug}
|
||||||
disabled={!isEditable}
|
projectId={projectId}
|
||||||
/>
|
issueId={issue.id}
|
||||||
|
disabled={!isEditable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<InboxIssueContentProperties
|
<InboxIssueContentProperties
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
@ -168,7 +173,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
duplicateIssueDetails={inboxIssue?.duplicate_issue_detail}
|
duplicateIssueDetails={inboxIssue?.duplicate_issue_detail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="pb-12">
|
<div className="pb-12 pl-3">
|
||||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issue.id} />
|
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issue.id} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -52,7 +52,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
|||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5 vertical-scrollbar scrollbar-md">
|
<div className="h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto px-6 py-5 vertical-scrollbar scrollbar-md">
|
||||||
<InboxIssueMainContent
|
<InboxIssueMainContent
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
@ -34,8 +34,8 @@ export const InboxIssueAppliedFiltersMember: FC<InboxIssueAppliedFiltersMember>
|
|||||||
if (!optionDetail) return <></>;
|
if (!optionDetail) return <></>;
|
||||||
return (
|
return (
|
||||||
<div key={value} className="relative flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
<div key={value} className="relative flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||||
<div className="w-3 h-3 flex-shrink-0 relative flex justify-center items-center overflow-hidden">
|
<div className="flex-shrink-0 relative flex justify-center items-center overflow-hidden">
|
||||||
<Avatar name={optionDetail.display_name} src={optionDetail.avatar} showTooltip={false} size="md" />
|
<Avatar name={optionDetail.display_name} src={optionDetail.avatar} showTooltip={false} size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs truncate">{optionDetail?.display_name}</div>
|
<div className="text-xs truncate">{optionDetail?.display_name}</div>
|
||||||
<div
|
<div
|
||||||
|
@ -25,7 +25,7 @@ export const InboxIssueAppliedFilters: FC = observer(() => {
|
|||||||
{/* priority */}
|
{/* priority */}
|
||||||
<InboxIssueAppliedFiltersPriority />
|
<InboxIssueAppliedFiltersPriority />
|
||||||
{/* assignees */}
|
{/* assignees */}
|
||||||
<InboxIssueAppliedFiltersMember filterKey="assignee" label="Assignee" />
|
<InboxIssueAppliedFiltersMember filterKey="assignees" label="Assignees" />
|
||||||
{/* created_by */}
|
{/* created_by */}
|
||||||
<InboxIssueAppliedFiltersMember filterKey="created_by" label="Created By" />
|
<InboxIssueAppliedFiltersMember filterKey="created_by" label="Created By" />
|
||||||
{/* label */}
|
{/* label */}
|
||||||
|
@ -60,8 +60,8 @@ export const InboxIssueFilterSelection: FC = observer(() => {
|
|||||||
{/* assignees */}
|
{/* assignees */}
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<FilterMember
|
<FilterMember
|
||||||
filterKey="assignee"
|
filterKey="assignees"
|
||||||
label="Assignee"
|
label="Assignees"
|
||||||
searchQuery={filtersSearchQuery}
|
searchQuery={filtersSearchQuery}
|
||||||
memberIds={projectMemberIds ?? []}
|
memberIds={projectMemberIds ?? []}
|
||||||
/>
|
/>
|
||||||
|
@ -133,6 +133,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
|||||||
data={formData}
|
data={formData}
|
||||||
handleData={handleFormData}
|
handleData={handleFormData}
|
||||||
editorRef={descriptionEditorRef}
|
editorRef={descriptionEditorRef}
|
||||||
|
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||||
/>
|
/>
|
||||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
||||||
<div className="relative flex justify-between items-center gap-3">
|
<div className="relative flex justify-between items-center gap-3">
|
||||||
|
@ -138,6 +138,7 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
|||||||
data={formData}
|
data={formData}
|
||||||
handleData={handleFormData}
|
handleData={handleFormData}
|
||||||
editorRef={descriptionEditorRef}
|
editorRef={descriptionEditorRef}
|
||||||
|
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||||
/>
|
/>
|
||||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} isVisible />
|
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} isVisible />
|
||||||
<div className="relative flex justify-end items-center gap-3">
|
<div className="relative flex justify-end items-center gap-3">
|
||||||
|
@ -11,6 +11,7 @@ import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
|||||||
import { useProjectInbox } from "@/hooks/store";
|
import { useProjectInbox } from "@/hooks/store";
|
||||||
|
|
||||||
type TInboxIssueDescription = {
|
type TInboxIssueDescription = {
|
||||||
|
containerClassName?: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@ -21,7 +22,7 @@ type TInboxIssueDescription = {
|
|||||||
|
|
||||||
// TODO: have to implement GPT Assistance
|
// TODO: have to implement GPT Assistance
|
||||||
export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props) => {
|
export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, workspaceId, data, handleData, editorRef } = props;
|
const { containerClassName, workspaceSlug, projectId, workspaceId, data, handleData, editorRef } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { loader } = useProjectInbox();
|
const { loader } = useProjectInbox();
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
|
|||||||
dragDropEnabled={false}
|
dragDropEnabled={false}
|
||||||
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
|
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
|
||||||
placeholder={getDescriptionPlaceholder}
|
placeholder={getDescriptionPlaceholder}
|
||||||
|
containerClassName={containerClassName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,326 +0,0 @@
|
|||||||
import { Fragment, useRef, useState } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
// icons
|
|
||||||
import { Sparkle } from "lucide-react";
|
|
||||||
import { Transition, Dialog } from "@headlessui/react";
|
|
||||||
import { EditorRefApi } from "@plane/rich-text-editor";
|
|
||||||
// types
|
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
// ui
|
|
||||||
import { Button, Input, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { GptAssistantPopover } from "@/components/core";
|
|
||||||
import { PriorityDropdown } from "@/components/dropdowns";
|
|
||||||
import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-editor";
|
|
||||||
import { ISSUE_CREATED } from "@/constants/event-tracker";
|
|
||||||
// hooks
|
|
||||||
import { useEventTracker, useWorkspace, useInstance, useProjectInbox } from "@/hooks/store";
|
|
||||||
// services
|
|
||||||
import { AIService } from "@/services/ai.service";
|
|
||||||
// components
|
|
||||||
// ui
|
|
||||||
// types
|
|
||||||
// constants
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultValues: Partial<TIssue> = {
|
|
||||||
name: "",
|
|
||||||
description_html: "<p></p>",
|
|
||||||
priority: "none",
|
|
||||||
};
|
|
||||||
|
|
||||||
// services
|
|
||||||
const aiService = new AIService();
|
|
||||||
|
|
||||||
export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
|
||||||
const { isOpen, onClose } = props;
|
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
if (!workspaceSlug || !projectId) return null;
|
|
||||||
// states
|
|
||||||
const [createMore, setCreateMore] = useState(false);
|
|
||||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
|
||||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
|
||||||
// refs
|
|
||||||
const editorRef = useRef<EditorRefApi>(null);
|
|
||||||
// hooks
|
|
||||||
const workspaceStore = useWorkspace();
|
|
||||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug.toString() as string)?.id.toString() as string;
|
|
||||||
|
|
||||||
// store hooks
|
|
||||||
const { createInboxIssue } = useProjectInbox();
|
|
||||||
const { instance } = useInstance();
|
|
||||||
const { captureIssueEvent } = useEventTracker();
|
|
||||||
// form info
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
watch,
|
|
||||||
getValues,
|
|
||||||
} = useForm<Partial<TIssue>>({ defaultValues });
|
|
||||||
const issueName = watch("name");
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
onClose();
|
|
||||||
reset(defaultValues);
|
|
||||||
editorRef?.current?.clearEditor();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: Partial<TIssue>) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
await createInboxIssue(workspaceSlug.toString(), projectId.toString(), formData)
|
|
||||||
.then((res) => {
|
|
||||||
if (!createMore) {
|
|
||||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox/?currentTab=open&inboxIssueId=${res?.issue?.id}`);
|
|
||||||
handleClose();
|
|
||||||
} else {
|
|
||||||
reset(defaultValues);
|
|
||||||
editorRef?.current?.clearEditor();
|
|
||||||
}
|
|
||||||
captureIssueEvent({
|
|
||||||
eventName: ISSUE_CREATED,
|
|
||||||
payload: {
|
|
||||||
...formData,
|
|
||||||
state: "SUCCESS",
|
|
||||||
element: "Inbox page",
|
|
||||||
},
|
|
||||||
path: router.pathname,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
captureIssueEvent({
|
|
||||||
eventName: ISSUE_CREATED,
|
|
||||||
payload: {
|
|
||||||
...formData,
|
|
||||||
state: "FAILED",
|
|
||||||
element: "Inbox page",
|
|
||||||
},
|
|
||||||
path: router.pathname,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAiAssistance = async (response: string) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
editorRef.current?.setEditorValueAtCursorPosition(response);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAutoGenerateDescription = async () => {
|
|
||||||
const issueName = getValues("name");
|
|
||||||
if (!workspaceSlug || !projectId || !issueName) return;
|
|
||||||
|
|
||||||
setIAmFeelingLucky(true);
|
|
||||||
|
|
||||||
aiService
|
|
||||||
.createGptTask(workspaceSlug as string, projectId as string, {
|
|
||||||
prompt: issueName,
|
|
||||||
task: "Generate a proper description for this issue.",
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.response === "")
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message:
|
|
||||||
"Issue title isn't informative enough to generate the description. Please try with a different title.",
|
|
||||||
});
|
|
||||||
else handleAiAssistance(res.response_html);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
const error = err?.data?.error;
|
|
||||||
|
|
||||||
if (err.status === 429)
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: error || "You have reached the maximum number of requests of 50 requests per month per user.",
|
|
||||||
});
|
|
||||||
else
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: error || "Some error occurred. Please try again.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => setIAmFeelingLucky(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
|
||||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
|
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
|
||||||
<div className="space-y-5">
|
|
||||||
<h3 className="text-xl font-semibold leading-6 text-custom-text-100">Create Inbox Issue</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="mt-2 space-y-3">
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
rules={{
|
|
||||||
required: "Title is required",
|
|
||||||
maxLength: {
|
|
||||||
value: 255,
|
|
||||||
message: "Title should be less than 255 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
ref={ref}
|
|
||||||
hasError={Boolean(errors.name)}
|
|
||||||
placeholder="Title"
|
|
||||||
className="w-full resize-none text-xl"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="border-0.5 absolute bottom-3.5 right-3.5 z-10 flex rounded bg-custom-background-80">
|
|
||||||
{watch("name") && issueName !== "" && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${
|
|
||||||
iAmFeelingLucky ? "cursor-wait" : ""
|
|
||||||
}`}
|
|
||||||
onClick={handleAutoGenerateDescription}
|
|
||||||
disabled={iAmFeelingLucky}
|
|
||||||
>
|
|
||||||
{iAmFeelingLucky ? (
|
|
||||||
"Generating response..."
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Sparkle className="h-4 w-4" />I{"'"}m feeling lucky
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{instance?.config?.has_openai_configured && (
|
|
||||||
<GptAssistantPopover
|
|
||||||
isOpen={gptAssistantModal}
|
|
||||||
projectId={projectId.toString()}
|
|
||||||
handleClose={() => {
|
|
||||||
setGptAssistantModal((prevData) => !prevData);
|
|
||||||
// this is done so that the title do not reset after gpt popover closed
|
|
||||||
reset(getValues());
|
|
||||||
}}
|
|
||||||
onResponse={(response) => {
|
|
||||||
handleAiAssistance(response);
|
|
||||||
}}
|
|
||||||
button={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
|
||||||
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
|
||||||
>
|
|
||||||
<Sparkle className="h-4 w-4" />
|
|
||||||
AI
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
className="!min-w-[38rem]"
|
|
||||||
placement="top-end"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
name="description_html"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<RichTextEditor
|
|
||||||
initialValue={!value || value === "" ? "<p></p>" : value}
|
|
||||||
ref={editorRef}
|
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
|
||||||
workspaceId={workspaceId}
|
|
||||||
projectId={projectId.toString()}
|
|
||||||
dragDropEnabled={false}
|
|
||||||
onChange={(_description: object, description_html: string) => {
|
|
||||||
onChange(description_html);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="priority"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<div className="h-5">
|
|
||||||
<PriorityDropdown
|
|
||||||
value={value ?? "none"}
|
|
||||||
onChange={onChange}
|
|
||||||
buttonVariant="background-with-text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t border-custom-border-200 px-5 pt-5">
|
|
||||||
<div
|
|
||||||
className="flex cursor-pointer items-center gap-1"
|
|
||||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
|
||||||
>
|
|
||||||
<span className="text-xs">Create more</span>
|
|
||||||
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="neutral-primary" size="sm" onClick={() => handleClose()}>
|
|
||||||
Discard
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
|
||||||
{isSubmitting ? "Adding Issue..." : "Add Issue"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
);
|
|
||||||
});
|
|
@ -2,19 +2,19 @@ import { FC } from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AlertCircle, X } from "lucide-react";
|
import { AlertCircle, X } from "lucide-react";
|
||||||
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { getFileIcon } from "@/components/icons/attachment";
|
// icons
|
||||||
|
import { getFileIcon } from "@/components/icons";
|
||||||
|
// components
|
||||||
|
import { IssueAttachmentDeleteModal } from "@/components/issues";
|
||||||
|
// helpers
|
||||||
import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper";
|
import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper";
|
||||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
import { truncateText } from "@/helpers/string.helper";
|
import { truncateText } from "@/helpers/string.helper";
|
||||||
|
// hooks
|
||||||
import { useIssueDetail, useMember } from "@/hooks/store";
|
import { useIssueDetail, useMember } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// hooks
|
|
||||||
// ui
|
|
||||||
// components
|
|
||||||
// icons
|
|
||||||
// helper
|
|
||||||
import { IssueAttachmentDeleteModal } from "./delete-attachment-confirmation-modal";
|
|
||||||
// types
|
// types
|
||||||
import { TAttachmentOperations } from "./root";
|
import { TAttachmentOperations } from "./root";
|
||||||
|
|
||||||
@ -36,24 +36,24 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
|
|||||||
isDeleteAttachmentModalOpen,
|
isDeleteAttachmentModalOpen,
|
||||||
toggleDeleteAttachmentModal,
|
toggleDeleteAttachmentModal,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
// states
|
// derived values
|
||||||
|
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
|
||||||
|
// hooks
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
const attachment = attachmentId && getAttachmentById(attachmentId);
|
|
||||||
|
|
||||||
if (!attachment) return <></>;
|
if (!attachment) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IssueAttachmentDeleteModal
|
{isDeleteAttachmentModalOpen === attachment.id && (
|
||||||
isOpen={isDeleteAttachmentModalOpen}
|
<IssueAttachmentDeleteModal
|
||||||
setIsOpen={() => toggleDeleteAttachmentModal(false)}
|
isOpen={!!isDeleteAttachmentModalOpen}
|
||||||
handleAttachmentOperations={handleAttachmentOperations}
|
onClose={() => toggleDeleteAttachmentModal(null)}
|
||||||
data={attachment}
|
handleAttachmentOperations={handleAttachmentOperations}
|
||||||
/>
|
data={attachment}
|
||||||
|
/>
|
||||||
<div
|
)}
|
||||||
key={attachmentId}
|
<div className="flex h-[60px] items-center justify-between gap-1 rounded-md border-[2px] border-custom-border-200 bg-custom-background-100 px-4 py-2 text-sm">
|
||||||
className="flex h-[60px] items-center justify-between gap-1 rounded-md border-[2px] border-custom-border-200 bg-custom-background-100 px-4 py-2 text-sm"
|
|
||||||
>
|
|
||||||
<Link href={attachment.asset} target="_blank" rel="noopener noreferrer">
|
<Link href={attachment.asset} target="_blank" rel="noopener noreferrer">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset))}</div>
|
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset))}</div>
|
||||||
@ -83,7 +83,7 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<button onClick={() => toggleDeleteAttachmentModal(true)}>
|
<button type="button" onClick={() => toggleDeleteAttachmentModal(attachment.id)}>
|
||||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@ -25,24 +25,28 @@ export const IssueAttachmentUpload: React.FC<Props> = observer((props) => {
|
|||||||
// states
|
// states
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const onDrop = useCallback(
|
||||||
const currentFile: File = acceptedFiles[0];
|
(acceptedFiles: File[]) => {
|
||||||
if (!currentFile || !workspaceSlug) return;
|
const currentFile: File = acceptedFiles[0];
|
||||||
|
if (!currentFile || !workspaceSlug) return;
|
||||||
|
|
||||||
const uploadedFile: File = new File([currentFile], generateFileName(currentFile.name), { type: currentFile.type });
|
const uploadedFile: File = new File([currentFile], generateFileName(currentFile.name), {
|
||||||
const formData = new FormData();
|
type: currentFile.type,
|
||||||
formData.append("asset", uploadedFile);
|
});
|
||||||
formData.append(
|
const formData = new FormData();
|
||||||
"attributes",
|
formData.append("asset", uploadedFile);
|
||||||
JSON.stringify({
|
formData.append(
|
||||||
name: uploadedFile.name,
|
"attributes",
|
||||||
size: uploadedFile.size,
|
JSON.stringify({
|
||||||
})
|
name: uploadedFile.name,
|
||||||
);
|
size: uploadedFile.size,
|
||||||
setIsLoading(true);
|
})
|
||||||
handleAttachmentOperations.create(formData).finally(() => setIsLoading(false));
|
);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
setIsLoading(true);
|
||||||
}, []);
|
handleAttachmentOperations.create(formData).finally(() => setIsLoading(false));
|
||||||
|
},
|
||||||
|
[handleAttachmentOperations, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, Fragment, Dispatch, SetStateAction, useState } from "react";
|
import { FC, Fragment, useState } from "react";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import type { TIssueAttachment } from "@plane/types";
|
import type { TIssueAttachment } from "@plane/types";
|
||||||
@ -14,18 +14,18 @@ export type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "c
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
onClose: () => void;
|
||||||
data: TIssueAttachment;
|
data: TIssueAttachment;
|
||||||
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
||||||
const { isOpen, setIsOpen, data, handleAttachmentOperations } = props;
|
const { isOpen, onClose, data, handleAttachmentOperations } = props;
|
||||||
// state
|
// states
|
||||||
const [loader, setLoader] = useState(false);
|
const [loader, setLoader] = useState(false);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsOpen(false);
|
onClose();
|
||||||
setLoader(false);
|
setLoader(false);
|
||||||
};
|
};
|
||||||
|
|
@ -1,7 +1,5 @@
|
|||||||
export * from "./root";
|
|
||||||
|
|
||||||
export * from "./attachment-upload";
|
|
||||||
export * from "./delete-attachment-confirmation-modal";
|
|
||||||
|
|
||||||
export * from "./attachments-list";
|
|
||||||
export * from "./attachment-detail";
|
export * from "./attachment-detail";
|
||||||
|
export * from "./attachment-upload";
|
||||||
|
export * from "./attachments-list";
|
||||||
|
export * from "./delete-attachment-modal";
|
||||||
|
export * from "./root";
|
||||||
|
@ -95,7 +95,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[workspaceSlug, projectId, issueId, createAttachment, removeAttachment]
|
[captureIssueEvent, workspaceSlug, projectId, issueId, createAttachment, removeAttachment]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -15,6 +15,7 @@ import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
|||||||
import { useWorkspace } from "@/hooks/store";
|
import { useWorkspace } from "@/hooks/store";
|
||||||
|
|
||||||
export type IssueDescriptionInputProps = {
|
export type IssueDescriptionInputProps = {
|
||||||
|
containerClassName?: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
@ -28,6 +29,7 @@ export type IssueDescriptionInputProps = {
|
|||||||
|
|
||||||
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
|
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
|
containerClassName,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
issueId,
|
issueId,
|
||||||
@ -110,11 +112,12 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||||||
placeholder={
|
placeholder={
|
||||||
placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)
|
placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)
|
||||||
}
|
}
|
||||||
|
containerClassName={containerClassName}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RichTextReadOnlyEditor
|
<RichTextReadOnlyEditor
|
||||||
initialValue={localIssueDescription.description_html ?? ""}
|
initialValue={localIssueDescription.description_html ?? ""}
|
||||||
containerClassName="!p-0 !pt-2 text-custom-text-200 min-h-[150px]"
|
containerClassName={containerClassName}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,11 @@ type TIssueCommentCreate = {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
activityOperations: TActivityOperations;
|
activityOperations: TActivityOperations;
|
||||||
showAccessSpecifier?: boolean;
|
showAccessSpecifier?: boolean;
|
||||||
|
issueId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||||
const { workspaceSlug, projectId, activityOperations, showAccessSpecifier = false } = props;
|
const { workspaceSlug, projectId, issueId, activityOperations, showAccessSpecifier = false } = props;
|
||||||
// refs
|
// refs
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
// store hooks
|
// store hooks
|
||||||
@ -72,6 +73,8 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<LiteTextEditor
|
<LiteTextEditor
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
|
id={"add_comment_" + issueId}
|
||||||
|
value={"<p></p>"}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
onEnterKeyPress={(e) => {
|
onEnterKeyPress={(e) => {
|
||||||
|
@ -146,6 +146,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<IssueCommentCreate
|
<IssueCommentCreate
|
||||||
|
issueId={issueId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
activityOperations={activityOperations}
|
activityOperations={activityOperations}
|
||||||
@ -165,6 +166,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<IssueCommentCreate
|
<IssueCommentCreate
|
||||||
|
issueId={issueId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
activityOperations={activityOperations}
|
activityOperations={activityOperations}
|
||||||
|
@ -54,7 +54,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-4 rounded-lg">
|
<div className="rounded-lg space-y-4 pl-3">
|
||||||
{issue.parent_id && (
|
{issue.parent_id && (
|
||||||
<IssueParentDetail
|
<IssueParentDetail
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
@ -85,6 +85,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={!isEditable}
|
disabled={!isEditable}
|
||||||
value={issue.name}
|
value={issue.name}
|
||||||
|
containerClassName="-ml-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* {issue?.description_html === issueDescription && ( */}
|
{/* {issue?.description_html === issueDescription && ( */}
|
||||||
@ -97,6 +98,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
disabled={!isEditable}
|
disabled={!isEditable}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
|
containerClassName="-ml-3 !mb-6 border-none"
|
||||||
/>
|
/>
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
|
|
||||||
@ -121,14 +123,18 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IssueAttachmentRoot
|
<div className="pl-3">
|
||||||
workspaceSlug={workspaceSlug}
|
<IssueAttachmentRoot
|
||||||
projectId={projectId}
|
workspaceSlug={workspaceSlug}
|
||||||
issueId={issueId}
|
projectId={projectId}
|
||||||
disabled={!isEditable}
|
issueId={issueId}
|
||||||
/>
|
disabled={!isEditable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={isArchived} />
|
<div className="pl-3">
|
||||||
|
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={isArchived} />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -47,7 +47,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
|||||||
try {
|
try {
|
||||||
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
|
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
|
||||||
await issueOperations.fetch(workspaceSlug, projectId, issueId);
|
await issueOperations.fetch(workspaceSlug, projectId, issueId);
|
||||||
toggleParentIssueModal(false);
|
toggleParentIssueModal(issueId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("something went wrong while fetching the issue");
|
console.error("something went wrong while fetching the issue");
|
||||||
}
|
}
|
||||||
@ -79,8 +79,8 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
|||||||
<ParentIssuesListModal
|
<ParentIssuesListModal
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
isOpen={isParentIssueModalOpen}
|
isOpen={isParentIssueModalOpen === issueId}
|
||||||
handleClose={() => toggleParentIssueModal(false)}
|
handleClose={() => toggleParentIssueModal(null)}
|
||||||
onChange={(issue: any) => handleParentIssue(issue?.id)}
|
onChange={(issue: any) => handleParentIssue(issue?.id)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@ -94,7 +94,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
|||||||
},
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleParentIssueModal(true)}
|
onClick={() => toggleParentIssueModal(issue.id)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{issue.parent_id && parentIssue ? (
|
{issue.parent_id && parentIssue ? (
|
||||||
|
@ -81,22 +81,26 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||||||
data.map((i) => i.id)
|
data.map((i) => i.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
toggleRelationModal(null);
|
toggleRelationModal(null, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!relationIssueIds) return null;
|
if (!relationIssueIds) return null;
|
||||||
|
|
||||||
|
const isRelationKeyModalActive =
|
||||||
|
isRelationModalOpen?.relationType === relationKey && isRelationModalOpen?.issueId === issueId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExistingIssuesListModal
|
<ExistingIssuesListModal
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
isOpen={isRelationModalOpen === relationKey}
|
isOpen={isRelationKeyModalActive}
|
||||||
handleClose={() => toggleRelationModal(null)}
|
handleClose={() => toggleRelationModal(null, null)}
|
||||||
searchParams={{ issue_relation: true, issue_id: issueId }}
|
searchParams={{ issue_relation: true, issue_id: issueId }}
|
||||||
handleOnSubmit={onSubmit}
|
handleOnSubmit={onSubmit}
|
||||||
workspaceLevelToggle
|
workspaceLevelToggle
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -104,11 +108,11 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||||||
{
|
{
|
||||||
"cursor-not-allowed": disabled,
|
"cursor-not-allowed": disabled,
|
||||||
"hover:bg-custom-background-80": !disabled,
|
"hover:bg-custom-background-80": !disabled,
|
||||||
"bg-custom-background-80": isRelationModalOpen === relationKey,
|
"bg-custom-background-80": isRelationKeyModalActive,
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleRelationModal(relationKey)}
|
onClick={() => toggleRelationModal(issueId, relationKey)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-start justify-between">
|
<div className="flex w-full items-start justify-between">
|
||||||
|
@ -357,7 +357,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full overflow-hidden">
|
<div className="flex h-full w-full overflow-hidden">
|
||||||
<div className="max-w-2/3 h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto p-5">
|
<div className="max-w-2/3 h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto px-6 py-5">
|
||||||
<IssueMainContent
|
<IssueMainContent
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
swrIssueDetails={swrIssueDetails}
|
swrIssueDetails={swrIssueDetails}
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Triangle,
|
Triangle,
|
||||||
XCircle,
|
XCircle,
|
||||||
UserCircle2
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
// components
|
// components
|
||||||
@ -37,7 +36,6 @@ import {
|
|||||||
} from "@/components/dropdowns";
|
} from "@/components/dropdowns";
|
||||||
// ui
|
// ui
|
||||||
// helpers
|
// helpers
|
||||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
|
||||||
import {
|
import {
|
||||||
ArchiveIssueModal,
|
ArchiveIssueModal,
|
||||||
DeleteIssueModal,
|
DeleteIssueModal,
|
||||||
@ -56,7 +54,7 @@ import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"
|
|||||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { useEstimate, useIssueDetail, useMember, useProject, useProjectState, useUser } from "@/hooks/store";
|
import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
import type { TIssueOperations } from "./root";
|
import type { TIssueOperations } from "./root";
|
||||||
@ -90,12 +88,9 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { getStateById } = useProjectState();
|
const { getStateById } = useProjectState();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
const { getUserDetails } = useMember();
|
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
if (!issue) return <></>;
|
if (!issue) return <></>;
|
||||||
|
|
||||||
const createdByDetails = getUserDetails(issue.created_by);
|
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
|
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
|
||||||
@ -262,21 +257,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{createdByDetails && (
|
|
||||||
<div className="flex h-8 items-center gap-2">
|
|
||||||
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
|
|
||||||
<UserCircle2 className="h-4 w-4 flex-shrink-0" />
|
|
||||||
<span>Created by</span>
|
|
||||||
</div>
|
|
||||||
<Tooltip tooltipContent={createdByDetails?.display_name} isMobile={isMobile}>
|
|
||||||
<div className="h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-default">
|
|
||||||
<ButtonAvatars showTooltip={false} userIds={createdByDetails.id} />
|
|
||||||
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex h-8 items-center gap-2">
|
<div className="flex h-8 items-center gap-2">
|
||||||
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
|
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
|
||||||
<CalendarClock className="h-4 w-4 flex-shrink-0" />
|
<CalendarClock className="h-4 w-4 flex-shrink-0" />
|
||||||
|
@ -96,12 +96,13 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
|||||||
...(appliedFilters ?? {}),
|
...(appliedFilters ?? {}),
|
||||||
},
|
},
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
captureEvent(GLOBAL_VIEW_UPDATED, {
|
if (res)
|
||||||
view_id: res.id,
|
captureEvent(GLOBAL_VIEW_UPDATED, {
|
||||||
applied_filters: res.filters,
|
view_id: res.id,
|
||||||
state: "SUCCESS",
|
applied_filters: res.filters,
|
||||||
element: "Spreadsheet view",
|
state: "SUCCESS",
|
||||||
});
|
element: "Spreadsheet view",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -75,6 +75,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
const sub_group_by = displayFilters?.sub_group_by;
|
const sub_group_by = displayFilters?.sub_group_by;
|
||||||
const group_by = displayFilters?.group_by;
|
const group_by = displayFilters?.group_by;
|
||||||
|
|
||||||
|
const orderBy = displayFilters?.order_by;
|
||||||
|
|
||||||
const userDisplayFilters = displayFilters || null;
|
const userDisplayFilters = displayFilters || null;
|
||||||
|
|
||||||
const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
|
const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
|
||||||
@ -157,7 +159,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
issues.getIssueIds,
|
issues.getIssueIds,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
group_by,
|
group_by,
|
||||||
sub_group_by
|
sub_group_by,
|
||||||
|
orderBy !== "sort_order"
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
setToast({
|
setToast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@ -259,6 +262,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
|
orderBy={orderBy}
|
||||||
updateIssue={updateIssue}
|
updateIssue={updateIssue}
|
||||||
quickActions={renderQuickActions}
|
quickActions={renderQuickActions}
|
||||||
handleKanbanFilters={handleKanbanFilters}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
|
@ -4,11 +4,12 @@ import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-d
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
import { ControlLink, DropIndicator, Tooltip } from "@plane/ui";
|
import { ControlLink, DropIndicator, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { useAppRouter, useIssueDetail, useProject, useKanbanView } from "@/hooks/store";
|
import { useAppRouter, useIssueDetail, useProject, useKanbanView } from "@/hooks/store";
|
||||||
|
|
||||||
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
import { TRenderQuickActions } from "../list/list-view-types";
|
import { TRenderQuickActions } from "../list/list-view-types";
|
||||||
@ -131,6 +132,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
|||||||
|
|
||||||
const isDragAllowed = !isDragDisabled && !issue?.tempId && canEditIssueProperties;
|
const isDragAllowed = !isDragDisabled && !issue?.tempId && canEditIssueProperties;
|
||||||
|
|
||||||
|
useOutsideClickDetector(cardRef, () => {
|
||||||
|
cardRef?.current?.classList?.remove("highlight");
|
||||||
|
});
|
||||||
|
|
||||||
// Make Issue block both as as Draggable and,
|
// Make Issue block both as as Draggable and,
|
||||||
// as a DropTarget for other issues being dragged to get the location of drop
|
// as a DropTarget for other issues being dragged to get the location of drop
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -177,7 +182,15 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
|||||||
<div
|
<div
|
||||||
// make Z-index higher at the beginning of drag, to have a issue drag image of issue block without any overlaps
|
// make Z-index higher at the beginning of drag, to have a issue drag image of issue block without any overlaps
|
||||||
className={cn("group/kanban-block relative p-1.5", { "z-[1]": isCurrentBlockDragging })}
|
className={cn("group/kanban-block relative p-1.5", { "z-[1]": isCurrentBlockDragging })}
|
||||||
onDragStart={() => isDragAllowed && setIsCurrentBlockDragging(true)}
|
onDragStart={() => {
|
||||||
|
if (isDragAllowed) setIsCurrentBlockDragging(true);
|
||||||
|
else
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.WARNING,
|
||||||
|
title: "Cannot move issue",
|
||||||
|
message: "Drag and drop is disabled for the current grouping",
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ControlLink
|
<ControlLink
|
||||||
id={`issue-${issue.id}`}
|
id={`issue-${issue.id}`}
|
||||||
@ -186,12 +199,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
|||||||
}`}
|
}`}
|
||||||
ref={cardRef}
|
ref={cardRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"block rounded border-[0.5px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
"block rounded border-[1px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||||
{
|
{ "hover:cursor-pointer": isDragAllowed },
|
||||||
"hover:cursor-pointer": isDragAllowed,
|
{ "border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id) },
|
||||||
"border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id),
|
{ "bg-custom-background-80 z-[100]": isCurrentBlockDragging }
|
||||||
"bg-custom-background-80 z-[100]": isCurrentBlockDragging,
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={() => handleIssuePeekOverview(issue)}
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
TUnGroupedIssues,
|
TUnGroupedIssues,
|
||||||
TIssueKanbanFilters,
|
TIssueKanbanFilters,
|
||||||
TIssueGroupByOptions,
|
TIssueGroupByOptions,
|
||||||
|
TIssueOrderByOptions,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
// hooks
|
// hooks
|
||||||
@ -31,6 +32,7 @@ export interface IGroupByKanBan {
|
|||||||
displayProperties: IIssueDisplayProperties | undefined;
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
sub_group_by: TIssueGroupByOptions | undefined;
|
sub_group_by: TIssueGroupByOptions | undefined;
|
||||||
group_by: TIssueGroupByOptions | undefined;
|
group_by: TIssueGroupByOptions | undefined;
|
||||||
|
orderBy: TIssueOrderByOptions | undefined;
|
||||||
sub_group_id: string;
|
sub_group_id: string;
|
||||||
isDragDisabled: boolean;
|
isDragDisabled: boolean;
|
||||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||||
@ -79,6 +81,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
handleOnDrop,
|
handleOnDrop,
|
||||||
showEmptyGroup = true,
|
showEmptyGroup = true,
|
||||||
subGroupIssueHeaderCount,
|
subGroupIssueHeaderCount,
|
||||||
|
orderBy,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const member = useMember();
|
const member = useMember();
|
||||||
@ -170,6 +173,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
|
orderBy={orderBy}
|
||||||
sub_group_id={sub_group_id}
|
sub_group_id={sub_group_id}
|
||||||
isDragDisabled={isDragDisabled}
|
isDragDisabled={isDragDisabled}
|
||||||
updateIssue={updateIssue}
|
updateIssue={updateIssue}
|
||||||
@ -196,6 +200,7 @@ export interface IKanBan {
|
|||||||
displayProperties: IIssueDisplayProperties | undefined;
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
sub_group_by: TIssueGroupByOptions | undefined;
|
sub_group_by: TIssueGroupByOptions | undefined;
|
||||||
group_by: TIssueGroupByOptions | undefined;
|
group_by: TIssueGroupByOptions | undefined;
|
||||||
|
orderBy: TIssueOrderByOptions | undefined;
|
||||||
sub_group_id?: string;
|
sub_group_id?: string;
|
||||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||||
quickActions: TRenderQuickActions;
|
quickActions: TRenderQuickActions;
|
||||||
@ -242,6 +247,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
handleOnDrop,
|
handleOnDrop,
|
||||||
showEmptyGroup,
|
showEmptyGroup,
|
||||||
subGroupIssueHeaderCount,
|
subGroupIssueHeaderCount,
|
||||||
|
orderBy,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const issueKanBanView = useKanbanView();
|
const issueKanBanView = useKanbanView();
|
||||||
@ -253,6 +259,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
|
orderBy={orderBy}
|
||||||
sub_group_id={sub_group_id}
|
sub_group_id={sub_group_id}
|
||||||
isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)}
|
isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)}
|
||||||
updateIssue={updateIssue}
|
updateIssue={updateIssue}
|
||||||
|
@ -11,14 +11,21 @@ import {
|
|||||||
TSubGroupedIssues,
|
TSubGroupedIssues,
|
||||||
TUnGroupedIssues,
|
TUnGroupedIssues,
|
||||||
TIssueGroupByOptions,
|
TIssueGroupByOptions,
|
||||||
|
TIssueOrderByOptions,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
|
import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectState } from "@/hooks/store";
|
import { useProjectState } from "@/hooks/store";
|
||||||
//components
|
//components
|
||||||
import { TRenderQuickActions } from "../list/list-view-types";
|
import { TRenderQuickActions } from "../list/list-view-types";
|
||||||
import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils";
|
import {
|
||||||
|
KanbanDropLocation,
|
||||||
|
getSourceFromDropPayload,
|
||||||
|
getDestinationFromDropPayload,
|
||||||
|
highlightIssueOnDrop,
|
||||||
|
} from "./utils";
|
||||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||||
|
|
||||||
interface IKanbanGroup {
|
interface IKanbanGroup {
|
||||||
@ -45,6 +52,7 @@ interface IKanbanGroup {
|
|||||||
groupByVisibilityToggle?: boolean;
|
groupByVisibilityToggle?: boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
|
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
|
||||||
|
orderBy: TIssueOrderByOptions | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanbanGroup = (props: IKanbanGroup) => {
|
export const KanbanGroup = (props: IKanbanGroup) => {
|
||||||
@ -52,6 +60,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
groupId,
|
groupId,
|
||||||
sub_group_id,
|
sub_group_id,
|
||||||
group_by,
|
group_by,
|
||||||
|
orderBy,
|
||||||
sub_group_by,
|
sub_group_by,
|
||||||
issuesMap,
|
issuesMap,
|
||||||
displayProperties,
|
displayProperties,
|
||||||
@ -101,13 +110,15 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
if (!source || !destination) return;
|
if (!source || !destination) return;
|
||||||
|
|
||||||
handleOnDrop(source, destination);
|
handleOnDrop(source, destination);
|
||||||
|
|
||||||
|
highlightIssueOnDrop(payload.source.element.id, orderBy !== "sort_order");
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
autoScrollForElements({
|
autoScrollForElements({
|
||||||
element,
|
element,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn]);
|
}, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn, orderBy]);
|
||||||
|
|
||||||
const prePopulateQuickAddData = (
|
const prePopulateQuickAddData = (
|
||||||
groupByKey: string | undefined,
|
groupByKey: string | undefined,
|
||||||
@ -161,16 +172,33 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
return preloadedData;
|
return preloadedData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shouldOverlay = isDraggingOverColumn && orderBy !== "sort_order";
|
||||||
|
const readableOrderBy = ISSUE_ORDER_BY_OPTIONS.find((orderByObj) => orderByObj.key === orderBy)?.title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`${groupId}__${sub_group_id}`}
|
id={`${groupId}__${sub_group_id}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative h-full transition-all min-h-[50px]",
|
"relative h-full transition-all min-h-[50px]",
|
||||||
{ "bg-custom-background-80": isDraggingOverColumn },
|
{ "bg-custom-background-80 rounded": isDraggingOverColumn },
|
||||||
{ "vertical-scrollbar scrollbar-md": !sub_group_by }
|
{ "vertical-scrollbar scrollbar-md": !sub_group_by && !shouldOverlay }
|
||||||
)}
|
)}
|
||||||
ref={columnRef}
|
ref={columnRef}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
//column overlay when issues are not sorted by manual
|
||||||
|
className={cn(
|
||||||
|
"absolute top-0 left-0 h-full w-full items-center text-sm font-medium text-custom-text-300 rounded",
|
||||||
|
{
|
||||||
|
"flex flex-col bg-custom-background-80 border-[1px] border-custom-border-300 z-[2]": shouldOverlay,
|
||||||
|
},
|
||||||
|
{ hidden: !shouldOverlay },
|
||||||
|
{ "justify-center": !sub_group_by }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{readableOrderBy && <span className="pt-6">The layout is ordered by {readableOrderBy}.</span>}
|
||||||
|
<span>Drop here to move the issue.</span>
|
||||||
|
</div>
|
||||||
<KanbanIssueBlocksList
|
<KanbanIssueBlocksList
|
||||||
sub_group_id={sub_group_id}
|
sub_group_id={sub_group_id}
|
||||||
columnId={groupId}
|
columnId={groupId}
|
||||||
@ -181,7 +209,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
updateIssue={updateIssue}
|
updateIssue={updateIssue}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={sub_group_by ? scrollableContainerRef : columnRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
TUnGroupedIssues,
|
TUnGroupedIssues,
|
||||||
TIssueKanbanFilters,
|
TIssueKanbanFilters,
|
||||||
TIssueGroupByOptions,
|
TIssueGroupByOptions,
|
||||||
|
TIssueOrderByOptions,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||||
@ -114,6 +115,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
|||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
storeType: KanbanStoreType;
|
storeType: KanbanStoreType;
|
||||||
enableQuickIssueCreate: boolean;
|
enableQuickIssueCreate: boolean;
|
||||||
|
orderBy: TIssueOrderByOptions | undefined;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
quickAddCallback?: (
|
quickAddCallback?: (
|
||||||
@ -146,6 +148,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
viewId,
|
viewId,
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
handleOnDrop,
|
handleOnDrop,
|
||||||
|
orderBy,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const calculateIssueCount = (column_id: string) => {
|
const calculateIssueCount = (column_id: string) => {
|
||||||
@ -181,7 +184,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
|
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
|
||||||
return (
|
return (
|
||||||
<div key={_list.id} className="flex flex-shrink-0 flex-col">
|
<div key={_list.id} className="flex flex-shrink-0 flex-col">
|
||||||
<div className="sticky top-[50px] z-[1] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
|
<div className="sticky top-[50px] z-[3] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
|
||||||
<div className="sticky left-0 flex-shrink-0">
|
<div className="sticky left-0 flex-shrink-0">
|
||||||
<HeaderSubGroupByCard
|
<HeaderSubGroupByCard
|
||||||
column_id={_list.id}
|
column_id={_list.id}
|
||||||
@ -216,6 +219,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
handleOnDrop={handleOnDrop}
|
handleOnDrop={handleOnDrop}
|
||||||
|
orderBy={orderBy}
|
||||||
subGroupIssueHeaderCount={(groupByListId: string) =>
|
subGroupIssueHeaderCount={(groupByListId: string) =>
|
||||||
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
|
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
|
||||||
}
|
}
|
||||||
@ -254,6 +258,7 @@ export interface IKanBanSwimLanes {
|
|||||||
viewId?: string;
|
viewId?: string;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
orderBy: TIssueOrderByOptions | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||||
@ -263,6 +268,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
displayProperties,
|
displayProperties,
|
||||||
sub_group_by,
|
sub_group_by,
|
||||||
group_by,
|
group_by,
|
||||||
|
orderBy,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
storeType,
|
storeType,
|
||||||
quickActions,
|
quickActions,
|
||||||
@ -313,7 +319,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="sticky top-0 z-[2] h-[50px] bg-custom-background-90 px-2">
|
<div className="sticky top-0 z-[4] h-[50px] bg-custom-background-90 px-2">
|
||||||
<SubGroupSwimlaneHeader
|
<SubGroupSwimlaneHeader
|
||||||
issueIds={issueIds}
|
issueIds={issueIds}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
@ -334,6 +340,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
|
orderBy={orderBy}
|
||||||
updateIssue={updateIssue}
|
updateIssue={updateIssue}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
kanbanFilters={kanbanFilters}
|
kanbanFilters={kanbanFilters}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import pull from "lodash/pull";
|
import pull from "lodash/pull";
|
||||||
|
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||||
import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
|
import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
|
||||||
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
|
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
|
||||||
|
|
||||||
@ -87,14 +88,17 @@ export const getDestinationFromDropPayload = (payload: IPragmaticDropPayload): K
|
|||||||
const handleSortOrder = (
|
const handleSortOrder = (
|
||||||
destinationIssues: string[],
|
destinationIssues: string[],
|
||||||
destinationIssueId: string | undefined,
|
destinationIssueId: string | undefined,
|
||||||
getIssueById: (issueId: string) => TIssue | undefined
|
getIssueById: (issueId: string) => TIssue | undefined,
|
||||||
|
shouldAddIssueAtTop = false
|
||||||
) => {
|
) => {
|
||||||
const sortOrderDefaultValue = 65535;
|
const sortOrderDefaultValue = 65535;
|
||||||
let currentIssueState = {};
|
let currentIssueState = {};
|
||||||
|
|
||||||
const destinationIndex = destinationIssueId
|
const destinationIndex = destinationIssueId
|
||||||
? destinationIssues.indexOf(destinationIssueId)
|
? destinationIssues.indexOf(destinationIssueId)
|
||||||
: destinationIssues.length;
|
: shouldAddIssueAtTop
|
||||||
|
? 0
|
||||||
|
: destinationIssues.length;
|
||||||
|
|
||||||
if (destinationIssues && destinationIssues.length > 0) {
|
if (destinationIssues && destinationIssues.length > 0) {
|
||||||
if (destinationIndex === 0) {
|
if (destinationIndex === 0) {
|
||||||
@ -145,7 +149,8 @@ export const handleDragDrop = async (
|
|||||||
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined,
|
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined,
|
||||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined,
|
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined,
|
||||||
groupBy: TIssueGroupByOptions | undefined,
|
groupBy: TIssueGroupByOptions | undefined,
|
||||||
subGroupBy: TIssueGroupByOptions | undefined
|
subGroupBy: TIssueGroupByOptions | undefined,
|
||||||
|
shouldAddIssueAtTop = false
|
||||||
) => {
|
) => {
|
||||||
if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
|
if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
|
||||||
|
|
||||||
@ -165,7 +170,7 @@ export const handleDragDrop = async (
|
|||||||
// for both horizontal and vertical dnd
|
// for both horizontal and vertical dnd
|
||||||
updatedIssue = {
|
updatedIssue = {
|
||||||
...updatedIssue,
|
...updatedIssue,
|
||||||
...handleSortOrder(destinationIssues, destination.id, getIssueById),
|
...handleSortOrder(destinationIssues, destination.id, getIssueById, shouldAddIssueAtTop),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
|
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
|
||||||
@ -207,3 +212,18 @@ export const handleDragDrop = async (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Method finds the DOM element with elementId, scrolls to it and highlights the issue block
|
||||||
|
* @param elementId
|
||||||
|
* @param shouldScrollIntoView
|
||||||
|
*/
|
||||||
|
export const highlightIssueOnDrop = (elementId: string | undefined, shouldScrollIntoView = true) => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
const sourceElementId = elementId ?? "";
|
||||||
|
const sourceElement = document.getElementById(sourceElementId);
|
||||||
|
sourceElement?.classList?.add("highlight");
|
||||||
|
if (shouldScrollIntoView && sourceElement)
|
||||||
|
await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 });
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
@ -161,6 +161,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
|
|||||||
portalElement={portalElement}
|
portalElement={portalElement}
|
||||||
placement={placements}
|
placement={placements}
|
||||||
menuItemsClassName="z-[14]"
|
menuItemsClassName="z-[14]"
|
||||||
|
maxHeight="lg"
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
>
|
>
|
||||||
{MENU_ITEMS.map((item) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
@ -180,6 +181,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
|
|||||||
},
|
},
|
||||||
item.className
|
item.className
|
||||||
)}
|
)}
|
||||||
|
disabled={item.disabled}
|
||||||
>
|
>
|
||||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||||
<div>
|
<div>
|
||||||
|
@ -123,6 +123,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((
|
|||||||
portalElement={portalElement}
|
portalElement={portalElement}
|
||||||
placement={placements}
|
placement={placements}
|
||||||
menuItemsClassName="z-[14]"
|
menuItemsClassName="z-[14]"
|
||||||
|
maxHeight="lg"
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
>
|
>
|
||||||
{MENU_ITEMS.map((item) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
@ -142,6 +143,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((
|
|||||||
},
|
},
|
||||||
item.className
|
item.className
|
||||||
)}
|
)}
|
||||||
|
disabled={item.disabled}
|
||||||
>
|
>
|
||||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||||
<div>
|
<div>
|
||||||
|
@ -181,6 +181,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||||||
customButton={customActionButton}
|
customButton={customActionButton}
|
||||||
portalElement={portalElement}
|
portalElement={portalElement}
|
||||||
menuItemsClassName="z-[14]"
|
menuItemsClassName="z-[14]"
|
||||||
|
maxHeight="lg"
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
>
|
>
|
||||||
{MENU_ITEMS.map((item) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
@ -200,6 +201,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||||||
},
|
},
|
||||||
item.className
|
item.className
|
||||||
)}
|
)}
|
||||||
|
disabled={item.disabled}
|
||||||
>
|
>
|
||||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||||
<div>
|
<div>
|
||||||
|
@ -107,6 +107,7 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||||||
portalElement={portalElement}
|
portalElement={portalElement}
|
||||||
placement={placements}
|
placement={placements}
|
||||||
menuItemsClassName="z-[14]"
|
menuItemsClassName="z-[14]"
|
||||||
|
maxHeight="lg"
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
>
|
>
|
||||||
{MENU_ITEMS.map((item) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
@ -126,6 +127,7 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||||||
},
|
},
|
||||||
item.className
|
item.className
|
||||||
)}
|
)}
|
||||||
|
disabled={item.disabled}
|
||||||
>
|
>
|
||||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||||
<div>
|
<div>
|
||||||
|
@ -178,6 +178,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
|
|||||||
customButton={customActionButton}
|
customButton={customActionButton}
|
||||||
portalElement={portalElement}
|
portalElement={portalElement}
|
||||||
menuItemsClassName="z-[14]"
|
menuItemsClassName="z-[14]"
|
||||||
|
maxHeight="lg"
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
>
|
>
|
||||||
{MENU_ITEMS.map((item) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
@ -197,6 +198,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
|
|||||||
},
|
},
|
||||||
item.className
|
item.className
|
||||||
)}
|
)}
|
||||||
|
disabled={item.disabled}
|
||||||
>
|
>
|
||||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||||
<div>
|
<div>
|
||||||
|
@ -171,6 +171,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
|||||||
customButton={customActionButton}
|
customButton={customActionButton}
|
||||||
portalElement={portalElement}
|
portalElement={portalElement}
|
||||||
menuItemsClassName="z-[14]"
|
menuItemsClassName="z-[14]"
|
||||||
|
maxHeight="lg"
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
>
|
>
|
||||||
{MENU_ITEMS.map((item) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
@ -190,6 +191,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
|||||||
},
|
},
|
||||||
item.className
|
item.className
|
||||||
)}
|
)}
|
||||||
|
disabled={item.disabled}
|
||||||
>
|
>
|
||||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||||
<div>
|
<div>
|
||||||
|
@ -475,6 +475,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
tabIndex={getTabIndex("description_html")}
|
tabIndex={getTabIndex("description_html")}
|
||||||
placeholder={getDescriptionPlaceholder}
|
placeholder={getDescriptionPlaceholder}
|
||||||
|
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -53,7 +53,7 @@ export type PeekOverviewHeaderProps = {
|
|||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
toggleDeleteIssueModal: (issueId: string | null) => void;
|
toggleDeleteIssueModal: (issueId: string | null) => void;
|
||||||
toggleArchiveIssueModal: (value: boolean) => void;
|
toggleArchiveIssueModal: (issueId: string | null) => void;
|
||||||
handleRestoreIssue: () => void;
|
handleRestoreIssue: () => void;
|
||||||
isSubmitting: "submitting" | "submitted" | "saved";
|
isSubmitting: "submitting" | "submitted" | "saved";
|
||||||
};
|
};
|
||||||
@ -177,7 +177,7 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
|||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isInArchivableGroup) return;
|
if (!isInArchivableGroup) return;
|
||||||
toggleArchiveIssueModal(true);
|
toggleArchiveIssueModal(issueId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArchiveIcon className="h-4 w-4" />
|
<ArchiveIcon className="h-4 w-4" />
|
||||||
|
@ -69,6 +69,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
|||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={issue.name}
|
value={issue.name}
|
||||||
|
containerClassName="-ml-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IssueDescriptionInput
|
<IssueDescriptionInput
|
||||||
@ -77,10 +78,11 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
|||||||
issueId={issue.id}
|
issueId={issue.id}
|
||||||
initialValue={issueDescription}
|
initialValue={issueDescription}
|
||||||
// for now peek overview doesn't have live syncing while tab changes
|
// for now peek overview doesn't have live syncing while tab changes
|
||||||
swrIssueDescription={null}
|
swrIssueDescription={issueDescription}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
|
containerClassName="-ml-3 !mb-6 border-none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
|
@ -10,11 +10,10 @@ import {
|
|||||||
XCircle,
|
XCircle,
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
CalendarCheck2,
|
CalendarCheck2,
|
||||||
UserCircle2,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
// ui icons
|
// ui icons
|
||||||
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon, Tooltip } from "@plane/ui";
|
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
DateDropdown,
|
DateDropdown,
|
||||||
@ -23,7 +22,6 @@ import {
|
|||||||
MemberDropdown,
|
MemberDropdown,
|
||||||
StateDropdown,
|
StateDropdown,
|
||||||
} from "@/components/dropdowns";
|
} from "@/components/dropdowns";
|
||||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
|
||||||
import {
|
import {
|
||||||
IssueLinkRoot,
|
IssueLinkRoot,
|
||||||
IssueCycleSelect,
|
IssueCycleSelect,
|
||||||
@ -37,8 +35,7 @@ import {
|
|||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||||
import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/store";
|
import { useIssueDetail, useProject, useProjectState } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
|
||||||
|
|
||||||
interface IPeekOverviewProperties {
|
interface IPeekOverviewProperties {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -56,12 +53,9 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { getStateById } = useProjectState();
|
const { getStateById } = useProjectState();
|
||||||
const { getUserDetails } = useMember();
|
|
||||||
const { isMobile } = usePlatformOS();
|
|
||||||
// derived values
|
// derived values
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
if (!issue) return <></>;
|
if (!issue) return <></>;
|
||||||
const createdByDetails = getUserDetails(issue?.created_by);
|
|
||||||
const projectDetails = getProjectById(issue.project_id);
|
const projectDetails = getProjectById(issue.project_id);
|
||||||
const isEstimateEnabled = projectDetails?.estimate;
|
const isEstimateEnabled = projectDetails?.estimate;
|
||||||
const stateDetails = getStateById(issue.state_id);
|
const stateDetails = getStateById(issue.state_id);
|
||||||
@ -137,22 +131,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* created by */}
|
|
||||||
{createdByDetails && (
|
|
||||||
<div className="flex w-full items-center gap-3 h-8">
|
|
||||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
|
||||||
<UserCircle2 className="h-4 w-4 flex-shrink-0" />
|
|
||||||
<span>Created by</span>
|
|
||||||
</div>
|
|
||||||
<Tooltip tooltipContent={createdByDetails?.display_name} isMobile={isMobile}>
|
|
||||||
<div className="h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-default">
|
|
||||||
<ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />
|
|
||||||
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* start date */}
|
{/* start date */}
|
||||||
<div className="flex w-full items-center gap-3 h-8">
|
<div className="flex w-full items-center gap-3 h-8">
|
||||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||||
|
@ -87,8 +87,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
<>
|
<>
|
||||||
{issue && !is_archived && (
|
{issue && !is_archived && (
|
||||||
<ArchiveIssueModal
|
<ArchiveIssueModal
|
||||||
isOpen={isArchiveIssueModalOpen}
|
isOpen={isArchiveIssueModalOpen === issueId}
|
||||||
handleClose={() => toggleArchiveIssueModal(false)}
|
handleClose={() => toggleArchiveIssueModal(null)}
|
||||||
data={issue}
|
data={issue}
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
if (issueOperations.archive) await issueOperations.archive(workspaceSlug, projectId, issueId);
|
if (issueOperations.archive) await issueOperations.archive(workspaceSlug, projectId, issueId);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user