mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of https://github.com/makeplane/plane into feat/csv_exporter
This commit is contained in:
commit
987806258a
@ -18,6 +18,8 @@ from .project import (
|
|||||||
ProjectFavoriteSerializer,
|
ProjectFavoriteSerializer,
|
||||||
ProjectLiteSerializer,
|
ProjectLiteSerializer,
|
||||||
ProjectMemberLiteSerializer,
|
ProjectMemberLiteSerializer,
|
||||||
|
ProjectDeployBoardSerializer,
|
||||||
|
ProjectMemberAdminSerializer,
|
||||||
)
|
)
|
||||||
from .state import StateSerializer, StateLiteSerializer
|
from .state import StateSerializer, StateLiteSerializer
|
||||||
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
|
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
|
||||||
@ -41,6 +43,7 @@ from .issue import (
|
|||||||
IssueSubscriberSerializer,
|
IssueSubscriberSerializer,
|
||||||
IssueReactionSerializer,
|
IssueReactionSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
|
IssueVoteSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .module import (
|
from .module import (
|
||||||
@ -78,3 +81,5 @@ from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSeriali
|
|||||||
from .analytic import AnalyticViewSerializer
|
from .analytic import AnalyticViewSerializer
|
||||||
|
|
||||||
from .notification import NotificationSerializer
|
from .notification import NotificationSerializer
|
||||||
|
|
||||||
|
from .exporter import ExporterHistorySerializer
|
||||||
|
@ -41,6 +41,7 @@ class CycleSerializer(BaseSerializer):
|
|||||||
{
|
{
|
||||||
"avatar": assignee.avatar,
|
"avatar": assignee.avatar,
|
||||||
"first_name": assignee.first_name,
|
"first_name": assignee.first_name,
|
||||||
|
"display_name": assignee.display_name,
|
||||||
"id": assignee.id,
|
"id": assignee.id,
|
||||||
}
|
}
|
||||||
for issue_cycle in obj.issue_cycle.all()
|
for issue_cycle in obj.issue_cycle.all()
|
||||||
|
26
apiserver/plane/api/serializers/exporter.py
Normal file
26
apiserver/plane/api/serializers/exporter.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Module imports
|
||||||
|
from .base import BaseSerializer
|
||||||
|
from plane.db.models import ExporterHistory
|
||||||
|
from .user import UserLiteSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ExporterHistorySerializer(BaseSerializer):
|
||||||
|
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ExporterHistory
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"project",
|
||||||
|
"provider",
|
||||||
|
"status",
|
||||||
|
"url",
|
||||||
|
"initiated_by",
|
||||||
|
"initiated_by_detail",
|
||||||
|
"token",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
]
|
||||||
|
read_only_fields = fields
|
@ -31,6 +31,7 @@ from plane.db.models import (
|
|||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
CommentReaction,
|
CommentReaction,
|
||||||
|
IssueVote,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -111,6 +112,11 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
|
||||||
|
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||||
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
blockers = validated_data.pop("blockers_list", None)
|
blockers = validated_data.pop("blockers_list", None)
|
||||||
assignees = validated_data.pop("assignees_list", None)
|
assignees = validated_data.pop("assignees_list", None)
|
||||||
@ -549,6 +555,14 @@ class CommentReactionSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class IssueVoteSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = IssueVote
|
||||||
|
fields = ["issue", "vote", "workspace_id", "project_id", "actor"]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
class IssueCommentSerializer(BaseSerializer):
|
class IssueCommentSerializer(BaseSerializer):
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||||
@ -568,6 +582,7 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
"updated_by",
|
"updated_by",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
"access",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ from plane.db.models import (
|
|||||||
ProjectMemberInvite,
|
ProjectMemberInvite,
|
||||||
ProjectIdentifier,
|
ProjectIdentifier,
|
||||||
ProjectFavorite,
|
ProjectFavorite,
|
||||||
|
ProjectDeployBoard,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -80,7 +81,14 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
class ProjectLiteSerializer(BaseSerializer):
|
class ProjectLiteSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
fields = ["id", "identifier", "name"]
|
fields = [
|
||||||
|
"id",
|
||||||
|
"identifier",
|
||||||
|
"name",
|
||||||
|
"cover_image",
|
||||||
|
"icon_prop",
|
||||||
|
"emoji",
|
||||||
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
@ -94,6 +102,7 @@ class ProjectDetailSerializer(BaseSerializer):
|
|||||||
total_modules = serializers.IntegerField(read_only=True)
|
total_modules = serializers.IntegerField(read_only=True)
|
||||||
is_member = serializers.BooleanField(read_only=True)
|
is_member = serializers.BooleanField(read_only=True)
|
||||||
sort_order = serializers.FloatField(read_only=True)
|
sort_order = serializers.FloatField(read_only=True)
|
||||||
|
member_role = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
@ -115,7 +124,6 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
|||||||
project = ProjectLiteSerializer(read_only=True)
|
project = ProjectLiteSerializer(read_only=True)
|
||||||
member = UserAdminLiteSerializer(read_only=True)
|
member = UserAdminLiteSerializer(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectMember
|
model = ProjectMember
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@ -148,8 +156,6 @@ class ProjectFavoriteSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberLiteSerializer(BaseSerializer):
|
class ProjectMemberLiteSerializer(BaseSerializer):
|
||||||
member = UserLiteSerializer(read_only=True)
|
member = UserLiteSerializer(read_only=True)
|
||||||
is_subscribed = serializers.BooleanField(read_only=True)
|
is_subscribed = serializers.BooleanField(read_only=True)
|
||||||
@ -158,3 +164,16 @@ class ProjectMemberLiteSerializer(BaseSerializer):
|
|||||||
model = ProjectMember
|
model = ProjectMember
|
||||||
fields = ["member", "id", "is_subscribed"]
|
fields = ["member", "id", "is_subscribed"]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectDeployBoardSerializer(BaseSerializer):
|
||||||
|
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
|
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ProjectDeployBoard
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project" "anchor",
|
||||||
|
]
|
||||||
|
@ -86,6 +86,7 @@ from plane.api.views import (
|
|||||||
IssueAttachmentEndpoint,
|
IssueAttachmentEndpoint,
|
||||||
IssueArchiveViewSet,
|
IssueArchiveViewSet,
|
||||||
IssueSubscriberViewSet,
|
IssueSubscriberViewSet,
|
||||||
|
IssueCommentPublicViewSet,
|
||||||
IssueReactionViewSet,
|
IssueReactionViewSet,
|
||||||
CommentReactionViewSet,
|
CommentReactionViewSet,
|
||||||
ExportIssuesEndpoint,
|
ExportIssuesEndpoint,
|
||||||
@ -165,6 +166,15 @@ from plane.api.views import (
|
|||||||
NotificationViewSet,
|
NotificationViewSet,
|
||||||
UnreadNotificationEndpoint,
|
UnreadNotificationEndpoint,
|
||||||
## End Notification
|
## End Notification
|
||||||
|
# Public Boards
|
||||||
|
ProjectDeployBoardViewSet,
|
||||||
|
ProjectDeployBoardIssuesPublicEndpoint,
|
||||||
|
ProjectDeployBoardPublicSettingsEndpoint,
|
||||||
|
IssueReactionPublicViewSet,
|
||||||
|
CommentReactionPublicViewSet,
|
||||||
|
InboxIssuePublicViewSet,
|
||||||
|
IssueVotePublicViewSet,
|
||||||
|
## End Public Boards
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1481,4 +1491,128 @@ urlpatterns = [
|
|||||||
name="unread-notifications",
|
name="unread-notifications",
|
||||||
),
|
),
|
||||||
## End Notification
|
## End Notification
|
||||||
|
# Public Boards
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
|
||||||
|
ProjectDeployBoardViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-deploy-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
|
||||||
|
ProjectDeployBoardViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-deploy-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/settings/",
|
||||||
|
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
|
||||||
|
name="project-deploy-board-settings",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
|
||||||
|
ProjectDeployBoardIssuesPublicEndpoint.as_view(),
|
||||||
|
name="project-deploy-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
|
||||||
|
IssueCommentPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-comments-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
|
||||||
|
IssueCommentPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-comments-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
|
||||||
|
IssueReactionPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-reactions-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
|
||||||
|
IssueReactionPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-reactions-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
|
||||||
|
CommentReactionPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="comment-reactions-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
|
||||||
|
CommentReactionPublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="comment-reactions-project-board",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
|
||||||
|
InboxIssuePublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="inbox-issue",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||||
|
InboxIssuePublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="inbox-issue",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/votes/",
|
||||||
|
IssueVotePublicViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-vote-project-board",
|
||||||
|
),
|
||||||
|
## End Public Boards
|
||||||
]
|
]
|
||||||
|
@ -12,6 +12,9 @@ from .project import (
|
|||||||
ProjectUserViewsEndpoint,
|
ProjectUserViewsEndpoint,
|
||||||
ProjectMemberUserEndpoint,
|
ProjectMemberUserEndpoint,
|
||||||
ProjectFavoritesViewSet,
|
ProjectFavoritesViewSet,
|
||||||
|
ProjectDeployBoardIssuesPublicEndpoint,
|
||||||
|
ProjectDeployBoardViewSet,
|
||||||
|
ProjectDeployBoardPublicSettingsEndpoint,
|
||||||
ProjectMemberEndpoint,
|
ProjectMemberEndpoint,
|
||||||
)
|
)
|
||||||
from .user import (
|
from .user import (
|
||||||
@ -75,9 +78,12 @@ from .issue import (
|
|||||||
IssueAttachmentEndpoint,
|
IssueAttachmentEndpoint,
|
||||||
IssueArchiveViewSet,
|
IssueArchiveViewSet,
|
||||||
IssueSubscriberViewSet,
|
IssueSubscriberViewSet,
|
||||||
|
IssueCommentPublicViewSet,
|
||||||
CommentReactionViewSet,
|
CommentReactionViewSet,
|
||||||
IssueReactionViewSet,
|
IssueReactionViewSet,
|
||||||
ExportIssuesEndpoint
|
IssueReactionPublicViewSet,
|
||||||
|
CommentReactionPublicViewSet,
|
||||||
|
IssueVotePublicViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .auth_extended import (
|
from .auth_extended import (
|
||||||
@ -145,7 +151,7 @@ from .estimate import (
|
|||||||
|
|
||||||
from .release import ReleaseNotesEndpoint
|
from .release import ReleaseNotesEndpoint
|
||||||
|
|
||||||
from .inbox import InboxViewSet, InboxIssueViewSet
|
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
|
||||||
|
|
||||||
from .analytic import (
|
from .analytic import (
|
||||||
AnalyticsEndpoint,
|
AnalyticsEndpoint,
|
||||||
@ -155,4 +161,8 @@ from .analytic import (
|
|||||||
DefaultAnalyticsEndpoint,
|
DefaultAnalyticsEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint
|
from .notification import NotificationViewSet, UnreadNotificationEndpoint
|
||||||
|
|
||||||
|
from .exporter import (
|
||||||
|
ExportIssuesEndpoint,
|
||||||
|
)
|
@ -243,21 +243,21 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
most_issue_created_user = (
|
most_issue_created_user = (
|
||||||
queryset.exclude(created_by=None)
|
queryset.exclude(created_by=None)
|
||||||
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name")
|
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name", "created_by__id")
|
||||||
.annotate(count=Count("id"))
|
.annotate(count=Count("id"))
|
||||||
.order_by("-count")
|
.order_by("-count")
|
||||||
)[:5]
|
)[:5]
|
||||||
|
|
||||||
most_issue_closed_user = (
|
most_issue_closed_user = (
|
||||||
queryset.filter(completed_at__isnull=False, assignees__isnull=False)
|
queryset.filter(completed_at__isnull=False, assignees__isnull=False)
|
||||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name")
|
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id")
|
||||||
.annotate(count=Count("id"))
|
.annotate(count=Count("id"))
|
||||||
.order_by("-count")
|
.order_by("-count")
|
||||||
)[:5]
|
)[:5]
|
||||||
|
|
||||||
pending_issue_user = (
|
pending_issue_user = (
|
||||||
queryset.filter(completed_at__isnull=True)
|
queryset.filter(completed_at__isnull=True)
|
||||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name")
|
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id")
|
||||||
.annotate(count=Count("id"))
|
.annotate(count=Count("id"))
|
||||||
.order_by("-count")
|
.order_by("-count")
|
||||||
)
|
)
|
||||||
|
@ -165,6 +165,9 @@ class CycleViewSet(BaseViewSet):
|
|||||||
try:
|
try:
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
cycle_view = request.GET.get("cycle_view", "all")
|
cycle_view = request.GET.get("cycle_view", "all")
|
||||||
|
order_by = request.GET.get("order_by", "sort_order")
|
||||||
|
|
||||||
|
queryset = queryset.order_by(order_by)
|
||||||
|
|
||||||
# All Cycles
|
# All Cycles
|
||||||
if cycle_view == "all":
|
if cycle_view == "all":
|
||||||
@ -370,7 +373,8 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.annotate(last_name=F("assignees__last_name"))
|
.annotate(last_name=F("assignees__last_name"))
|
||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.values("first_name", "last_name", "assignee_id", "avatar")
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
|
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
||||||
.annotate(total_issues=Count("assignee_id"))
|
.annotate(total_issues=Count("assignee_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
|
99
apiserver/plane/api/views/exporter.py
Normal file
99
apiserver/plane/api/views/exporter.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from . import BaseAPIView
|
||||||
|
from plane.api.permissions import WorkSpaceAdminPermission
|
||||||
|
from plane.bgtasks.export_task import issue_export_task
|
||||||
|
from plane.db.models import Project, ExporterHistory, Workspace
|
||||||
|
|
||||||
|
from plane.api.serializers import ExporterHistorySerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ExportIssuesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkSpaceAdminPermission,
|
||||||
|
]
|
||||||
|
model = ExporterHistory
|
||||||
|
serializer_class = ExporterHistorySerializer
|
||||||
|
|
||||||
|
def post(self, request, slug):
|
||||||
|
try:
|
||||||
|
# Get the workspace
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
provider = request.data.get("provider", False)
|
||||||
|
multiple = request.data.get("multiple", False)
|
||||||
|
project_ids = request.data.get("project", [])
|
||||||
|
|
||||||
|
if provider in ["csv", "xlsx", "json"]:
|
||||||
|
if not project_ids:
|
||||||
|
project_ids = Project.objects.filter(
|
||||||
|
workspace__slug=slug
|
||||||
|
).values_list("id", flat=True)
|
||||||
|
project_ids = [str(project_id) for project_id in project_ids]
|
||||||
|
|
||||||
|
exporter = ExporterHistory.objects.create(
|
||||||
|
workspace=workspace,
|
||||||
|
project=project_ids,
|
||||||
|
initiated_by=request.user,
|
||||||
|
provider=provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_export_task.delay(
|
||||||
|
provider=exporter.provider,
|
||||||
|
workspace_id=workspace.id,
|
||||||
|
project_ids=project_ids,
|
||||||
|
token_id=exporter.token,
|
||||||
|
multiple=multiple,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"message": f"Once the export is ready you will be able to download it"
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{"error": f"Provider '{provider}' not found."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Workspace.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Workspace does not exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
try:
|
||||||
|
exporter_history = ExporterHistory.objects.filter(
|
||||||
|
workspace__slug=slug
|
||||||
|
).select_related("workspace","initiated_by")
|
||||||
|
|
||||||
|
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
||||||
|
return self.paginate(
|
||||||
|
request=request,
|
||||||
|
queryset=exporter_history,
|
||||||
|
on_results=lambda exporter_history: ExporterHistorySerializer(
|
||||||
|
exporter_history, many=True
|
||||||
|
).data,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{"error": "per_page and cursor are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
@ -15,7 +15,6 @@ from sentry_sdk import capture_exception
|
|||||||
from .base import BaseViewSet
|
from .base import BaseViewSet
|
||||||
from plane.api.permissions import ProjectBasePermission, ProjectLitePermission
|
from plane.api.permissions import ProjectBasePermission, ProjectLitePermission
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Project,
|
|
||||||
Inbox,
|
Inbox,
|
||||||
InboxIssue,
|
InboxIssue,
|
||||||
Issue,
|
Issue,
|
||||||
@ -23,6 +22,7 @@ from plane.db.models import (
|
|||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
|
ProjectDeployBoard,
|
||||||
)
|
)
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
@ -377,4 +377,269 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Something went wrong please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InboxIssuePublicViewSet(BaseViewSet):
|
||||||
|
serializer_class = InboxIssueSerializer
|
||||||
|
model = InboxIssue
|
||||||
|
|
||||||
|
filterset_fields = [
|
||||||
|
"status",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"))
|
||||||
|
if project_deploy_board is not None:
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(
|
||||||
|
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
inbox_id=self.kwargs.get("inbox_id"),
|
||||||
|
)
|
||||||
|
.select_related("issue", "workspace", "project")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return InboxIssue.objects.none()
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id, inbox_id):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||||
|
if project_deploy_board.inbox is None:
|
||||||
|
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
issues = (
|
||||||
|
Issue.objects.filter(
|
||||||
|
issue_inbox__inbox_id=inbox_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.annotate(bridge_id=F("issue_inbox__id"))
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels")
|
||||||
|
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_inbox",
|
||||||
|
queryset=InboxIssue.objects.only(
|
||||||
|
"status", "duplicate_to", "snoozed_till", "source"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
issues_data = IssueStateInboxSerializer(issues, many=True).data
|
||||||
|
return Response(
|
||||||
|
issues_data,
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
except ProjectDeployBoard.DoesNotExist:
|
||||||
|
return Response({"error": "Project Deploy Board does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, inbox_id):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||||
|
if project_deploy_board.inbox is None:
|
||||||
|
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
|
return Response(
|
||||||
|
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for valid priority
|
||||||
|
if not request.data.get("issue", {}).get("priority", None) in [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high",
|
||||||
|
"urgent",
|
||||||
|
None,
|
||||||
|
]:
|
||||||
|
return Response(
|
||||||
|
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create or get state
|
||||||
|
state, _ = State.objects.get_or_create(
|
||||||
|
name="Triage",
|
||||||
|
group="backlog",
|
||||||
|
description="Default state for managing all Inbox Issues",
|
||||||
|
project_id=project_id,
|
||||||
|
color="#ff7700",
|
||||||
|
)
|
||||||
|
|
||||||
|
# create an issue
|
||||||
|
issue = Issue.objects.create(
|
||||||
|
name=request.data.get("issue", {}).get("name"),
|
||||||
|
description=request.data.get("issue", {}).get("description", {}),
|
||||||
|
description_html=request.data.get("issue", {}).get(
|
||||||
|
"description_html", "<p></p>"
|
||||||
|
),
|
||||||
|
priority=request.data.get("issue", {}).get("priority", "low"),
|
||||||
|
project_id=project_id,
|
||||||
|
state=state,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an Issue Activity
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.created",
|
||||||
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue.id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
)
|
||||||
|
# create an inbox issue
|
||||||
|
InboxIssue.objects.create(
|
||||||
|
inbox_id=inbox_id,
|
||||||
|
project_id=project_id,
|
||||||
|
issue=issue,
|
||||||
|
source=request.data.get("source", "in-app"),
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = IssueStateInboxSerializer(issue)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||||
|
if project_deploy_board.inbox is None:
|
||||||
|
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
inbox_issue = InboxIssue.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
|
)
|
||||||
|
# Get the project member
|
||||||
|
if str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
|
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Get issue data
|
||||||
|
issue_data = request.data.pop("issue", False)
|
||||||
|
|
||||||
|
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
# viewers and guests since only viewers and guests
|
||||||
|
issue_data = {
|
||||||
|
"name": issue_data.get("name", issue.name),
|
||||||
|
"description_html": issue_data.get("description_html", issue.description_html),
|
||||||
|
"description": issue_data.get("description", issue.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
issue_serializer = IssueCreateSerializer(
|
||||||
|
issue, data=issue_data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if issue_serializer.is_valid():
|
||||||
|
current_instance = issue
|
||||||
|
# Log all the updates
|
||||||
|
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
||||||
|
if issue is not None:
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.updated",
|
||||||
|
requested_data=requested_data,
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue.id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueSerializer(current_instance).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
issue_serializer.save()
|
||||||
|
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except InboxIssue.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Inbox Issue does not exist"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||||
|
if project_deploy_board.inbox is None:
|
||||||
|
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
inbox_issue = InboxIssue.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
|
)
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
serializer = IssueStateInboxSerializer(issue)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, inbox_id, pk):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||||
|
if project_deploy_board.inbox is None:
|
||||||
|
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
inbox_issue = InboxIssue.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
|
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
inbox_issue.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
except InboxIssue.DoesNotExist:
|
||||||
|
return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ from plane.api.serializers import (
|
|||||||
ProjectMemberLiteSerializer,
|
ProjectMemberLiteSerializer,
|
||||||
IssueReactionSerializer,
|
IssueReactionSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
|
IssueVoteSerializer,
|
||||||
)
|
)
|
||||||
from plane.api.permissions import (
|
from plane.api.permissions import (
|
||||||
WorkspaceEntityPermission,
|
WorkspaceEntityPermission,
|
||||||
@ -70,11 +71,12 @@ from plane.db.models import (
|
|||||||
ProjectMember,
|
ProjectMember,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
CommentReaction,
|
CommentReaction,
|
||||||
|
ProjectDeployBoard,
|
||||||
|
IssueVote,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
from plane.utils.grouper import group_results
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.bgtasks.project_issue_export import issue_export_task
|
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSet(BaseViewSet):
|
class IssueViewSet(BaseViewSet):
|
||||||
@ -169,7 +171,6 @@ class IssueViewSet(BaseViewSet):
|
|||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
print(filters)
|
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", None]
|
priority_order = ["urgent", "high", "medium", "low", None]
|
||||||
@ -362,6 +363,12 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
|
)
|
||||||
|
)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -744,21 +751,25 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
state_distribution = (
|
state_distribution = (
|
||||||
State.objects.filter(~Q(name="Triage"), workspace__slug=slug)
|
State.objects.filter(
|
||||||
.annotate(
|
workspace__slug=slug, state_issue__parent_id=issue_id
|
||||||
state_count=Count(
|
|
||||||
"state_issue",
|
|
||||||
filter=Q(state_issue__parent_id=issue_id),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.order_by("group")
|
.annotate(state_group=F("group"))
|
||||||
.values("group", "state_count")
|
.values("state_group")
|
||||||
|
.annotate(state_count=Count("state_group"))
|
||||||
|
.order_by("state_group")
|
||||||
)
|
)
|
||||||
|
|
||||||
result = {item["group"]: item["state_count"] for item in state_distribution}
|
result = {item["state_group"]: item["state_count"] for item in state_distribution}
|
||||||
|
|
||||||
serializer = IssueLiteSerializer(
|
serializer = IssueLiteSerializer(
|
||||||
sub_issues,
|
sub_issues,
|
||||||
@ -1448,6 +1459,374 @@ class CommentReactionViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueCommentPublicViewSet(BaseViewSet):
|
||||||
|
serializer_class = IssueCommentSerializer
|
||||||
|
model = IssueComment
|
||||||
|
|
||||||
|
filterset_fields = [
|
||||||
|
"issue__id",
|
||||||
|
"workspace__id",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
)
|
||||||
|
if project_deploy_board.comments:
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("issue")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return IssueComment.objects.none()
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, issue_id):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not project_deploy_board.comments:
|
||||||
|
return Response(
|
||||||
|
{"error": "Comments are not enabled for this project"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
access = (
|
||||||
|
"INTERNAL"
|
||||||
|
if ProjectMember.objects.filter(
|
||||||
|
project_id=project_id, member=request.user
|
||||||
|
).exists()
|
||||||
|
else "EXTERNAL"
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = IssueCommentSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
actor=request.user,
|
||||||
|
access=access,
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="comment.activity.created",
|
||||||
|
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue_id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, project_id, issue_id, pk):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not project_deploy_board.comments:
|
||||||
|
return Response(
|
||||||
|
{"error": "Comments are not enabled for this project"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
comment = IssueComment.objects.get(
|
||||||
|
workspace__slug=slug, pk=pk, actor=request.user
|
||||||
|
)
|
||||||
|
serializer = IssueCommentSerializer(
|
||||||
|
comment, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="comment.activity.updated",
|
||||||
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue_id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueCommentSerializer(comment).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist):
|
||||||
|
return Response(
|
||||||
|
{"error": "IssueComent Does not exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, issue_id, pk):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not project_deploy_board.comments:
|
||||||
|
return Response(
|
||||||
|
{"error": "Comments are not enabled for this project"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
comment = IssueComment.objects.get(
|
||||||
|
workspace__slug=slug, pk=pk, project_id=project_id, actor=request.user
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="comment.activity.deleted",
|
||||||
|
requested_data=json.dumps({"comment_id": str(pk)}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue_id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueCommentSerializer(comment).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
comment.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist):
|
||||||
|
return Response(
|
||||||
|
{"error": "IssueComent Does not exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueReactionPublicViewSet(BaseViewSet):
|
||||||
|
serializer_class = IssueReactionSerializer
|
||||||
|
model = IssueReaction
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
)
|
||||||
|
if project_deploy_board.reactions:
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||||
|
.order_by("-created_at")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return IssueReaction.objects.none()
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, issue_id):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not project_deploy_board.reactions:
|
||||||
|
return Response(
|
||||||
|
{"error": "Reactions are not enabled for this project board"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = IssueReactionSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(
|
||||||
|
project_id=project_id, issue_id=issue_id, actor=request.user
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except ProjectDeployBoard.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Project board does not exist"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not project_deploy_board.reactions:
|
||||||
|
return Response(
|
||||||
|
{"error": "Reactions are not enabled for this project board"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
issue_reaction = IssueReaction.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
issue_id=issue_id,
|
||||||
|
reaction=reaction_code,
|
||||||
|
actor=request.user,
|
||||||
|
)
|
||||||
|
issue_reaction.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
except IssueReaction.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issue reaction does not exist"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CommentReactionPublicViewSet(BaseViewSet):
|
||||||
|
serializer_class = CommentReactionSerializer
|
||||||
|
model = CommentReaction
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
)
|
||||||
|
if project_deploy_board.reactions:
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(comment_id=self.kwargs.get("comment_id"))
|
||||||
|
.order_by("-created_at")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return CommentReaction.objects.none()
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, comment_id):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not project_deploy_board.reactions:
|
||||||
|
return Response(
|
||||||
|
{"error": "Reactions are not enabled for this board"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = CommentReactionSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(
|
||||||
|
project_id=project_id, comment_id=comment_id, actor=request.user
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except ProjectDeployBoard.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Project board does not exist"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
if not project_deploy_board.reactions:
|
||||||
|
return Response(
|
||||||
|
{"error": "Reactions are not enabled for this board"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
comment_reaction = CommentReaction.objects.get(
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
comment_id=comment_id,
|
||||||
|
reaction=reaction_code,
|
||||||
|
actor=request.user,
|
||||||
|
)
|
||||||
|
comment_reaction.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
except CommentReaction.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Comment reaction does not exist"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueVotePublicViewSet(BaseViewSet):
|
||||||
|
model = IssueVote
|
||||||
|
serializer_class = IssueVoteSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, issue_id):
|
||||||
|
try:
|
||||||
|
issue_vote, _ = IssueVote.objects.get_or_create(
|
||||||
|
actor_id=request.user.id,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
vote=request.data.get("vote", 1),
|
||||||
|
)
|
||||||
|
serializer = IssueVoteSerializer(issue_vote)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, issue_id):
|
||||||
|
try:
|
||||||
|
issue_vote = IssueVote.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
)
|
||||||
|
issue_vote.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExportIssuesEndpoint(BaseAPIView):
|
class ExportIssuesEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
|
@ -53,6 +53,8 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
order_by = self.request.GET.get("order_by", "sort_order")
|
||||||
|
|
||||||
subquery = ModuleFavorite.objects.filter(
|
subquery = ModuleFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
module_id=OuterRef("pk"),
|
module_id=OuterRef("pk"),
|
||||||
@ -106,7 +108,7 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
filter=Q(issue_module__issue__state__group="backlog"),
|
filter=Q(issue_module__issue__state__group="backlog"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("-is_favorite", "name")
|
.order_by(order_by, "name")
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
@ -173,8 +175,9 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
.annotate(first_name=F("assignees__first_name"))
|
.annotate(first_name=F("assignees__first_name"))
|
||||||
.annotate(last_name=F("assignees__last_name"))
|
.annotate(last_name=F("assignees__last_name"))
|
||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.values("first_name", "last_name", "assignee_id", "avatar")
|
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
||||||
.annotate(total_issues=Count("assignee_id"))
|
.annotate(total_issues=Count("assignee_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
|
@ -5,7 +5,21 @@ from datetime import datetime
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Q, Exists, OuterRef, Func, F, Min, Subquery
|
from django.db.models import (
|
||||||
|
Q,
|
||||||
|
Exists,
|
||||||
|
OuterRef,
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Max,
|
||||||
|
CharField,
|
||||||
|
Func,
|
||||||
|
Subquery,
|
||||||
|
Prefetch,
|
||||||
|
When,
|
||||||
|
Case,
|
||||||
|
Value,
|
||||||
|
)
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@ -13,6 +27,7 @@ from django.conf import settings
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
@ -23,9 +38,16 @@ from plane.api.serializers import (
|
|||||||
ProjectDetailSerializer,
|
ProjectDetailSerializer,
|
||||||
ProjectMemberInviteSerializer,
|
ProjectMemberInviteSerializer,
|
||||||
ProjectFavoriteSerializer,
|
ProjectFavoriteSerializer,
|
||||||
|
IssueLiteSerializer,
|
||||||
|
ProjectDeployBoardSerializer,
|
||||||
|
ProjectMemberAdminSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
|
from plane.api.permissions import (
|
||||||
|
ProjectBasePermission,
|
||||||
|
ProjectEntityPermission,
|
||||||
|
ProjectMemberPermission,
|
||||||
|
)
|
||||||
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Project,
|
Project,
|
||||||
@ -48,9 +70,17 @@ from plane.db.models import (
|
|||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
ModuleMember,
|
ModuleMember,
|
||||||
Inbox,
|
Inbox,
|
||||||
|
ProjectDeployBoard,
|
||||||
|
Issue,
|
||||||
|
IssueReaction,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
|
Label,
|
||||||
)
|
)
|
||||||
|
|
||||||
from plane.bgtasks.project_invitation_task import project_invitation
|
from plane.bgtasks.project_invitation_task import project_invitation
|
||||||
|
from plane.utils.grouper import group_results
|
||||||
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
class ProjectViewSet(BaseViewSet):
|
class ProjectViewSet(BaseViewSet):
|
||||||
@ -109,6 +139,12 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
member_role=ProjectMember.objects.filter(
|
||||||
|
project_id=OuterRef("pk"),
|
||||||
|
member_id=self.request.user.id,
|
||||||
|
).values("role")
|
||||||
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -180,7 +216,7 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
project_id=serializer.data["id"], member=request.user, role=20
|
project_id=serializer.data["id"], member=request.user, role=20
|
||||||
)
|
)
|
||||||
|
|
||||||
if serializer.data["project_lead"] is not None:
|
if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str(request.user.id):
|
||||||
ProjectMember.objects.create(
|
ProjectMember.objects.create(
|
||||||
project_id=serializer.data["id"],
|
project_id=serializer.data["id"],
|
||||||
member_id=serializer.data["project_lead"],
|
member_id=serializer.data["project_lead"],
|
||||||
@ -451,7 +487,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectMemberViewSet(BaseViewSet):
|
class ProjectMemberViewSet(BaseViewSet):
|
||||||
serializer_class = ProjectMemberSerializer
|
serializer_class = ProjectMemberAdminSerializer
|
||||||
model = ProjectMember
|
model = ProjectMember
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectBasePermission,
|
ProjectBasePermission,
|
||||||
@ -986,6 +1022,63 @@ class ProjectFavoritesViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectDeployBoardViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectMemberPermission,
|
||||||
|
]
|
||||||
|
serializer_class = ProjectDeployBoardSerializer
|
||||||
|
model = ProjectDeployBoard
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
)
|
||||||
|
.select_related("project")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
comments = request.data.get("comments", False)
|
||||||
|
reactions = request.data.get("reactions", False)
|
||||||
|
inbox = request.data.get("inbox", None)
|
||||||
|
votes = request.data.get("votes", False)
|
||||||
|
views = request.data.get(
|
||||||
|
"views",
|
||||||
|
{
|
||||||
|
"list": True,
|
||||||
|
"kanban": True,
|
||||||
|
"calendar": True,
|
||||||
|
"gantt": True,
|
||||||
|
"spreadsheet": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create(
|
||||||
|
anchor=f"{slug}/{project_id}",
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
project_deploy_board.comments = comments
|
||||||
|
project_deploy_board.reactions = reactions
|
||||||
|
project_deploy_board.inbox = inbox
|
||||||
|
project_deploy_board.votes = votes
|
||||||
|
project_deploy_board.views = views
|
||||||
|
|
||||||
|
project_deploy_board.save()
|
||||||
|
|
||||||
|
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberEndpoint(BaseAPIView):
|
class ProjectMemberEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
@ -1004,3 +1097,176 @@ class ProjectMemberEndpoint(BaseAPIView):
|
|||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Something went wrong please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except ProjectDeployBoard.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Project Deploy Board does not exists"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectDeployBoardIssuesPublicEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
||||||
|
# Custom ordering for priority and state
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", None]
|
||||||
|
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||||
|
|
||||||
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
|
issue_queryset = (
|
||||||
|
Issue.issue_objects.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.filter(project_id=project_id)
|
||||||
|
.filter(workspace__slug=slug)
|
||||||
|
.select_related("project", "workspace", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels")
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Priority Ordering
|
||||||
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
|
priority_order = (
|
||||||
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
priority_order=Case(
|
||||||
|
*[
|
||||||
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("priority_order")
|
||||||
|
|
||||||
|
# State Ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"state__name",
|
||||||
|
"state__group",
|
||||||
|
"-state__name",
|
||||||
|
"-state__group",
|
||||||
|
]:
|
||||||
|
state_order = (
|
||||||
|
state_order
|
||||||
|
if order_by_param in ["state__name", "state__group"]
|
||||||
|
else state_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
state_order=Case(
|
||||||
|
*[
|
||||||
|
When(state__group=state_group, then=Value(i))
|
||||||
|
for i, state_group in enumerate(state_order)
|
||||||
|
],
|
||||||
|
default=Value(len(state_order)),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("state_order")
|
||||||
|
# assignee and label ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"labels__name",
|
||||||
|
"-labels__name",
|
||||||
|
"assignees__first_name",
|
||||||
|
"-assignees__first_name",
|
||||||
|
]:
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
max_values=Max(
|
||||||
|
order_by_param[1::]
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else order_by_param
|
||||||
|
)
|
||||||
|
).order_by(
|
||||||
|
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
|
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
||||||
|
|
||||||
|
states = State.objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
).values("name", "group", "color", "id")
|
||||||
|
|
||||||
|
labels = Label.objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
).values("id", "name", "color", "parent")
|
||||||
|
|
||||||
|
## Grouping the results
|
||||||
|
group_by = request.GET.get("group_by", False)
|
||||||
|
if group_by:
|
||||||
|
issues = group_results(issues, group_by)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"issues": issues,
|
||||||
|
"states": states,
|
||||||
|
"labels": labels,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
except ProjectDeployBoard.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
@ -19,6 +19,7 @@ from plane.db.models import (
|
|||||||
IssueView,
|
IssueView,
|
||||||
Issue,
|
Issue,
|
||||||
IssueViewFavorite,
|
IssueViewFavorite,
|
||||||
|
IssueReaction,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
@ -77,6 +78,12 @@ class ViewIssuesEndpoint(BaseAPIView):
|
|||||||
.select_related("parent")
|
.select_related("parent")
|
||||||
.prefetch_related("assignees")
|
.prefetch_related("assignees")
|
||||||
.prefetch_related("labels")
|
.prefetch_related("labels")
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = IssueLiteSerializer(issues, many=True)
|
serializer = IssueLiteSerializer(issues, many=True)
|
||||||
|
357
apiserver/plane/bgtasks/export_task.py
Normal file
357
apiserver/plane/bgtasks/export_task.py
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
# Python imports
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import boto3
|
||||||
|
import zipfile
|
||||||
|
from datetime import datetime, date, timedelta
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from celery import shared_task
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
from botocore.client import Config
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import NamedStyle
|
||||||
|
from openpyxl.utils.datetime import to_excel
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import Issue, ExporterHistory, Project
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimeEncoder(json.JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, (datetime, date)):
|
||||||
|
return obj.isoformat()
|
||||||
|
return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def create_csv_file(data):
|
||||||
|
csv_buffer = io.StringIO()
|
||||||
|
csv_writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
csv_writer.writerow(row)
|
||||||
|
|
||||||
|
csv_buffer.seek(0)
|
||||||
|
return csv_buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def create_json_file(data):
|
||||||
|
return json.dumps(data, cls=DateTimeEncoder)
|
||||||
|
|
||||||
|
|
||||||
|
def create_xlsx_file(data):
|
||||||
|
workbook = Workbook()
|
||||||
|
sheet = workbook.active
|
||||||
|
|
||||||
|
no_timezone_style = NamedStyle(name="no_timezone_style")
|
||||||
|
no_timezone_style.number_format = "yyyy-mm-dd hh:mm:ss"
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
sheet.append(row)
|
||||||
|
|
||||||
|
for column_cells in sheet.columns:
|
||||||
|
for cell in column_cells:
|
||||||
|
if isinstance(cell.value, datetime):
|
||||||
|
cell.style = no_timezone_style
|
||||||
|
cell.value = to_excel(cell.value.replace(tzinfo=None))
|
||||||
|
|
||||||
|
xlsx_buffer = io.BytesIO()
|
||||||
|
workbook.save(xlsx_buffer)
|
||||||
|
xlsx_buffer.seek(0)
|
||||||
|
return xlsx_buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def create_zip_file(files):
|
||||||
|
zip_buffer = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||||
|
for filename, file_content in files:
|
||||||
|
zipf.writestr(filename, file_content)
|
||||||
|
|
||||||
|
zip_buffer.seek(0)
|
||||||
|
return zip_buffer
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_s3(zip_file, workspace_id, token_id):
|
||||||
|
s3 = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name="ap-south-1",
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
config=Config(signature_version="s3v4"),
|
||||||
|
)
|
||||||
|
file_name = f"{workspace_id}/issues-{datetime.now().date()}.zip"
|
||||||
|
|
||||||
|
s3.upload_fileobj(
|
||||||
|
zip_file,
|
||||||
|
settings.AWS_S3_BUCKET_NAME,
|
||||||
|
file_name,
|
||||||
|
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||||
|
)
|
||||||
|
|
||||||
|
expires_in = 7 * 24 * 60 * 60
|
||||||
|
presigned_url = s3.generate_presigned_url(
|
||||||
|
"get_object",
|
||||||
|
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
|
||||||
|
ExpiresIn=expires_in,
|
||||||
|
)
|
||||||
|
|
||||||
|
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||||
|
|
||||||
|
if presigned_url:
|
||||||
|
exporter_instance.url = presigned_url
|
||||||
|
exporter_instance.status = "completed"
|
||||||
|
exporter_instance.key = file_name
|
||||||
|
else:
|
||||||
|
exporter_instance.status = "failed"
|
||||||
|
|
||||||
|
exporter_instance.save(update_fields=["status", "url","key"])
|
||||||
|
|
||||||
|
|
||||||
|
def generate_table_row(issue):
|
||||||
|
return [
|
||||||
|
f"""{issue["project__identifier"]}-{issue["sequence_id"]}""",
|
||||||
|
issue["project__name"],
|
||||||
|
issue["name"],
|
||||||
|
issue["description_stripped"],
|
||||||
|
issue["state__name"],
|
||||||
|
issue["priority"],
|
||||||
|
f"{issue['created_by__first_name']} {issue['created_by__last_name']}"
|
||||||
|
if issue["created_by__first_name"] and issue["created_by__last_name"]
|
||||||
|
else "",
|
||||||
|
f"{issue['assignees__first_name']} {issue['assignees__last_name']}"
|
||||||
|
if issue["assignees__first_name"] and issue["assignees__last_name"]
|
||||||
|
else "",
|
||||||
|
issue["labels__name"],
|
||||||
|
issue["issue_cycle__cycle__name"],
|
||||||
|
issue["issue_cycle__cycle__start_date"],
|
||||||
|
issue["issue_cycle__cycle__end_date"],
|
||||||
|
issue["issue_module__module__name"],
|
||||||
|
issue["issue_module__module__start_date"],
|
||||||
|
issue["issue_module__module__target_date"],
|
||||||
|
issue["created_at"],
|
||||||
|
issue["updated_at"],
|
||||||
|
issue["completed_at"],
|
||||||
|
issue["archived_at"],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_json_row(issue):
|
||||||
|
return {
|
||||||
|
"ID": f"""{issue["project__identifier"]}-{issue["sequence_id"]}""",
|
||||||
|
"Project": issue["project__name"],
|
||||||
|
"Name": issue["name"],
|
||||||
|
"Description": issue["description_stripped"],
|
||||||
|
"State": issue["state__name"],
|
||||||
|
"Priority": issue["priority"],
|
||||||
|
"Created By": f"{issue['created_by__first_name']} {issue['created_by__last_name']}"
|
||||||
|
if issue["created_by__first_name"] and issue["created_by__last_name"]
|
||||||
|
else "",
|
||||||
|
"Assignee": f"{issue['assignees__first_name']} {issue['assignees__last_name']}"
|
||||||
|
if issue["assignees__first_name"] and issue["assignees__last_name"]
|
||||||
|
else "",
|
||||||
|
"Labels": issue["labels__name"],
|
||||||
|
"Cycle Name": issue["issue_cycle__cycle__name"],
|
||||||
|
"Cycle Start Date": issue["issue_cycle__cycle__start_date"],
|
||||||
|
"Cycle End Date": issue["issue_cycle__cycle__end_date"],
|
||||||
|
"Module Name": issue["issue_module__module__name"],
|
||||||
|
"Module Start Date": issue["issue_module__module__start_date"],
|
||||||
|
"Module Target Date": issue["issue_module__module__target_date"],
|
||||||
|
"Created At": issue["created_at"],
|
||||||
|
"Updated At": issue["updated_at"],
|
||||||
|
"Completed At": issue["completed_at"],
|
||||||
|
"Archived At": issue["archived_at"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def update_json_row(rows, row):
|
||||||
|
matched_index = next(
|
||||||
|
(
|
||||||
|
index
|
||||||
|
for index, existing_row in enumerate(rows)
|
||||||
|
if existing_row["ID"] == row["ID"]
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if matched_index is not None:
|
||||||
|
existing_assignees, existing_labels = (
|
||||||
|
rows[matched_index]["Assignee"],
|
||||||
|
rows[matched_index]["Labels"],
|
||||||
|
)
|
||||||
|
assignee, label = row["Assignee"], row["Labels"]
|
||||||
|
|
||||||
|
if assignee is not None and assignee not in existing_assignees:
|
||||||
|
rows[matched_index]["Assignee"] += f", {assignee}"
|
||||||
|
if label is not None and label not in existing_labels:
|
||||||
|
rows[matched_index]["Labels"] += f", {label}"
|
||||||
|
else:
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
|
||||||
|
def update_table_row(rows, row):
|
||||||
|
matched_index = next(
|
||||||
|
(index for index, existing_row in enumerate(rows) if existing_row[0] == row[0]),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if matched_index is not None:
|
||||||
|
existing_assignees, existing_labels = rows[matched_index][7:9]
|
||||||
|
assignee, label = row[7:9]
|
||||||
|
|
||||||
|
if assignee is not None and assignee not in existing_assignees:
|
||||||
|
rows[matched_index][7] += f", {assignee}"
|
||||||
|
if label is not None and label not in existing_labels:
|
||||||
|
rows[matched_index][8] += f", {label}"
|
||||||
|
else:
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_csv(header, project_id, issues, files):
|
||||||
|
"""
|
||||||
|
Generate CSV export for all the passed issues.
|
||||||
|
"""
|
||||||
|
rows = [
|
||||||
|
header,
|
||||||
|
]
|
||||||
|
for issue in issues:
|
||||||
|
row = generate_table_row(issue)
|
||||||
|
update_table_row(rows, row)
|
||||||
|
csv_file = create_csv_file(rows)
|
||||||
|
files.append((f"{project_id}.csv", csv_file))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_json(header, project_id, issues, files):
|
||||||
|
rows = []
|
||||||
|
for issue in issues:
|
||||||
|
row = generate_json_row(issue)
|
||||||
|
update_json_row(rows, row)
|
||||||
|
json_file = create_json_file(rows)
|
||||||
|
files.append((f"{project_id}.json", json_file))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_xlsx(header, project_id, issues, files):
|
||||||
|
rows = [header]
|
||||||
|
for issue in issues:
|
||||||
|
row = generate_table_row(issue)
|
||||||
|
update_table_row(rows, row)
|
||||||
|
xlsx_file = create_xlsx_file(rows)
|
||||||
|
files.append((f"{project_id}.xlsx", xlsx_file))
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def issue_export_task(provider, workspace_id, project_ids, token_id, multiple):
|
||||||
|
try:
|
||||||
|
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||||
|
exporter_instance.status = "processing"
|
||||||
|
exporter_instance.save(update_fields=["status"])
|
||||||
|
|
||||||
|
workspace_issues = (
|
||||||
|
(
|
||||||
|
Issue.objects.filter(
|
||||||
|
workspace__id=workspace_id, project_id__in=project_ids
|
||||||
|
)
|
||||||
|
.select_related("project", "workspace", "state", "parent", "created_by")
|
||||||
|
.prefetch_related(
|
||||||
|
"assignees", "labels", "issue_cycle__cycle", "issue_module__module"
|
||||||
|
)
|
||||||
|
.values(
|
||||||
|
"id",
|
||||||
|
"project__identifier",
|
||||||
|
"project__name",
|
||||||
|
"project__id",
|
||||||
|
"sequence_id",
|
||||||
|
"name",
|
||||||
|
"description_stripped",
|
||||||
|
"priority",
|
||||||
|
"state__name",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"completed_at",
|
||||||
|
"archived_at",
|
||||||
|
"issue_cycle__cycle__name",
|
||||||
|
"issue_cycle__cycle__start_date",
|
||||||
|
"issue_cycle__cycle__end_date",
|
||||||
|
"issue_module__module__name",
|
||||||
|
"issue_module__module__start_date",
|
||||||
|
"issue_module__module__target_date",
|
||||||
|
"created_by__first_name",
|
||||||
|
"created_by__last_name",
|
||||||
|
"assignees__first_name",
|
||||||
|
"assignees__last_name",
|
||||||
|
"labels__name",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("project__identifier","sequence_id")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
# CSV header
|
||||||
|
header = [
|
||||||
|
"ID",
|
||||||
|
"Project",
|
||||||
|
"Name",
|
||||||
|
"Description",
|
||||||
|
"State",
|
||||||
|
"Priority",
|
||||||
|
"Created By",
|
||||||
|
"Assignee",
|
||||||
|
"Labels",
|
||||||
|
"Cycle Name",
|
||||||
|
"Cycle Start Date",
|
||||||
|
"Cycle End Date",
|
||||||
|
"Module Name",
|
||||||
|
"Module Start Date",
|
||||||
|
"Module Target Date",
|
||||||
|
"Created At",
|
||||||
|
"Updated At",
|
||||||
|
"Completed At",
|
||||||
|
"Archived At",
|
||||||
|
]
|
||||||
|
|
||||||
|
EXPORTER_MAPPER = {
|
||||||
|
"csv": generate_csv,
|
||||||
|
"json": generate_json,
|
||||||
|
"xlsx": generate_xlsx,
|
||||||
|
}
|
||||||
|
|
||||||
|
files = []
|
||||||
|
if multiple:
|
||||||
|
for project_id in project_ids:
|
||||||
|
issues = workspace_issues.filter(project__id=project_id)
|
||||||
|
exporter = EXPORTER_MAPPER.get(provider)
|
||||||
|
if exporter is not None:
|
||||||
|
exporter(
|
||||||
|
header,
|
||||||
|
project_id,
|
||||||
|
issues,
|
||||||
|
files,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
exporter = EXPORTER_MAPPER.get(provider)
|
||||||
|
if exporter is not None:
|
||||||
|
exporter(
|
||||||
|
header,
|
||||||
|
workspace_id,
|
||||||
|
workspace_issues,
|
||||||
|
files,
|
||||||
|
)
|
||||||
|
|
||||||
|
zip_buffer = create_zip_file(files)
|
||||||
|
upload_to_s3(zip_buffer, workspace_id, token_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||||
|
exporter_instance.status = "failed"
|
||||||
|
exporter_instance.reason = str(e)
|
||||||
|
exporter_instance.save(update_fields=["status", "reason"])
|
||||||
|
|
||||||
|
# Print logs if in DEBUG mode
|
||||||
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
|
capture_exception(e)
|
||||||
|
return
|
38
apiserver/plane/bgtasks/exporter_expired_task.py
Normal file
38
apiserver/plane/bgtasks/exporter_expired_task.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Python imports
|
||||||
|
import boto3
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from celery import shared_task
|
||||||
|
from botocore.client import Config
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import ExporterHistory
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def delete_old_s3_link():
|
||||||
|
# Get a list of keys and IDs to process
|
||||||
|
expired_exporter_history = ExporterHistory.objects.filter(
|
||||||
|
Q(url__isnull=False) & Q(created_at__lte=timezone.now() - timedelta(days=8))
|
||||||
|
).values_list("key", "id")
|
||||||
|
|
||||||
|
s3 = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name="ap-south-1",
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
config=Config(signature_version="s3v4"),
|
||||||
|
)
|
||||||
|
|
||||||
|
for file_name, exporter_id in expired_exporter_history:
|
||||||
|
# Delete object from S3
|
||||||
|
if file_name:
|
||||||
|
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name)
|
||||||
|
|
||||||
|
ExporterHistory.objects.filter(id=exporter_id).update(url=None)
|
@ -1,191 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import csv
|
|
||||||
import io
|
|
||||||
|
|
||||||
# Django imports
|
|
||||||
from django.core.mail import EmailMultiAlternatives
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
from django.utils.html import strip_tags
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
# Third party imports
|
|
||||||
from celery import shared_task
|
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
|
|
||||||
# Module imports
|
|
||||||
from plane.db.models import Issue
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def issue_export_task(email, data, slug, exporter_name):
|
|
||||||
try:
|
|
||||||
|
|
||||||
project_ids = data.get("project_id", [])
|
|
||||||
issues_filter = {"workspace__slug": slug}
|
|
||||||
|
|
||||||
if project_ids:
|
|
||||||
issues_filter["project_id__in"] = project_ids
|
|
||||||
|
|
||||||
issues = (
|
|
||||||
Issue.objects.filter(**issues_filter)
|
|
||||||
.select_related("project", "workspace", "state", "parent", "created_by")
|
|
||||||
.prefetch_related(
|
|
||||||
"assignees", "labels", "issue_cycle__cycle", "issue_module__module"
|
|
||||||
)
|
|
||||||
.values_list(
|
|
||||||
"project__identifier",
|
|
||||||
"sequence_id",
|
|
||||||
"name",
|
|
||||||
"description_stripped",
|
|
||||||
"priority",
|
|
||||||
"start_date",
|
|
||||||
"target_date",
|
|
||||||
"state__name",
|
|
||||||
"project__name",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"completed_at",
|
|
||||||
"archived_at",
|
|
||||||
"issue_cycle__cycle__name",
|
|
||||||
"issue_cycle__cycle__start_date",
|
|
||||||
"issue_cycle__cycle__end_date",
|
|
||||||
"issue_module__module__name",
|
|
||||||
"issue_module__module__start_date",
|
|
||||||
"issue_module__module__target_date",
|
|
||||||
"created_by__first_name",
|
|
||||||
"created_by__last_name",
|
|
||||||
"assignees__first_name",
|
|
||||||
"assignees__last_name",
|
|
||||||
"labels__name",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# CSV header
|
|
||||||
header = [
|
|
||||||
"Issue ID",
|
|
||||||
"Project",
|
|
||||||
"Name",
|
|
||||||
"Description",
|
|
||||||
"State",
|
|
||||||
"Priority",
|
|
||||||
"Created By",
|
|
||||||
"Assignee",
|
|
||||||
"Labels",
|
|
||||||
"Cycle Name",
|
|
||||||
"Cycle Start Date",
|
|
||||||
"Cycle End Date",
|
|
||||||
"Module Name",
|
|
||||||
"Module Start Date",
|
|
||||||
"Module Target Date",
|
|
||||||
"Created At"
|
|
||||||
"Updated At"
|
|
||||||
"Completed At"
|
|
||||||
"Archived At"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Prepare the CSV data
|
|
||||||
rows = [header]
|
|
||||||
|
|
||||||
# Write data for each issue
|
|
||||||
for issue in issues:
|
|
||||||
(
|
|
||||||
project_identifier,
|
|
||||||
sequence_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
priority,
|
|
||||||
start_date,
|
|
||||||
target_date,
|
|
||||||
state_name,
|
|
||||||
project_name,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
completed_at,
|
|
||||||
archived_at,
|
|
||||||
cycle_name,
|
|
||||||
cycle_start_date,
|
|
||||||
cycle_end_date,
|
|
||||||
module_name,
|
|
||||||
module_start_date,
|
|
||||||
module_target_date,
|
|
||||||
created_by_first_name,
|
|
||||||
created_by_last_name,
|
|
||||||
assignees_first_names,
|
|
||||||
assignees_last_names,
|
|
||||||
labels_names,
|
|
||||||
) = issue
|
|
||||||
|
|
||||||
created_by_fullname = (
|
|
||||||
f"{created_by_first_name} {created_by_last_name}"
|
|
||||||
if created_by_first_name and created_by_last_name
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
assignees_names = ""
|
|
||||||
if assignees_first_names and assignees_last_names:
|
|
||||||
assignees_names = ", ".join(
|
|
||||||
[
|
|
||||||
f"{assignees_first_name} {assignees_last_name}"
|
|
||||||
for assignees_first_name, assignees_last_name in zip(
|
|
||||||
assignees_first_names, assignees_last_names
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
labels_names = ", ".join(labels_names) if labels_names else ""
|
|
||||||
|
|
||||||
row = [
|
|
||||||
f"{project_identifier}-{sequence_id}",
|
|
||||||
project_name,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
state_name,
|
|
||||||
priority,
|
|
||||||
created_by_fullname,
|
|
||||||
assignees_names,
|
|
||||||
labels_names,
|
|
||||||
cycle_name,
|
|
||||||
cycle_start_date,
|
|
||||||
cycle_end_date,
|
|
||||||
module_name,
|
|
||||||
module_start_date,
|
|
||||||
module_target_date,
|
|
||||||
start_date,
|
|
||||||
target_date,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
completed_at,
|
|
||||||
archived_at,
|
|
||||||
]
|
|
||||||
rows.append(row)
|
|
||||||
|
|
||||||
# Create CSV file in-memory
|
|
||||||
csv_buffer = io.StringIO()
|
|
||||||
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
|
||||||
|
|
||||||
# Write CSV data to the buffer
|
|
||||||
for row in rows:
|
|
||||||
writer.writerow(row)
|
|
||||||
|
|
||||||
subject = "Your Issue Export is ready"
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"username": exporter_name,
|
|
||||||
}
|
|
||||||
|
|
||||||
html_content = render_to_string("emails/exports/issues.html", context)
|
|
||||||
text_content = strip_tags(html_content)
|
|
||||||
|
|
||||||
csv_buffer.seek(0)
|
|
||||||
msg = EmailMultiAlternatives(
|
|
||||||
subject, text_content, settings.EMAIL_FROM, [email]
|
|
||||||
)
|
|
||||||
msg.attach(f"{slug}-issues-{timezone.now().date()}.csv", csv_buffer.read(), "text/csv")
|
|
||||||
msg.send(fail_silently=False)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# Print logs if in DEBUG mode
|
|
||||||
if settings.DEBUG:
|
|
||||||
print(e)
|
|
||||||
capture_exception(e)
|
|
||||||
return
|
|
@ -20,6 +20,10 @@ app.conf.beat_schedule = {
|
|||||||
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
|
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
|
||||||
"schedule": crontab(hour=0, minute=0),
|
"schedule": crontab(hour=0, minute=0),
|
||||||
},
|
},
|
||||||
|
"check-every-day-to-delete_exporter_history": {
|
||||||
|
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
|
||||||
|
"schedule": crontab(hour=0, minute=0),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Load task modules from all registered Django app configs.
|
# Load task modules from all registered Django app configs.
|
||||||
|
@ -0,0 +1,965 @@
|
|||||||
|
# Generated by Django 4.2.3 on 2023-08-04 11:15
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import plane.db.models.project
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0040_projectmember_preferences_user_cover_image_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='analyticview',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='analyticview',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycle',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycle',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycle',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycle',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cyclefavorite',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cyclefavorite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cyclefavorite',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cyclefavorite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycleissue',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycleissue',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycleissue',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycleissue',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimate',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimate',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimate',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimate',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimatepoint',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimatepoint',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimatepoint',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimatepoint',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='fileasset',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='fileasset',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubcommentsync',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubcommentsync',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubcommentsync',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubcommentsync',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubissuesync',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubissuesync',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubissuesync',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubissuesync',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepository',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepository',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepository',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepository',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepositorysync',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepositorysync',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepositorysync',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepositorysync',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importer',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importer',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importer',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importer',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inbox',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inbox',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inbox',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inbox',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inboxissue',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inboxissue',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inboxissue',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inboxissue',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='integration',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='integration',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueactivity',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueactivity',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueactivity',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueactivity',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueassignee',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueassignee',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueassignee',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueassignee',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueattachment',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueattachment',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueattachment',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueattachment',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueblocker',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueblocker',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueblocker',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueblocker',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuecomment',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuecomment',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuecomment',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuecomment',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelabel',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelabel',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelabel',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelabel',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelink',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelink',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelink',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelink',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelink',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueproperty',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueproperty',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueproperty',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueproperty',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuesequence',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuesequence',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuesequence',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuesequence',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueview',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueview',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueview',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueview',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueviewfavorite',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueviewfavorite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueviewfavorite',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueviewfavorite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='label',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='label',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='label',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='label',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='module',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='module',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='module',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='module',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulefavorite',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulefavorite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulefavorite',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulefavorite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='moduleissue',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='moduleissue',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='moduleissue',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='moduleissue',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulelink',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulelink',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulelink',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulelink',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulemember',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulemember',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulemember',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulemember',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='page',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='page',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='page',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='page',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pageblock',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pageblock',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pageblock',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pageblock',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagefavorite',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagefavorite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagefavorite',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagefavorite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagelabel',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagelabel',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagelabel',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagelabel',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='project',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='project',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectfavorite',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectfavorite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectfavorite',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectfavorite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectidentifier',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectidentifier',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmemberinvite',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmemberinvite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmemberinvite',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmemberinvite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='slackprojectsync',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='slackprojectsync',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='slackprojectsync',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='slackprojectsync',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='socialloginconnection',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='socialloginconnection',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='state',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='state',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='state',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='state',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='team',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='team',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='teammember',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='teammember',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspace',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspace',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspaceintegration',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspaceintegration',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacememberinvite',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacememberinvite',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacetheme',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacetheme',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProjectDeployBoard',
|
||||||
|
fields=[
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||||
|
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
|
('anchor', models.CharField(db_index=True, default=plane.db.models.project.get_anchor, max_length=255, unique=True)),
|
||||||
|
('comments', models.BooleanField(default=False)),
|
||||||
|
('reactions', models.BooleanField(default=False)),
|
||||||
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||||
|
('inbox', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bord_inbox', to='db.inbox')),
|
||||||
|
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||||
|
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||||
|
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Project Deploy Board',
|
||||||
|
'verbose_name_plural': 'Project Deploy Boards',
|
||||||
|
'db_table': 'project_deploy_boards',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
'unique_together': {('project', 'anchor')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
30
apiserver/plane/db/migrations/0042_auto_20230809_1745.py
Normal file
30
apiserver/plane/db/migrations/0042_auto_20230809_1745.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 4.2.3 on 2023-08-09 12:15
|
||||||
|
import random
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
def random_cycle_order(apps, schema_editor):
|
||||||
|
CycleModel = apps.get_model("db", "Cycle")
|
||||||
|
updated_cycles = []
|
||||||
|
for obj in CycleModel.objects.all():
|
||||||
|
obj.sort_order = random.randint(1, 65536)
|
||||||
|
updated_cycles.append(obj)
|
||||||
|
CycleModel.objects.bulk_update(updated_cycles, ["sort_order"], batch_size=100)
|
||||||
|
|
||||||
|
def random_module_order(apps, schema_editor):
|
||||||
|
ModuleModel = apps.get_model("db", "Module")
|
||||||
|
updated_modules = []
|
||||||
|
for obj in ModuleModel.objects.all():
|
||||||
|
obj.sort_order = random.randint(1, 65536)
|
||||||
|
updated_modules.append(obj)
|
||||||
|
ModuleModel.objects.bulk_update(updated_modules, ["sort_order"], batch_size=100)
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0041_user_display_name_alter_analyticview_created_by_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(random_cycle_order),
|
||||||
|
migrations.RunPython(random_module_order),
|
||||||
|
]
|
38
apiserver/plane/db/migrations/0043_auto_20230809_1645.py
Normal file
38
apiserver/plane/db/migrations/0043_auto_20230809_1645.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 4.2.3 on 2023-08-09 11:15
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_issue_properties(apps, schema_editor):
|
||||||
|
IssuePropertyModel = apps.get_model("db", "IssueProperty")
|
||||||
|
updated_issue_properties = []
|
||||||
|
for obj in IssuePropertyModel.objects.all():
|
||||||
|
obj.properties["start_date"] = True
|
||||||
|
updated_issue_properties.append(obj)
|
||||||
|
IssuePropertyModel.objects.bulk_update(
|
||||||
|
updated_issue_properties, ["properties"], batch_size=100
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def workspace_member_properties(apps, schema_editor):
|
||||||
|
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
|
||||||
|
updated_workspace_members = []
|
||||||
|
for obj in WorkspaceMemberModel.objects.all():
|
||||||
|
obj.view_props["properties"]["start_date"] = True
|
||||||
|
obj.default_props["properties"]["start_date"] = True
|
||||||
|
updated_workspace_members.append(obj)
|
||||||
|
|
||||||
|
WorkspaceMemberModel.objects.bulk_update(
|
||||||
|
updated_workspace_members, ["view_props", "default_props"], batch_size=100
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("db", "0042_alter_analyticview_created_by_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(update_user_issue_properties),
|
||||||
|
migrations.RunPython(workspace_member_properties),
|
||||||
|
]
|
@ -18,6 +18,7 @@ from .project import (
|
|||||||
ProjectMemberInvite,
|
ProjectMemberInvite,
|
||||||
ProjectIdentifier,
|
ProjectIdentifier,
|
||||||
ProjectFavorite,
|
ProjectFavorite,
|
||||||
|
ProjectDeployBoard,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .issue import (
|
from .issue import (
|
||||||
@ -36,6 +37,7 @@ from .issue import (
|
|||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
CommentReaction,
|
CommentReaction,
|
||||||
|
IssueVote,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .asset import FileAsset
|
from .asset import FileAsset
|
||||||
@ -72,4 +74,6 @@ from .inbox import Inbox, InboxIssue
|
|||||||
|
|
||||||
from .analytic import AnalyticView
|
from .analytic import AnalyticView
|
||||||
|
|
||||||
from .notification import Notification
|
from .notification import Notification
|
||||||
|
|
||||||
|
from .exporter import ExporterHistory
|
@ -17,6 +17,7 @@ class Cycle(ProjectBaseModel):
|
|||||||
related_name="owned_by_cycle",
|
related_name="owned_by_cycle",
|
||||||
)
|
)
|
||||||
view_props = models.JSONField(default=dict)
|
view_props = models.JSONField(default=dict)
|
||||||
|
sort_order = models.FloatField(default=65535)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Cycle"
|
verbose_name = "Cycle"
|
||||||
@ -24,6 +25,17 @@ class Cycle(ProjectBaseModel):
|
|||||||
db_table = "cycles"
|
db_table = "cycles"
|
||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self._state.adding:
|
||||||
|
smallest_sort_order = Cycle.objects.filter(
|
||||||
|
project=self.project
|
||||||
|
).aggregate(smallest=models.Min("sort_order"))["smallest"]
|
||||||
|
|
||||||
|
if smallest_sort_order is not None:
|
||||||
|
self.sort_order = smallest_sort_order - 10000
|
||||||
|
|
||||||
|
super(Cycle, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return name of the cycle"""
|
"""Return name of the cycle"""
|
||||||
return f"{self.name} <{self.project.name}>"
|
return f"{self.name} <{self.project.name}>"
|
||||||
|
56
apiserver/plane/db/models/exporter.py
Normal file
56
apiserver/plane/db/models/exporter.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from . import BaseModel
|
||||||
|
|
||||||
|
def generate_token():
|
||||||
|
return uuid4().hex
|
||||||
|
|
||||||
|
class ExporterHistory(BaseModel):
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
"db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_exporters"
|
||||||
|
)
|
||||||
|
project = ArrayField(models.UUIDField(default=uuid.uuid4), blank=True, null=True)
|
||||||
|
provider = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=(
|
||||||
|
("json", "json"),
|
||||||
|
("csv", "csv"),
|
||||||
|
("xlsx", "xlsx"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=(
|
||||||
|
("queued", "Queued"),
|
||||||
|
("processing", "Processing"),
|
||||||
|
("completed", "Completed"),
|
||||||
|
("failed", "Failed"),
|
||||||
|
),
|
||||||
|
default="queued",
|
||||||
|
)
|
||||||
|
reason = models.TextField(blank=True)
|
||||||
|
key = models.TextField(blank=True)
|
||||||
|
url = models.URLField(max_length=800, blank=True, null=True)
|
||||||
|
token = models.CharField(max_length=255, default=generate_token, unique=True)
|
||||||
|
initiated_by = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="workspace_exporters"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Exporter"
|
||||||
|
verbose_name_plural = "Exporters"
|
||||||
|
db_table = "exporters"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return name of the service"""
|
||||||
|
return f"{self.provider} <{self.workspace.name}>"
|
@ -108,11 +108,7 @@ class Issue(ProjectBaseModel):
|
|||||||
~models.Q(name="Triage"), project=self.project
|
~models.Q(name="Triage"), project=self.project
|
||||||
).first()
|
).first()
|
||||||
self.state = random_state
|
self.state = random_state
|
||||||
if random_state.group == "started":
|
|
||||||
self.start_date = timezone.now().date()
|
|
||||||
else:
|
else:
|
||||||
if default_state.group == "started":
|
|
||||||
self.start_date = timezone.now().date()
|
|
||||||
self.state = default_state
|
self.state = default_state
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
@ -127,8 +123,6 @@ class Issue(ProjectBaseModel):
|
|||||||
PageBlock.objects.filter(issue_id=self.id).filter().update(
|
PageBlock.objects.filter(issue_id=self.id).filter().update(
|
||||||
completed_at=timezone.now()
|
completed_at=timezone.now()
|
||||||
)
|
)
|
||||||
elif self.state.group == "started":
|
|
||||||
self.start_date = timezone.now().date()
|
|
||||||
else:
|
else:
|
||||||
PageBlock.objects.filter(issue_id=self.id).filter().update(
|
PageBlock.objects.filter(issue_id=self.id).filter().update(
|
||||||
completed_at=None
|
completed_at=None
|
||||||
@ -153,9 +147,6 @@ class Issue(ProjectBaseModel):
|
|||||||
if largest_sort_order is not None:
|
if largest_sort_order is not None:
|
||||||
self.sort_order = largest_sort_order + 10000
|
self.sort_order = largest_sort_order + 10000
|
||||||
|
|
||||||
# If adding it to started state
|
|
||||||
if self.state.group == "started":
|
|
||||||
self.start_date = timezone.now().date()
|
|
||||||
# Strip the html tags using html parser
|
# Strip the html tags using html parser
|
||||||
self.description_stripped = (
|
self.description_stripped = (
|
||||||
None
|
None
|
||||||
@ -310,6 +301,14 @@ class IssueComment(ProjectBaseModel):
|
|||||||
related_name="comments",
|
related_name="comments",
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
access = models.CharField(
|
||||||
|
choices=(
|
||||||
|
("INTERNAL", "INTERNAL"),
|
||||||
|
("EXTERNAL", "EXTERNAL"),
|
||||||
|
),
|
||||||
|
default="INTERNAL",
|
||||||
|
max_length=100,
|
||||||
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.comment_stripped = (
|
self.comment_stripped = (
|
||||||
@ -425,13 +424,14 @@ class IssueSubscriber(ProjectBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class IssueReaction(ProjectBaseModel):
|
class IssueReaction(ProjectBaseModel):
|
||||||
|
|
||||||
actor = models.ForeignKey(
|
actor = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="issue_reactions",
|
related_name="issue_reactions",
|
||||||
)
|
)
|
||||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_reactions")
|
issue = models.ForeignKey(
|
||||||
|
Issue, on_delete=models.CASCADE, related_name="issue_reactions"
|
||||||
|
)
|
||||||
reaction = models.CharField(max_length=20)
|
reaction = models.CharField(max_length=20)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -446,13 +446,14 @@ class IssueReaction(ProjectBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CommentReaction(ProjectBaseModel):
|
class CommentReaction(ProjectBaseModel):
|
||||||
|
|
||||||
actor = models.ForeignKey(
|
actor = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="comment_reactions",
|
related_name="comment_reactions",
|
||||||
)
|
)
|
||||||
comment = models.ForeignKey(IssueComment, on_delete=models.CASCADE, related_name="comment_reactions")
|
comment = models.ForeignKey(
|
||||||
|
IssueComment, on_delete=models.CASCADE, related_name="comment_reactions"
|
||||||
|
)
|
||||||
reaction = models.CharField(max_length=20)
|
reaction = models.CharField(max_length=20)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -466,6 +467,27 @@ class CommentReaction(ProjectBaseModel):
|
|||||||
return f"{self.issue.name} {self.actor.email}"
|
return f"{self.issue.name} {self.actor.email}"
|
||||||
|
|
||||||
|
|
||||||
|
class IssueVote(ProjectBaseModel):
|
||||||
|
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="votes")
|
||||||
|
actor = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="votes"
|
||||||
|
)
|
||||||
|
vote = models.IntegerField(
|
||||||
|
choices=(
|
||||||
|
(-1, "DOWNVOTE"),
|
||||||
|
(1, "UPVOTE"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["issue", "actor"]
|
||||||
|
verbose_name = "Issue Vote"
|
||||||
|
verbose_name_plural = "Issue Votes"
|
||||||
|
db_table = "issue_votes"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.issue.name} {self.actor.email}"
|
||||||
|
|
||||||
|
|
||||||
# TODO: Find a better method to save the model
|
# TODO: Find a better method to save the model
|
||||||
@receiver(post_save, sender=Issue)
|
@receiver(post_save, sender=Issue)
|
||||||
|
@ -40,6 +40,7 @@ class Module(ProjectBaseModel):
|
|||||||
through_fields=("module", "member"),
|
through_fields=("module", "member"),
|
||||||
)
|
)
|
||||||
view_props = models.JSONField(default=dict)
|
view_props = models.JSONField(default=dict)
|
||||||
|
sort_order = models.FloatField(default=65535)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["name", "project"]
|
unique_together = ["name", "project"]
|
||||||
@ -48,6 +49,17 @@ class Module(ProjectBaseModel):
|
|||||||
db_table = "modules"
|
db_table = "modules"
|
||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self._state.adding:
|
||||||
|
smallest_sort_order = Module.objects.filter(
|
||||||
|
project=self.project
|
||||||
|
).aggregate(smallest=models.Min("sort_order"))["smallest"]
|
||||||
|
|
||||||
|
if smallest_sort_order is not None:
|
||||||
|
self.sort_order = smallest_sort_order - 10000
|
||||||
|
|
||||||
|
super(Module, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name} {self.start_date} {self.target_date}"
|
return f"{self.name} {self.start_date} {self.target_date}"
|
||||||
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# Python imports
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -31,12 +34,9 @@ def get_default_props():
|
|||||||
"showEmptyGroups": True,
|
"showEmptyGroups": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_default_preferences():
|
def get_default_preferences():
|
||||||
return {
|
return {"pages": {"block_display": True}}
|
||||||
"pages": {
|
|
||||||
"block_display": True
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Project(BaseModel):
|
class Project(BaseModel):
|
||||||
@ -157,7 +157,6 @@ class ProjectMember(ProjectBaseModel):
|
|||||||
preferences = models.JSONField(default=get_default_preferences)
|
preferences = models.JSONField(default=get_default_preferences)
|
||||||
sort_order = models.FloatField(default=65535)
|
sort_order = models.FloatField(default=65535)
|
||||||
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self._state.adding:
|
if self._state.adding:
|
||||||
smallest_sort_order = ProjectMember.objects.filter(
|
smallest_sort_order = ProjectMember.objects.filter(
|
||||||
@ -217,3 +216,41 @@ class ProjectFavorite(ProjectBaseModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return user of the project"""
|
"""Return user of the project"""
|
||||||
return f"{self.user.email} <{self.project.name}>"
|
return f"{self.user.email} <{self.project.name}>"
|
||||||
|
|
||||||
|
|
||||||
|
def get_anchor():
|
||||||
|
return uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_views():
|
||||||
|
return {
|
||||||
|
"list": True,
|
||||||
|
"kanban": True,
|
||||||
|
"calendar": True,
|
||||||
|
"gantt": True,
|
||||||
|
"spreadsheet": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectDeployBoard(ProjectBaseModel):
|
||||||
|
anchor = models.CharField(
|
||||||
|
max_length=255, default=get_anchor, unique=True, db_index=True
|
||||||
|
)
|
||||||
|
comments = models.BooleanField(default=False)
|
||||||
|
reactions = models.BooleanField(default=False)
|
||||||
|
inbox = models.ForeignKey(
|
||||||
|
"db.Inbox", related_name="bord_inbox", on_delete=models.SET_NULL, null=True
|
||||||
|
)
|
||||||
|
votes = models.BooleanField(default=False)
|
||||||
|
views = models.JSONField(default=get_default_views)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["project", "anchor"]
|
||||||
|
verbose_name = "Project Deploy Board"
|
||||||
|
verbose_name_plural = "Project Deploy Boards"
|
||||||
|
db_table = "project_deploy_boards"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return project and anchor"""
|
||||||
|
return f"{self.anchor} <{self.project.name}>"
|
||||||
|
@ -33,6 +33,7 @@ def get_default_props():
|
|||||||
"estimate": True,
|
"estimate": True,
|
||||||
"created_on": True,
|
"created_on": True,
|
||||||
"updated_on": True,
|
"updated_on": True,
|
||||||
|
"start_date": True,
|
||||||
},
|
},
|
||||||
"showEmptyGroups": True,
|
"showEmptyGroups": True,
|
||||||
}
|
}
|
||||||
|
@ -214,4 +214,4 @@ SIMPLE_JWT = {
|
|||||||
CELERY_TIMEZONE = TIME_ZONE
|
CELERY_TIMEZONE = TIME_ZONE
|
||||||
CELERY_TASK_SERIALIZER = 'json'
|
CELERY_TASK_SERIALIZER = 'json'
|
||||||
CELERY_ACCEPT_CONTENT = ['application/json']
|
CELERY_ACCEPT_CONTENT = ['application/json']
|
||||||
CELERY_IMPORTS = ("plane.bgtasks.issue_automation_task",)
|
CELERY_IMPORTS = ("plane.bgtasks.issue_automation_task","plane.bgtasks.exporter_expired_task")
|
||||||
|
@ -124,10 +124,11 @@ def filter_created_at(params, filter, method):
|
|||||||
else:
|
else:
|
||||||
if params.get("created_at", None) and len(params.get("created_at")):
|
if params.get("created_at", None) and len(params.get("created_at")):
|
||||||
for query in params.get("created_at"):
|
for query in params.get("created_at"):
|
||||||
if query.get("timeline", "after") == "after":
|
created_at_query = query.split(";")
|
||||||
filter["created_at__date__gte"] = query.get("datetime")
|
if len(created_at_query) == 2 and "after" in created_at_query:
|
||||||
|
filter["created_at__date__gte"] = created_at_query[0]
|
||||||
else:
|
else:
|
||||||
filter["created_at__date__lte"] = query.get("datetime")
|
filter["created_at__date__lte"] = created_at_query[0]
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
@ -144,10 +145,11 @@ def filter_updated_at(params, filter, method):
|
|||||||
else:
|
else:
|
||||||
if params.get("updated_at", None) and len(params.get("updated_at")):
|
if params.get("updated_at", None) and len(params.get("updated_at")):
|
||||||
for query in params.get("updated_at"):
|
for query in params.get("updated_at"):
|
||||||
if query.get("timeline", "after") == "after":
|
updated_at_query = query.split(";")
|
||||||
filter["updated_at__date__gte"] = query.get("datetime")
|
if len(updated_at_query) == 2 and "after" in updated_at_query:
|
||||||
|
filter["updated_at__date__gte"] = updated_at_query[0]
|
||||||
else:
|
else:
|
||||||
filter["updated_at__date__lte"] = query.get("datetime")
|
filter["updated_at__date__lte"] = updated_at_query[0]
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
@ -164,10 +166,11 @@ def filter_start_date(params, filter, method):
|
|||||||
else:
|
else:
|
||||||
if params.get("start_date", None) and len(params.get("start_date")):
|
if params.get("start_date", None) and len(params.get("start_date")):
|
||||||
for query in params.get("start_date"):
|
for query in params.get("start_date"):
|
||||||
if query.get("timeline", "after") == "after":
|
start_date_query = query.split(";")
|
||||||
filter["start_date__gte"] = query.get("datetime")
|
if len(start_date_query) == 2 and "after" in start_date_query:
|
||||||
|
filter["start_date__gte"] = start_date_query[0]
|
||||||
else:
|
else:
|
||||||
filter["start_date__lte"] = query.get("datetime")
|
filter["start_date__lte"] = start_date_query[0]
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
@ -184,10 +187,11 @@ def filter_target_date(params, filter, method):
|
|||||||
else:
|
else:
|
||||||
if params.get("target_date", None) and len(params.get("target_date")):
|
if params.get("target_date", None) and len(params.get("target_date")):
|
||||||
for query in params.get("target_date"):
|
for query in params.get("target_date"):
|
||||||
if query.get("timeline", "after") == "after":
|
target_date_query = query.split(";")
|
||||||
filter["target_date__gt"] = query.get("datetime")
|
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||||
|
filter["target_date__gt"] = target_date_query[0]
|
||||||
else:
|
else:
|
||||||
filter["target_date__lt"] = query.get("datetime")
|
filter["target_date__lt"] = target_date_query[0]
|
||||||
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
@ -205,10 +209,11 @@ def filter_completed_at(params, filter, method):
|
|||||||
else:
|
else:
|
||||||
if params.get("completed_at", None) and len(params.get("completed_at")):
|
if params.get("completed_at", None) and len(params.get("completed_at")):
|
||||||
for query in params.get("completed_at"):
|
for query in params.get("completed_at"):
|
||||||
if query.get("timeline", "after") == "after":
|
completed_at_query = query.split(";")
|
||||||
filter["completed_at__date__gte"] = query.get("datetime")
|
if len(completed_at_query) == 2 and "after" in completed_at_query:
|
||||||
|
filter["completed_at__date__gte"] = completed_at_query[0]
|
||||||
else:
|
else:
|
||||||
filter["completed_at__lte"] = query.get("datetime")
|
filter["completed_at__lte"] = completed_at_query[0]
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
@ -292,9 +297,16 @@ def filter_subscribed_issues(params, filter, method):
|
|||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
|
def filter_start_target_date_issues(params, filter, method):
|
||||||
|
start_target_date = params.get("start_target_date", "false")
|
||||||
|
if start_target_date == "true":
|
||||||
|
filter["target_date__isnull"] = False
|
||||||
|
filter["start_date__isnull"] = False
|
||||||
|
return filter
|
||||||
|
|
||||||
|
|
||||||
def issue_filters(query_params, method):
|
def issue_filters(query_params, method):
|
||||||
filter = dict()
|
filter = dict()
|
||||||
print(query_params)
|
|
||||||
|
|
||||||
ISSUE_FILTER = {
|
ISSUE_FILTER = {
|
||||||
"state": filter_state,
|
"state": filter_state,
|
||||||
@ -318,6 +330,7 @@ def issue_filters(query_params, method):
|
|||||||
"inbox_status": filter_inbox_status,
|
"inbox_status": filter_inbox_status,
|
||||||
"sub_issue": filter_sub_issue_toggle,
|
"sub_issue": filter_sub_issue_toggle,
|
||||||
"subscriber": filter_subscribed_issues,
|
"subscriber": filter_subscribed_issues,
|
||||||
|
"start_target_date": filter_start_target_date_issues,
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value in ISSUE_FILTER.items():
|
for key, value in ISSUE_FILTER.items():
|
||||||
|
@ -32,4 +32,5 @@ celery==5.3.1
|
|||||||
django_celery_beat==2.5.0
|
django_celery_beat==2.5.0
|
||||||
psycopg-binary==3.1.9
|
psycopg-binary==3.1.9
|
||||||
psycopg-c==3.1.9
|
psycopg-c==3.1.9
|
||||||
scout-apm==2.26.1
|
scout-apm==2.26.1
|
||||||
|
openpyxl==3.1.2
|
@ -5,18 +5,23 @@ type Props = {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
count: number;
|
count: number;
|
||||||
|
id: string;
|
||||||
}[];
|
}[];
|
||||||
title: string;
|
title: string;
|
||||||
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title, workspaceSlug }) => (
|
||||||
<div className="p-3 border border-custom-border-200 rounded-[10px]">
|
<div className="p-3 border border-custom-border-200 rounded-[10px]">
|
||||||
<h6 className="text-base font-medium">{title}</h6>
|
<h6 className="text-base font-medium">{title}</h6>
|
||||||
{users.length > 0 ? (
|
{users.length > 0 ? (
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
{users.map((user) => (
|
{users.map((user) => (
|
||||||
<div
|
<a
|
||||||
key={user.display_name ?? "None"}
|
key={user.display_name ?? "None"}
|
||||||
|
href={`/${workspaceSlug}/profile/${user.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
className="flex items-start justify-between gap-4 text-xs"
|
className="flex items-start justify-between gap-4 text-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -38,7 +43,7 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="flex-shrink-0">{user.count}</span>
|
<span className="flex-shrink-0">{user.count}</span>
|
||||||
</div>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -60,8 +60,10 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
|||||||
lastName: user?.created_by__last_name,
|
lastName: user?.created_by__last_name,
|
||||||
display_name: user?.created_by__display_name,
|
display_name: user?.created_by__display_name,
|
||||||
count: user?.count,
|
count: user?.count,
|
||||||
|
id: user?.created_by__id,
|
||||||
}))}
|
}))}
|
||||||
title="Most issues created"
|
title="Most issues created"
|
||||||
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
/>
|
/>
|
||||||
<AnalyticsLeaderboard
|
<AnalyticsLeaderboard
|
||||||
users={defaultAnalytics.most_issue_closed_user?.map((user) => ({
|
users={defaultAnalytics.most_issue_closed_user?.map((user) => ({
|
||||||
@ -70,8 +72,10 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
|||||||
lastName: user?.assignees__last_name,
|
lastName: user?.assignees__last_name,
|
||||||
display_name: user?.assignees__display_name,
|
display_name: user?.assignees__display_name,
|
||||||
count: user?.count,
|
count: user?.count,
|
||||||
|
id: user?.assignees__id,
|
||||||
}))}
|
}))}
|
||||||
title="Most issues closed"
|
title="Most issues closed"
|
||||||
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
/>
|
/>
|
||||||
<div className={fullScreen ? "md:col-span-2" : ""}>
|
<div className={fullScreen ? "md:col-span-2" : ""}>
|
||||||
<AnalyticsYearWiseIssues defaultAnalytics={defaultAnalytics} />
|
<AnalyticsYearWiseIssues defaultAnalytics={defaultAnalytics} />
|
||||||
|
@ -7,6 +7,8 @@ import { useTheme } from "next-themes";
|
|||||||
import { SettingIcon } from "components/icons";
|
import { SettingIcon } from "components/icons";
|
||||||
import userService from "services/user.service";
|
import userService from "services/user.service";
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
|
// helper
|
||||||
|
import { unsetCustomCssVariables } from "helpers/theme.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
@ -22,6 +24,8 @@ export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
|
|||||||
const updateUserTheme = (newTheme: string) => {
|
const updateUserTheme = (newTheme: string) => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
|
unsetCustomCssVariables();
|
||||||
|
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
|
|
||||||
mutateUser((prevData) => {
|
mutateUser((prevData) => {
|
||||||
|
@ -354,8 +354,8 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
<Command.Item
|
<Command.Item
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push(currentSection.path(item));
|
|
||||||
setIsPaletteOpen(false);
|
setIsPaletteOpen(false);
|
||||||
|
router.push(currentSection.path(item));
|
||||||
}}
|
}}
|
||||||
value={`${key}-${item?.name}`}
|
value={`${key}-${item?.name}`}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
@ -379,6 +379,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
<Command.Group heading="Issue actions">
|
<Command.Group heading="Issue actions">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
setIsPaletteOpen(false);
|
||||||
setPlaceholder("Change state...");
|
setPlaceholder("Change state...");
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
setPages([...pages, "change-issue-state"]);
|
setPages([...pages, "change-issue-state"]);
|
||||||
@ -460,6 +461,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
<Command.Group heading="Issue">
|
<Command.Group heading="Issue">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
setIsPaletteOpen(false);
|
||||||
const e = new KeyboardEvent("keydown", {
|
const e = new KeyboardEvent("keydown", {
|
||||||
key: "c",
|
key: "c",
|
||||||
});
|
});
|
||||||
@ -479,6 +481,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
<Command.Group heading="Project">
|
<Command.Group heading="Project">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
setIsPaletteOpen(false);
|
||||||
const e = new KeyboardEvent("keydown", {
|
const e = new KeyboardEvent("keydown", {
|
||||||
key: "p",
|
key: "p",
|
||||||
});
|
});
|
||||||
@ -500,6 +503,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
<Command.Group heading="Cycle">
|
<Command.Group heading="Cycle">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
setIsPaletteOpen(false);
|
||||||
const e = new KeyboardEvent("keydown", {
|
const e = new KeyboardEvent("keydown", {
|
||||||
key: "q",
|
key: "q",
|
||||||
});
|
});
|
||||||
@ -517,6 +521,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
<Command.Group heading="Module">
|
<Command.Group heading="Module">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
setIsPaletteOpen(false);
|
||||||
const e = new KeyboardEvent("keydown", {
|
const e = new KeyboardEvent("keydown", {
|
||||||
key: "m",
|
key: "m",
|
||||||
});
|
});
|
||||||
@ -534,6 +539,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
<Command.Group heading="View">
|
<Command.Group heading="View">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
setIsPaletteOpen(false);
|
||||||
const e = new KeyboardEvent("keydown", {
|
const e = new KeyboardEvent("keydown", {
|
||||||
key: "v",
|
key: "v",
|
||||||
});
|
});
|
||||||
@ -551,6 +557,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
<Command.Group heading="Page">
|
<Command.Group heading="Page">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
setIsPaletteOpen(false);
|
||||||
const e = new KeyboardEvent("keydown", {
|
const e = new KeyboardEvent("keydown", {
|
||||||
key: "d",
|
key: "d",
|
||||||
});
|
});
|
||||||
@ -568,11 +575,12 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
{projectDetails && projectDetails.inbox_view && (
|
{projectDetails && projectDetails.inbox_view && (
|
||||||
<Command.Group heading="Inbox">
|
<Command.Group heading="Inbox">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() =>
|
onSelect={() => {
|
||||||
|
setIsPaletteOpen(false);
|
||||||
redirect(
|
redirect(
|
||||||
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`
|
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
|
@ -35,6 +35,22 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UserLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`/${workspaceSlug}/profile/${activity.new_identifier ?? activity.old_identifier}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-custom-text-100 inline-flex items-center hover:underline"
|
||||||
|
>
|
||||||
|
{activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const activityDetails: {
|
const activityDetails: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode;
|
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode;
|
||||||
@ -46,8 +62,7 @@ const activityDetails: {
|
|||||||
if (activity.old_value === "")
|
if (activity.old_value === "")
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
added a new assignee{" "}
|
added a new assignee <UserLink activity={activity} />
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
@ -60,8 +75,7 @@ const activityDetails: {
|
|||||||
else
|
else
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
removed the assignee{" "}
|
removed the assignee <UserLink activity={activity} />
|
||||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
|
@ -113,49 +113,51 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<SelectFilters
|
{issueView !== "gantt_chart" && (
|
||||||
filters={filters}
|
<SelectFilters
|
||||||
onSelect={(option) => {
|
filters={filters}
|
||||||
const key = option.key as keyof typeof filters;
|
onSelect={(option) => {
|
||||||
|
const key = option.key as keyof typeof filters;
|
||||||
|
|
||||||
if (key === "target_date") {
|
if (key === "target_date") {
|
||||||
const valueExists = checkIfArraysHaveSameElements(
|
const valueExists = checkIfArraysHaveSameElements(
|
||||||
filters.target_date ?? [],
|
filters.target_date ?? [],
|
||||||
option.value
|
option.value
|
||||||
);
|
|
||||||
|
|
||||||
setFilters({
|
|
||||||
target_date: valueExists ? null : option.value,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const valueExists = filters[key]?.includes(option.value);
|
|
||||||
|
|
||||||
if (valueExists)
|
|
||||||
setFilters(
|
|
||||||
{
|
|
||||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
|
||||||
(val) => val !== option.value
|
|
||||||
),
|
|
||||||
},
|
|
||||||
!Boolean(viewId)
|
|
||||||
);
|
);
|
||||||
else
|
|
||||||
setFilters(
|
setFilters({
|
||||||
{
|
target_date: valueExists ? null : option.value,
|
||||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
});
|
||||||
},
|
} else {
|
||||||
!Boolean(viewId)
|
const valueExists = filters[key]?.includes(option.value);
|
||||||
);
|
|
||||||
}
|
if (valueExists)
|
||||||
}}
|
setFilters(
|
||||||
direction="left"
|
{
|
||||||
height="rg"
|
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||||
/>
|
(val) => val !== option.value
|
||||||
|
),
|
||||||
|
},
|
||||||
|
!Boolean(viewId)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
setFilters(
|
||||||
|
{
|
||||||
|
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||||
|
},
|
||||||
|
!Boolean(viewId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
direction="left"
|
||||||
|
height="rg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||||
open
|
open
|
||||||
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
||||||
: "text-custom-sidebar-text-200"
|
: "text-custom-sidebar-text-200"
|
||||||
@ -177,8 +179,9 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||||
<div className="relative divide-y-2 divide-custom-border-200">
|
<div className="relative divide-y-2 divide-custom-border-200">
|
||||||
<div className="space-y-4 pb-3 text-xs">
|
<div className="space-y-4 pb-3 text-xs">
|
||||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
{issueView !== "calendar" &&
|
||||||
<>
|
issueView !== "spreadsheet" &&
|
||||||
|
issueView !== "gantt_chart" && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-custom-text-200">Group by</h4>
|
<h4 className="text-custom-text-200">Group by</h4>
|
||||||
<div className="w-28">
|
<div className="w-28">
|
||||||
@ -206,34 +209,34 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
)}
|
||||||
<h4 className="text-custom-text-200">Order by</h4>
|
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||||
<div className="w-28">
|
<div className="flex items-center justify-between">
|
||||||
<CustomMenu
|
<h4 className="text-custom-text-200">Order by</h4>
|
||||||
label={
|
<div className="w-28">
|
||||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
<CustomMenu
|
||||||
"Select"
|
label={
|
||||||
}
|
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||||
className="!w-full"
|
"Select"
|
||||||
buttonClassName="w-full"
|
}
|
||||||
>
|
className="!w-full"
|
||||||
{ORDER_BY_OPTIONS.map((option) =>
|
buttonClassName="w-full"
|
||||||
groupByProperty === "priority" &&
|
>
|
||||||
option.key === "priority" ? null : (
|
{ORDER_BY_OPTIONS.map((option) =>
|
||||||
<CustomMenu.MenuItem
|
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||||
key={option.key}
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
key={option.key}
|
||||||
setOrderBy(option.key);
|
onClick={() => {
|
||||||
}}
|
setOrderBy(option.key);
|
||||||
>
|
}}
|
||||||
{option.name}
|
>
|
||||||
</CustomMenu.MenuItem>
|
{option.name}
|
||||||
)
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)
|
||||||
</CustomMenu>
|
)}
|
||||||
</div>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-custom-text-200">Issue type</h4>
|
<h4 className="text-custom-text-200">Issue type</h4>
|
||||||
@ -263,16 +266,19 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||||
<>
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
||||||
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
<div className="w-28">
|
||||||
<div className="w-28">
|
<ToggleSwitch
|
||||||
<ToggleSwitch
|
value={showSubIssues}
|
||||||
value={showSubIssues}
|
onChange={() => setShowSubIssues(!showSubIssues)}
|
||||||
onChange={() => setShowSubIssues(!showSubIssues)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{issueView !== "calendar" &&
|
||||||
|
issueView !== "spreadsheet" &&
|
||||||
|
issueView !== "gantt_chart" && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||||
<div className="w-28">
|
<div className="w-28">
|
||||||
@ -282,6 +288,10 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{issueView !== "calendar" &&
|
||||||
|
issueView !== "spreadsheet" &&
|
||||||
|
issueView !== "gantt_chart" && (
|
||||||
<div className="relative flex justify-end gap-x-3">
|
<div className="relative flex justify-end gap-x-3">
|
||||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||||
Reset to default
|
Reset to default
|
||||||
@ -294,47 +304,48 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
Set as default
|
Set as default
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 py-3">
|
{issueView !== "gantt_chart" && (
|
||||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
<div className="space-y-2 py-3">
|
||||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||||
{Object.keys(properties).map((key) => {
|
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||||
if (key === "estimate" && !isEstimateActive) return null;
|
{Object.keys(properties).map((key) => {
|
||||||
|
if (key === "estimate" && !isEstimateActive) return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
issueView === "spreadsheet" &&
|
issueView === "spreadsheet" &&
|
||||||
(key === "attachment_count" ||
|
(key === "attachment_count" ||
|
||||||
key === "link" ||
|
key === "link" ||
|
||||||
key === "sub_issue_count")
|
key === "sub_issue_count")
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
issueView !== "spreadsheet" &&
|
issueView !== "spreadsheet" &&
|
||||||
(key === "created_on" || key === "updated_on")
|
(key === "created_on" || key === "updated_on")
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
type="button"
|
type="button"
|
||||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||||
properties[key as keyof Properties]
|
properties[key as keyof Properties]
|
||||||
? "border-custom-primary bg-custom-primary text-white"
|
? "border-custom-primary bg-custom-primary text-white"
|
||||||
: "border-custom-border-200"
|
: "border-custom-border-200"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
onClick={() => setProperties(key as keyof Properties)}
|
||||||
>
|
>
|
||||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
@ -27,8 +27,8 @@ const unsplashEnabled =
|
|||||||
|
|
||||||
const tabOptions = [
|
const tabOptions = [
|
||||||
{
|
{
|
||||||
key: "unsplash",
|
key: "images",
|
||||||
title: "Unsplash",
|
title: "Images",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "upload",
|
key: "upload",
|
||||||
|
@ -21,13 +21,21 @@ import { ICustomTheme } from "types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: keyof ICustomTheme;
|
name: keyof ICustomTheme;
|
||||||
|
position?: "left" | "right";
|
||||||
watch: UseFormWatch<any>;
|
watch: UseFormWatch<any>;
|
||||||
setValue: UseFormSetValue<any>;
|
setValue: UseFormSetValue<any>;
|
||||||
error: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
|
error: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
|
||||||
register: UseFormRegister<any>;
|
register: UseFormRegister<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ColorPickerInput: React.FC<Props> = ({ name, watch, setValue, error, register }) => {
|
export const ColorPickerInput: React.FC<Props> = ({
|
||||||
|
name,
|
||||||
|
position = "left",
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
error,
|
||||||
|
register,
|
||||||
|
}) => {
|
||||||
const handleColorChange = (newColor: ColorResult) => {
|
const handleColorChange = (newColor: ColorResult) => {
|
||||||
const { hex } = newColor;
|
const { hex } = newColor;
|
||||||
setValue(name, hex);
|
setValue(name, hex);
|
||||||
@ -104,7 +112,11 @@ export const ColorPickerInput: React.FC<Props> = ({ name, watch, setValue, error
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute bottom-8 right-0 z-20 mt-1 max-w-xs px-2 sm:px-0">
|
<Popover.Panel
|
||||||
|
className={`absolute bottom-8 z-20 mt-1 max-w-xs px-2 sm:px-0 ${
|
||||||
|
position === "right" ? "left-0" : "right-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<SketchPicker color={watch(name)} onChange={handleColorChange} />
|
<SketchPicker color={watch(name)} onChange={handleColorChange} />
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
@ -83,6 +83,7 @@ export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData })
|
|||||||
</h3>
|
</h3>
|
||||||
<ColorPickerInput
|
<ColorPickerInput
|
||||||
name="background"
|
name="background"
|
||||||
|
position="right"
|
||||||
error={errors.background}
|
error={errors.background}
|
||||||
watch={watch}
|
watch={watch}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
@ -120,6 +121,7 @@ export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData })
|
|||||||
</h3>
|
</h3>
|
||||||
<ColorPickerInput
|
<ColorPickerInput
|
||||||
name="sidebarBackground"
|
name="sidebarBackground"
|
||||||
|
position="right"
|
||||||
error={errors.sidebarBackground}
|
error={errors.sidebarBackground}
|
||||||
watch={watch}
|
watch={watch}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
|
@ -10,7 +10,7 @@ import projectService from "services/project.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useProjects from "hooks/use-projects";
|
import useProjects from "hooks/use-projects";
|
||||||
// component
|
// component
|
||||||
import { Avatar } from "components/ui";
|
import { Avatar, Icon } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
|
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||||
@ -140,7 +140,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<div className={`flex items-center ${isCollapsed ? "gap-1" : "flex-col gap-2"}`}>
|
<div className={`flex items-center ${isCollapsed ? "gap-1" : "flex-col gap-2"}`}>
|
||||||
<div
|
<div
|
||||||
className={`flex cursor-pointer items-center gap-x-3 max-w-[316px] ${
|
className={`flex cursor-pointer items-center gap-x-2 max-w-[316px] ${
|
||||||
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -155,11 +155,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{getGroupTitle()}
|
{getGroupTitle()}
|
||||||
</h2>
|
</h2>
|
||||||
<span
|
<span className={`${isCollapsed ? "ml-0.5" : ""} py-1 text-center text-sm`}>
|
||||||
className={`${
|
|
||||||
isCollapsed ? "ml-0.5" : ""
|
|
||||||
} min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs`}
|
|
||||||
>
|
|
||||||
{groupedIssues?.[groupTitle].length ?? 0}
|
{groupedIssues?.[groupTitle].length ?? 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -174,9 +170,12 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
<ArrowsPointingInIcon className="h-4 w-4" />
|
<Icon
|
||||||
|
iconName="close_fullscreen"
|
||||||
|
className="text-base font-medium text-custom-text-900"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ArrowsPointingOutIcon className="h-4 w-4" />
|
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{!disableUserActions && selectedGroup !== "created_by" && (
|
{!disableUserActions && selectedGroup !== "created_by" && (
|
||||||
|
@ -232,7 +232,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
</a>
|
</a>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div
|
<div
|
||||||
className={`mb-3 rounded bg-custom-background-90 shadow ${
|
className={`mb-3 rounded bg-custom-background-100 shadow ${
|
||||||
snapshot.isDragging ? "border-2 border-custom-primary shadow-lg" : ""
|
snapshot.isDragging ? "border-2 border-custom-primary shadow-lg" : ""
|
||||||
}`}
|
}`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
@ -301,10 +301,10 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h5 className="text-sm break-words line-clamp-3">{issue.name}</h5>
|
<h5 className="text-sm break-words line-clamp-2">{issue.name}</h5>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="relative mt-2.5 flex flex-wrap items-center gap-2 text-xs">
|
<div className="mt-2.5 flex overflow-x-scroll items-center gap-2 text-xs">
|
||||||
{properties.priority && (
|
{properties.priority && (
|
||||||
<ViewPrioritySelect
|
<ViewPrioritySelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
@ -347,6 +347,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
issue={issue}
|
issue={issue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
|
customButton
|
||||||
user={user}
|
user={user}
|
||||||
selfPositioned
|
selfPositioned
|
||||||
/>
|
/>
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
ViewEstimateSelect,
|
ViewEstimateSelect,
|
||||||
ViewIssueLabel,
|
ViewIssueLabel,
|
||||||
ViewPrioritySelect,
|
ViewPrioritySelect,
|
||||||
|
ViewStartDateSelect,
|
||||||
ViewStateSelect,
|
ViewStateSelect,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
import { Popover2 } from "@blueprintjs/popover2";
|
import { Popover2 } from "@blueprintjs/popover2";
|
||||||
@ -315,6 +316,19 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{properties.start_date && (
|
||||||
|
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
<ViewStartDateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
|
noBorder
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{properties.due_date && (
|
{properties.due_date && (
|
||||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
<ViewDueDateSelect
|
<ViewDueDateSelect
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// next imports
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { KeyedMutator } from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import cyclesService from "services/cycles.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
// components
|
// components
|
||||||
import { GanttChartRoot } from "components/gantt-chart";
|
import { CycleGanttBlock, GanttChartRoot, IBlockUpdateData } from "components/gantt-chart";
|
||||||
// ui
|
|
||||||
import { Tooltip } from "components/ui";
|
|
||||||
// types
|
// types
|
||||||
import { ICycle } from "types";
|
import { ICycle } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
cycles: ICycle[];
|
cycles: ICycle[];
|
||||||
|
mutateCycles: KeyedMutator<ICycle[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CyclesListGanttChartView: FC<Props> = ({ cycles }) => {
|
export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
// rendering issues on gantt sidebar
|
// rendering issues on gantt sidebar
|
||||||
const GanttSidebarBlockView = ({ data }: any) => (
|
const GanttSidebarBlockView = ({ data }: any) => (
|
||||||
@ -28,53 +35,65 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// rendering issues on gantt card
|
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
|
||||||
const GanttBlockView = ({ data }: { data: ICycle }) => (
|
if (!workspaceSlug || !user) return;
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${data?.id}`}>
|
|
||||||
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm">
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-[4px] h-full"
|
|
||||||
style={{ backgroundColor: "rgb(var(--color-primary-100))" }}
|
|
||||||
/>
|
|
||||||
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
|
|
||||||
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
|
|
||||||
{data?.name}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle gantt issue start date and target date
|
mutateCycles((prevData) => {
|
||||||
const handleUpdateDates = async (data: any) => {
|
if (!prevData) return prevData;
|
||||||
const payload = {
|
|
||||||
id: data?.id,
|
const newList = prevData.map((p) => ({
|
||||||
start_date: data?.start_date,
|
...p,
|
||||||
target_date: data?.target_date,
|
...(p.id === cycle.id
|
||||||
};
|
? {
|
||||||
|
start_date: payload.start_date ? payload.start_date : p.start_date,
|
||||||
|
target_date: payload.target_date ? payload.target_date : p.end_date,
|
||||||
|
sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (payload.sort_order) {
|
||||||
|
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
|
||||||
|
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newList;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
const newPayload: any = { ...payload };
|
||||||
|
|
||||||
|
if (newPayload.sort_order && payload.sort_order)
|
||||||
|
newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||||
|
|
||||||
|
cyclesService
|
||||||
|
.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload, user)
|
||||||
|
.finally(() => mutateCycles());
|
||||||
};
|
};
|
||||||
|
|
||||||
const blockFormat = (blocks: any) =>
|
const blockFormat = (blocks: ICycle[]) =>
|
||||||
blocks && blocks.length > 0
|
blocks && blocks.length > 0
|
||||||
? blocks.map((_block: any) => {
|
? blocks
|
||||||
if (_block?.start_date && _block.target_date) console.log("_block", _block);
|
.filter((b) => b.start_date && b.end_date)
|
||||||
return {
|
.map((block) => ({
|
||||||
start_date: new Date(_block.created_at),
|
data: block,
|
||||||
target_date: new Date(_block.updated_at),
|
id: block.id,
|
||||||
data: _block,
|
sort_order: block.sort_order,
|
||||||
};
|
start_date: new Date(block.start_date ?? ""),
|
||||||
})
|
target_date: new Date(block.end_date ?? ""),
|
||||||
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full overflow-y-auto">
|
<div className="w-full h-full overflow-y-auto">
|
||||||
<GanttChartRoot
|
<GanttChartRoot
|
||||||
title={"Cycles"}
|
title="Cycles"
|
||||||
loaderTitle="Cycles"
|
loaderTitle="Cycles"
|
||||||
blocks={cycles ? blockFormat(cycles) : null}
|
blocks={cycles ? blockFormat(cycles) : null}
|
||||||
blockUpdateHandler={handleUpdateDates}
|
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
|
||||||
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
||||||
blockRender={(data: any) => <GanttBlockView data={data} />}
|
blockRender={(data: any) => <CycleGanttBlock cycle={data as ICycle} />}
|
||||||
|
enableLeftDrag={false}
|
||||||
|
enableRightDrag={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -17,7 +17,7 @@ export const AllCyclesList: React.FC<Props> = ({ viewType }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { data: allCyclesList } = useSWR(
|
const { data: allCyclesList, mutate } = useSWR(
|
||||||
workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null,
|
workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () =>
|
? () =>
|
||||||
@ -25,5 +25,5 @@ export const AllCyclesList: React.FC<Props> = ({ viewType }) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
return <CyclesView cycles={allCyclesList} viewType={viewType} />;
|
return <CyclesView cycles={allCyclesList} mutateCycles={mutate} viewType={viewType} />;
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ export const CompletedCyclesList: React.FC<Props> = ({ viewType }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { data: completedCyclesList } = useSWR(
|
const { data: completedCyclesList, mutate } = useSWR(
|
||||||
workspaceSlug && projectId ? COMPLETED_CYCLES_LIST(projectId.toString()) : null,
|
workspaceSlug && projectId ? COMPLETED_CYCLES_LIST(projectId.toString()) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () =>
|
? () =>
|
||||||
@ -29,5 +29,5 @@ export const CompletedCyclesList: React.FC<Props> = ({ viewType }) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
return <CyclesView cycles={completedCyclesList} viewType={viewType} />;
|
return <CyclesView cycles={completedCyclesList} mutateCycles={mutate} viewType={viewType} />;
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ export const DraftCyclesList: React.FC<Props> = ({ viewType }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { data: draftCyclesList } = useSWR(
|
const { data: draftCyclesList, mutate } = useSWR(
|
||||||
workspaceSlug && projectId ? DRAFT_CYCLES_LIST(projectId.toString()) : null,
|
workspaceSlug && projectId ? DRAFT_CYCLES_LIST(projectId.toString()) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () =>
|
? () =>
|
||||||
@ -25,5 +25,5 @@ export const DraftCyclesList: React.FC<Props> = ({ viewType }) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
return <CyclesView cycles={draftCyclesList} viewType={viewType} />;
|
return <CyclesView cycles={draftCyclesList} mutateCycles={mutate} viewType={viewType} />;
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ export const UpcomingCyclesList: React.FC<Props> = ({ viewType }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { data: upcomingCyclesList } = useSWR(
|
const { data: upcomingCyclesList, mutate } = useSWR(
|
||||||
workspaceSlug && projectId ? UPCOMING_CYCLES_LIST(projectId.toString()) : null,
|
workspaceSlug && projectId ? UPCOMING_CYCLES_LIST(projectId.toString()) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () =>
|
? () =>
|
||||||
@ -29,5 +29,5 @@ export const UpcomingCyclesList: React.FC<Props> = ({ viewType }) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
return <CyclesView cycles={upcomingCyclesList} viewType={viewType} />;
|
return <CyclesView cycles={upcomingCyclesList} mutateCycles={mutate} viewType={viewType} />;
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import React, { useState } from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { KeyedMutator, mutate } from "swr";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import cyclesService from "services/cycles.service";
|
import cyclesService from "services/cycles.service";
|
||||||
@ -35,10 +35,11 @@ import {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
cycles: ICycle[] | undefined;
|
cycles: ICycle[] | undefined;
|
||||||
|
mutateCycles: KeyedMutator<ICycle[]>;
|
||||||
viewType: string | null;
|
viewType: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CyclesView: React.FC<Props> = ({ cycles, viewType }) => {
|
export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType }) => {
|
||||||
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
||||||
const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState<ICycle | null>(null);
|
const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState<ICycle | null>(null);
|
||||||
|
|
||||||
@ -202,7 +203,7 @@ export const CyclesView: React.FC<Props> = ({ cycles, viewType }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<CyclesListGanttChartView cycles={cycles ?? []} />
|
<CyclesListGanttChartView cycles={cycles ?? []} mutateCycles={mutateCycles} />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full grid place-items-center text-center">
|
<div className="h-full grid place-items-center text-center">
|
||||||
|
@ -1,20 +1,27 @@
|
|||||||
import { FC } from "react";
|
|
||||||
// next imports
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// components
|
|
||||||
import { GanttChartRoot } from "components/gantt-chart";
|
|
||||||
// ui
|
|
||||||
import { Tooltip } from "components/ui";
|
|
||||||
// hooks
|
// hooks
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view";
|
import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view";
|
||||||
|
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
|
||||||
|
// components
|
||||||
|
import {
|
||||||
|
GanttChartRoot,
|
||||||
|
IssueGanttBlock,
|
||||||
|
renderIssueBlocksStructure,
|
||||||
|
} from "components/gantt-chart";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {};
|
export const CycleIssuesGanttChartView = () => {
|
||||||
|
|
||||||
export const CycleIssuesGanttChartView: FC<Props> = ({}) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
|
|
||||||
|
const { orderBy } = useIssuesView();
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const { ganttIssues, mutateGanttIssues } = useGanttChartCycleIssues(
|
const { ganttIssues, mutateGanttIssues } = useGanttChartCycleIssues(
|
||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
projectId as string,
|
projectId as string,
|
||||||
@ -32,77 +39,18 @@ export const CycleIssuesGanttChartView: FC<Props> = ({}) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// rendering issues on gantt card
|
|
||||||
const GanttBlockView = ({ data }: any) => (
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${data?.id}`}>
|
|
||||||
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm">
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-[4px] h-full"
|
|
||||||
style={{ backgroundColor: data?.state_detail?.color || "rgb(var(--color-primary-100))" }}
|
|
||||||
/>
|
|
||||||
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
|
|
||||||
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
|
|
||||||
{data?.name}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{data.infoToggle && (
|
|
||||||
<Tooltip
|
|
||||||
tooltipContent={`No due-date set, rendered according to last updated date.`}
|
|
||||||
className={`z-[999999]`}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 mx-2 w-[18px] h-[18px] overflow-hidden flex justify-center items-center">
|
|
||||||
<span className="material-symbols-rounded text-custom-text-200 text-[18px]">
|
|
||||||
info
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle gantt issue start date and target date
|
|
||||||
const handleUpdateDates = async (data: any) => {
|
|
||||||
const payload = {
|
|
||||||
id: data?.id,
|
|
||||||
start_date: data?.start_date,
|
|
||||||
target_date: data?.target_date,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("payload", payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const blockFormat = (blocks: any) =>
|
|
||||||
blocks && blocks.length > 0
|
|
||||||
? blocks.map((_block: any) => {
|
|
||||||
let startDate = new Date(_block.created_at);
|
|
||||||
let targetDate = new Date(_block.updated_at);
|
|
||||||
let infoToggle = true;
|
|
||||||
|
|
||||||
if (_block?.start_date && _block.target_date) {
|
|
||||||
startDate = _block?.start_date;
|
|
||||||
targetDate = _block.target_date;
|
|
||||||
infoToggle = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
start_date: new Date(startDate),
|
|
||||||
target_date: new Date(targetDate),
|
|
||||||
infoToggle: infoToggle,
|
|
||||||
data: _block,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full p-3">
|
<div className="w-full h-full p-3">
|
||||||
<GanttChartRoot
|
<GanttChartRoot
|
||||||
title="Cycles"
|
title="Cycles"
|
||||||
loaderTitle="Cycles"
|
loaderTitle="Cycles"
|
||||||
blocks={ganttIssues ? blockFormat(ganttIssues) : null}
|
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
|
||||||
blockUpdateHandler={handleUpdateDates}
|
blockUpdateHandler={(block, payload) =>
|
||||||
|
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
|
||||||
|
}
|
||||||
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
||||||
blockRender={(data: any) => <GanttBlockView data={data} />}
|
blockRender={(data: any) => <IssueGanttBlock issue={data as IIssue} />}
|
||||||
|
enableReorder={orderBy === "sort_order"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
103
apps/app/components/gantt-chart/blocks/block.tsx
Normal file
103
apps/app/components/gantt-chart/blocks/block.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// ui
|
||||||
|
import { Tooltip } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { renderShortDate } from "helpers/date-time.helper";
|
||||||
|
// types
|
||||||
|
import { ICycle, IIssue, IModule } from "types";
|
||||||
|
// constants
|
||||||
|
import { MODULE_STATUS } from "constants/module";
|
||||||
|
|
||||||
|
export const IssueGanttBlock = ({ issue }: { issue: IIssue }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
||||||
|
<a className="relative flex items-center w-full h-full shadow-sm transition-all duration-300">
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 w-0.5 h-full"
|
||||||
|
style={{ backgroundColor: issue.state_detail?.color }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
tooltipContent={
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h5>{issue.name}</h5>
|
||||||
|
<div>
|
||||||
|
{renderShortDate(issue.start_date ?? "")} to{" "}
|
||||||
|
{renderShortDate(issue.target_date ?? "")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
position="top-left"
|
||||||
|
>
|
||||||
|
<div className="text-custom-text-100 text-sm truncate py-1 px-2.5 w-full">
|
||||||
|
{issue.name}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CycleGanttBlock = ({ cycle }: { cycle: ICycle }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={`/${workspaceSlug}/projects/${cycle.project}/cycles/${cycle.id}`}>
|
||||||
|
<a className="relative flex items-center w-full h-full shadow-sm transition-all duration-300">
|
||||||
|
<div className="flex-shrink-0 w-0.5 h-full bg-custom-primary-100" />
|
||||||
|
<Tooltip
|
||||||
|
tooltipContent={
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h5>{cycle.name}</h5>
|
||||||
|
<div>
|
||||||
|
{renderShortDate(cycle.start_date ?? "")} to {renderShortDate(cycle.end_date ?? "")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
position="top-left"
|
||||||
|
>
|
||||||
|
<div className="text-custom-text-100 text-sm truncate py-1 px-2.5 w-full">
|
||||||
|
{cycle.name}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModuleGanttBlock = ({ module }: { module: IModule }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
|
||||||
|
<a className="relative flex items-center w-full h-full shadow-sm transition-all duration-300">
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 w-0.5 h-full"
|
||||||
|
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === module.status)?.color }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
tooltipContent={
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h5>{module.name}</h5>
|
||||||
|
<div>
|
||||||
|
{renderShortDate(module.start_date ?? "")} to{" "}
|
||||||
|
{renderShortDate(module.target_date ?? "")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
position="top-left"
|
||||||
|
>
|
||||||
|
<div className="text-custom-text-100 text-sm truncate py-1 px-2.5 w-full">
|
||||||
|
{module.name}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
178
apps/app/components/gantt-chart/blocks/blocks-display.tsx
Normal file
178
apps/app/components/gantt-chart/blocks/blocks-display.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
// react-beautiful-dnd
|
||||||
|
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||||
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
|
// helpers
|
||||||
|
import { ChartDraggable } from "../helpers/draggable";
|
||||||
|
import { renderDateFormat } from "helpers/date-time.helper";
|
||||||
|
// types
|
||||||
|
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||||
|
|
||||||
|
export const GanttChartBlocks: FC<{
|
||||||
|
itemsContainerWidth: number;
|
||||||
|
blocks: IGanttBlock[] | null;
|
||||||
|
sidebarBlockRender: FC;
|
||||||
|
blockRender: FC;
|
||||||
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
|
enableLeftDrag: boolean;
|
||||||
|
enableRightDrag: boolean;
|
||||||
|
enableReorder: boolean;
|
||||||
|
}> = ({
|
||||||
|
itemsContainerWidth,
|
||||||
|
blocks,
|
||||||
|
sidebarBlockRender,
|
||||||
|
blockRender,
|
||||||
|
blockUpdateHandler,
|
||||||
|
enableLeftDrag,
|
||||||
|
enableRightDrag,
|
||||||
|
enableReorder,
|
||||||
|
}) => {
|
||||||
|
const handleChartBlockPosition = (
|
||||||
|
block: IGanttBlock,
|
||||||
|
totalBlockShifts: number,
|
||||||
|
dragDirection: "left" | "right"
|
||||||
|
) => {
|
||||||
|
let updatedDate = new Date();
|
||||||
|
|
||||||
|
if (dragDirection === "left") {
|
||||||
|
const originalDate = new Date(block.start_date);
|
||||||
|
|
||||||
|
const currentDay = originalDate.getDate();
|
||||||
|
updatedDate = new Date(originalDate);
|
||||||
|
|
||||||
|
updatedDate.setDate(currentDay - totalBlockShifts);
|
||||||
|
} else {
|
||||||
|
const originalDate = new Date(block.target_date);
|
||||||
|
|
||||||
|
const currentDay = originalDate.getDate();
|
||||||
|
updatedDate = new Date(originalDate);
|
||||||
|
|
||||||
|
updatedDate.setDate(currentDay + totalBlockShifts);
|
||||||
|
}
|
||||||
|
|
||||||
|
blockUpdateHandler(block.data, {
|
||||||
|
[dragDirection === "left" ? "start_date" : "target_date"]: renderDateFormat(updatedDate),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOrderChange = (result: DropResult) => {
|
||||||
|
if (!blocks) return;
|
||||||
|
|
||||||
|
const { source, destination, draggableId } = result;
|
||||||
|
|
||||||
|
if (!destination) return;
|
||||||
|
|
||||||
|
if (source.index === destination.index && document) {
|
||||||
|
// const draggedBlock = document.querySelector(`#${draggableId}`) as HTMLElement;
|
||||||
|
// const blockStyles = window.getComputedStyle(draggedBlock);
|
||||||
|
|
||||||
|
// console.log(blockStyles.marginLeft);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedSortOrder = blocks[source.index].sort_order;
|
||||||
|
|
||||||
|
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
|
||||||
|
else if (destination.index === blocks.length - 1)
|
||||||
|
updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
|
||||||
|
else {
|
||||||
|
const destinationSortingOrder = blocks[destination.index].sort_order;
|
||||||
|
const relativeDestinationSortingOrder =
|
||||||
|
source.index < destination.index
|
||||||
|
? blocks[destination.index + 1].sort_order
|
||||||
|
: blocks[destination.index - 1].sort_order;
|
||||||
|
|
||||||
|
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedElement = blocks.splice(source.index, 1)[0];
|
||||||
|
blocks.splice(destination.index, 0, removedElement);
|
||||||
|
|
||||||
|
blockUpdateHandler(removedElement.data, {
|
||||||
|
sort_order: {
|
||||||
|
destinationIndex: destination.index,
|
||||||
|
newSortOrder: updatedSortOrder,
|
||||||
|
sourceIndex: source.index,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto"
|
||||||
|
style={{ width: `${itemsContainerWidth}px` }}
|
||||||
|
>
|
||||||
|
<DragDropContext onDragEnd={handleOrderChange}>
|
||||||
|
<StrictModeDroppable droppableId="gantt">
|
||||||
|
{(droppableProvided, droppableSnapshot) => (
|
||||||
|
<div
|
||||||
|
className="w-full space-y-2"
|
||||||
|
ref={droppableProvided.innerRef}
|
||||||
|
{...droppableProvided.droppableProps}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{blocks &&
|
||||||
|
blocks.length > 0 &&
|
||||||
|
blocks.map(
|
||||||
|
(block, index: number) =>
|
||||||
|
block.start_date &&
|
||||||
|
block.target_date && (
|
||||||
|
<Draggable
|
||||||
|
key={`block-${block.id}`}
|
||||||
|
draggableId={`block-${block.id}`}
|
||||||
|
index={index}
|
||||||
|
isDragDisabled={!enableReorder}
|
||||||
|
>
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
droppableSnapshot.isDraggingOver ? "bg-custom-border-100/10" : ""
|
||||||
|
}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
>
|
||||||
|
<ChartDraggable
|
||||||
|
block={block}
|
||||||
|
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
|
||||||
|
enableLeftDrag={enableLeftDrag}
|
||||||
|
enableRightDrag={enableRightDrag}
|
||||||
|
provided={provided}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="rounded shadow-sm bg-custom-background-80 overflow-hidden h-9 flex items-center transition-all"
|
||||||
|
style={{
|
||||||
|
width: `${block.position?.width}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{blockRender({
|
||||||
|
...block.data,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ChartDraggable>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{droppableProvided.placeholder}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StrictModeDroppable>
|
||||||
|
</DragDropContext>
|
||||||
|
|
||||||
|
{/* sidebar */}
|
||||||
|
{/* <div className="fixed top-0 bottom-0 w-[300px] flex-shrink-0 divide-y divide-custom-border-200 border-r border-custom-border-200 overflow-y-auto">
|
||||||
|
{blocks &&
|
||||||
|
blocks.length > 0 &&
|
||||||
|
blocks.map((block: any, _idx: number) => (
|
||||||
|
<div className="relative h-[40px] bg-custom-background-100" key={`sidebar-blocks-${_idx}`}>
|
||||||
|
{sidebarBlockRender(block?.data)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
2
apps/app/components/gantt-chart/blocks/index.ts
Normal file
2
apps/app/components/gantt-chart/blocks/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./block";
|
||||||
|
export * from "./blocks-display";
|
@ -1,82 +0,0 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
|
||||||
// helpers
|
|
||||||
import { ChartDraggable } from "../helpers/draggable";
|
|
||||||
// data
|
|
||||||
import { datePreview } from "../data";
|
|
||||||
|
|
||||||
export const GanttChartBlocks: FC<{
|
|
||||||
itemsContainerWidth: number;
|
|
||||||
blocks: null | any[];
|
|
||||||
sidebarBlockRender: FC;
|
|
||||||
blockRender: FC;
|
|
||||||
}> = ({ itemsContainerWidth, blocks, sidebarBlockRender, blockRender }) => {
|
|
||||||
const handleChartBlockPosition = (block: any) => {
|
|
||||||
// setChartBlocks((prevData: any) =>
|
|
||||||
// prevData.map((_block: any) => (_block?.data?.id == block?.data?.id ? block : _block))
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="relative z-[5] mt-[58px] h-full w-[4000px] divide-x divide-gray-300 overflow-hidden overflow-y-auto"
|
|
||||||
style={{ width: `${itemsContainerWidth}px` }}
|
|
||||||
>
|
|
||||||
<div className="w-full">
|
|
||||||
{blocks &&
|
|
||||||
blocks.length > 0 &&
|
|
||||||
blocks.map((block: any, _idx: number) => (
|
|
||||||
<>
|
|
||||||
{block.start_date && block.target_date && (
|
|
||||||
<ChartDraggable
|
|
||||||
className="relative flex h-[40px] items-center"
|
|
||||||
key={`blocks-${_idx}`}
|
|
||||||
block={block}
|
|
||||||
handleBlock={handleChartBlockPosition}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="relative group inline-flex cursor-pointer items-center font-medium transition-all"
|
|
||||||
style={{ marginLeft: `${block?.position?.marginLeft}px` }}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 relative w-0 h-0 flex items-center invisible group-hover:visible whitespace-nowrap">
|
|
||||||
<div className="absolute right-0 mr-[5px] rounded-sm bg-custom-background-90 px-2 py-0.5 text-xs font-medium">
|
|
||||||
{block?.start_date ? datePreview(block?.start_date) : "-"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="rounded shadow-sm bg-custom-background-100 overflow-hidden relative flex items-center h-[34px] border border-custom-border-200"
|
|
||||||
style={{
|
|
||||||
width: `${block?.position?.width}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{blockRender({
|
|
||||||
...block?.data,
|
|
||||||
infoToggle: block?.infoToggle ? true : false,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 relative w-0 h-0 flex items-center invisible group-hover:visible whitespace-nowrap">
|
|
||||||
<div className="absolute left-0 ml-[5px] mr-[5px] rounded-sm bg-custom-background-90 px-2 py-0.5 text-xs font-medium">
|
|
||||||
{block?.target_date ? datePreview(block?.target_date) : "-"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ChartDraggable>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* sidebar */}
|
|
||||||
{/* <div className="fixed top-0 bottom-0 w-[300px] flex-shrink-0 divide-y divide-custom-border-200 border-r border-custom-border-200 overflow-y-auto">
|
|
||||||
{blocks &&
|
|
||||||
blocks.length > 0 &&
|
|
||||||
blocks.map((block: any, _idx: number) => (
|
|
||||||
<div className="relative h-[40px] bg-custom-background-100" key={`sidebar-blocks-${_idx}`}>
|
|
||||||
{sidebarBlockRender(block?.data)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -25,7 +25,7 @@ export const BiWeekChartView: FC<any> = () => {
|
|||||||
<div
|
<div
|
||||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||||
style={{ width: `${currentViewData.data.width}px` }}
|
style={{ width: `${currentViewData?.data.width}px` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
||||||
|
@ -25,7 +25,7 @@ export const DayChartView: FC<any> = () => {
|
|||||||
<div
|
<div
|
||||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||||
style={{ width: `${currentViewData.data.width}px` }}
|
style={{ width: `${currentViewData?.data.width}px` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
||||||
|
@ -25,7 +25,7 @@ export const HourChartView: FC<any> = () => {
|
|||||||
<div
|
<div
|
||||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||||
style={{ width: `${currentViewData.data.width}px` }}
|
style={{ width: `${currentViewData?.data.width}px` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/20/solid";
|
||||||
Bars4Icon,
|
|
||||||
XMarkIcon,
|
|
||||||
ArrowsPointingInIcon,
|
|
||||||
ArrowsPointingOutIcon,
|
|
||||||
} from "@heroicons/react/20/solid";
|
|
||||||
// components
|
// components
|
||||||
import { GanttChartBlocks } from "../blocks";
|
import { GanttChartBlocks } from "components/gantt-chart";
|
||||||
// import { HourChartView } from "./hours";
|
// import { HourChartView } from "./hours";
|
||||||
// import { DayChartView } from "./day";
|
// import { DayChartView } from "./day";
|
||||||
// import { WeekChartView } from "./week";
|
// import { WeekChartView } from "./week";
|
||||||
@ -30,9 +25,9 @@ import {
|
|||||||
getMonthChartItemPositionWidthInMonth,
|
getMonthChartItemPositionWidthInMonth,
|
||||||
} from "../views";
|
} from "../views";
|
||||||
// types
|
// types
|
||||||
import { ChartDataType } from "../types";
|
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types";
|
||||||
// data
|
// data
|
||||||
import { datePreview, currentViewDataWithView } from "../data";
|
import { currentViewDataWithView } from "../data";
|
||||||
// context
|
// context
|
||||||
import { useChart } from "../hooks";
|
import { useChart } from "../hooks";
|
||||||
|
|
||||||
@ -40,10 +35,13 @@ type ChartViewRootProps = {
|
|||||||
border: boolean;
|
border: boolean;
|
||||||
title: null | string;
|
title: null | string;
|
||||||
loaderTitle: string;
|
loaderTitle: string;
|
||||||
blocks: any;
|
blocks: IGanttBlock[] | null;
|
||||||
blockUpdateHandler: (data: any) => void;
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
sidebarBlockRender: FC<any>;
|
sidebarBlockRender: FC<any>;
|
||||||
blockRender: FC<any>;
|
blockRender: FC<any>;
|
||||||
|
enableLeftDrag: boolean;
|
||||||
|
enableRightDrag: boolean;
|
||||||
|
enableReorder: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
||||||
@ -54,6 +52,9 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
blockUpdateHandler,
|
blockUpdateHandler,
|
||||||
sidebarBlockRender,
|
sidebarBlockRender,
|
||||||
blockRender,
|
blockRender,
|
||||||
|
enableLeftDrag,
|
||||||
|
enableRightDrag,
|
||||||
|
enableReorder,
|
||||||
}) => {
|
}) => {
|
||||||
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
|
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
|
||||||
|
|
||||||
@ -62,13 +63,13 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
const [blocksSidebarView, setBlocksSidebarView] = useState<boolean>(false);
|
const [blocksSidebarView, setBlocksSidebarView] = useState<boolean>(false);
|
||||||
|
|
||||||
// blocks state management starts
|
// blocks state management starts
|
||||||
const [chartBlocks, setChartBlocks] = useState<any[] | null>(null);
|
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
|
||||||
|
|
||||||
const renderBlockStructure = (view: any, blocks: any) =>
|
const renderBlockStructure = (view: any, blocks: IGanttBlock[]) =>
|
||||||
blocks && blocks.length > 0
|
blocks && blocks.length > 0
|
||||||
? blocks.map((_block: any) => ({
|
? blocks.map((block: any) => ({
|
||||||
..._block,
|
...block,
|
||||||
position: getMonthChartItemPositionWidthInMonth(view, _block),
|
position: getMonthChartItemPositionWidthInMonth(view, block),
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@ -154,13 +155,14 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
|
|
||||||
const updatingCurrentLeftScrollPosition = (width: number) => {
|
const updatingCurrentLeftScrollPosition = (width: number) => {
|
||||||
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
||||||
scrollContainer.scrollLeft = width + scrollContainer.scrollLeft;
|
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
|
||||||
setItemsContainerWidth(width + scrollContainer.scrollLeft);
|
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
|
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
|
||||||
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
||||||
const clientVisibleWidth: number = scrollContainer.clientWidth;
|
|
||||||
|
const clientVisibleWidth: number = scrollContainer?.clientWidth;
|
||||||
let scrollWidth: number = 0;
|
let scrollWidth: number = 0;
|
||||||
let daysDifference: number = 0;
|
let daysDifference: number = 0;
|
||||||
|
|
||||||
@ -189,9 +191,9 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
||||||
|
|
||||||
const scrollWidth: number = scrollContainer.scrollWidth;
|
const scrollWidth: number = scrollContainer?.scrollWidth;
|
||||||
const clientVisibleWidth: number = scrollContainer.clientWidth;
|
const clientVisibleWidth: number = scrollContainer?.clientWidth;
|
||||||
const currentScrollPosition: number = scrollContainer.scrollLeft;
|
const currentScrollPosition: number = scrollContainer?.scrollLeft;
|
||||||
|
|
||||||
const approxRangeLeft: number =
|
const approxRangeLeft: number =
|
||||||
scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
|
scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
|
||||||
@ -207,6 +209,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
|
||||||
|
|
||||||
scrollContainer.addEventListener("scroll", onScroll);
|
scrollContainer.addEventListener("scroll", onScroll);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
scrollContainer.removeEventListener("scroll", onScroll);
|
scrollContainer.removeEventListener("scroll", onScroll);
|
||||||
};
|
};
|
||||||
@ -242,7 +245,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
{/* chart header */}
|
{/* chart header */}
|
||||||
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-5 gap-y-3 whitespace-nowrap p-2">
|
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap p-2">
|
||||||
{/* <div
|
{/* <div
|
||||||
className="transition-all border border-custom-border-200 w-[30px] h-[30px] flex justify-center items-center cursor-pointer rounded-sm hover:bg-custom-background-80"
|
className="transition-all border border-custom-border-200 w-[30px] h-[30px] flex justify-center items-center cursor-pointer rounded-sm hover:bg-custom-background-80"
|
||||||
onClick={() => setBlocksSidebarView(() => !blocksSidebarView)}
|
onClick={() => setBlocksSidebarView(() => !blocksSidebarView)}
|
||||||
@ -301,8 +304,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="transition-all border border-custom-border-200 w-[30px] h-[30px] flex justify-center items-center cursor-pointer rounded-sm hover:bg-custom-background-80"
|
className="transition-all border border-custom-border-200 p-1 flex justify-center items-center cursor-pointer rounded-sm hover:bg-custom-background-80"
|
||||||
onClick={() => setFullScreenMode(() => !fullScreenMode)}
|
onClick={() => setFullScreenMode((prevData) => !prevData)}
|
||||||
>
|
>
|
||||||
{fullScreenMode ? (
|
{fullScreenMode ? (
|
||||||
<ArrowsPointingInIcon className="h-4 w-4" />
|
<ArrowsPointingInIcon className="h-4 w-4" />
|
||||||
@ -325,6 +328,10 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
blocks={chartBlocks}
|
blocks={chartBlocks}
|
||||||
sidebarBlockRender={sidebarBlockRender}
|
sidebarBlockRender={sidebarBlockRender}
|
||||||
blockRender={blockRender}
|
blockRender={blockRender}
|
||||||
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
|
enableLeftDrag={enableLeftDrag}
|
||||||
|
enableRightDrag={enableRightDrag}
|
||||||
|
enableReorder={enableReorder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,48 +1,55 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// context
|
|
||||||
|
// hooks
|
||||||
import { useChart } from "../hooks";
|
import { useChart } from "../hooks";
|
||||||
|
// types
|
||||||
|
import { IMonthBlock } from "../views";
|
||||||
|
|
||||||
export const MonthChartView: FC<any> = () => {
|
export const MonthChartView: FC<any> = () => {
|
||||||
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
|
const { currentViewData, renderView } = useChart();
|
||||||
|
|
||||||
|
const monthBlocks: IMonthBlock[] = renderView;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-200">
|
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-100/50">
|
||||||
{renderView &&
|
{monthBlocks &&
|
||||||
renderView.length > 0 &&
|
monthBlocks.length > 0 &&
|
||||||
renderView.map((_itemRoot: any, _idxRoot: any) => (
|
monthBlocks.map((block, _idxRoot) => (
|
||||||
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
|
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
|
||||||
<div className="relative border-b border-custom-border-200">
|
<div className="relative border-b border-custom-border-200">
|
||||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
|
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
|
||||||
{_itemRoot?.title}
|
{block?.title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-full w-full divide-x divide-custom-border-200">
|
<div className="flex h-full w-full divide-x divide-custom-border-100/50">
|
||||||
{_itemRoot.children &&
|
{block?.children &&
|
||||||
_itemRoot.children.length > 0 &&
|
block?.children.length > 0 &&
|
||||||
_itemRoot.children.map((_item: any, _idx: any) => (
|
block?.children.map((monthDay, _idx) => (
|
||||||
<div
|
<div
|
||||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||||
style={{ width: `${currentViewData.data.width}px` }}
|
style={{ width: `${currentViewData?.data.width}px` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
||||||
_item?.today ? `text-red-500 border-red-500` : `border-custom-border-200`
|
monthDay?.today
|
||||||
|
? `text-red-500 border-red-500`
|
||||||
|
: `border-custom-border-200`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div>{_item.title}</div>
|
<div>{monthDay?.title}</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`relative h-full w-full flex-1 flex justify-center ${
|
className={`relative h-full w-full flex-1 flex justify-center ${
|
||||||
["sat", "sun"].includes(_item?.dayData?.shortTitle || "")
|
["sat", "sun"].includes(monthDay?.dayData?.shortTitle || "")
|
||||||
? `bg-custom-background-90`
|
? `bg-custom-background-90`
|
||||||
: ``
|
: ``
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{_item?.today && (
|
{monthDay?.today && (
|
||||||
<div className="absolute top-0 bottom-0 border border-red-500"> </div>
|
<div className="absolute top-0 bottom-0 w-[1px] bg-red-500" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,7 +25,7 @@ export const QuarterChartView: FC<any> = () => {
|
|||||||
<div
|
<div
|
||||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||||
style={{ width: `${currentViewData.data.width}px` }}
|
style={{ width: `${currentViewData?.data.width}px` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
||||||
|
@ -25,7 +25,7 @@ export const WeekChartView: FC<any> = () => {
|
|||||||
<div
|
<div
|
||||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||||
style={{ width: `${currentViewData.data.width}px` }}
|
style={{ width: `${currentViewData?.data.width}px` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
||||||
|
@ -25,7 +25,7 @@ export const YearChartView: FC<any> = () => {
|
|||||||
<div
|
<div
|
||||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||||
style={{ width: `${currentViewData.data.width}px` }}
|
style={{ width: `${currentViewData?.data.width}px` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
||||||
|
14
apps/app/components/gantt-chart/helpers/block-structure.tsx
Normal file
14
apps/app/components/gantt-chart/helpers/block-structure.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
import { IGanttBlock } from "components/gantt-chart";
|
||||||
|
|
||||||
|
export const renderIssueBlocksStructure = (blocks: IIssue[]): IGanttBlock[] =>
|
||||||
|
blocks && blocks.length > 0
|
||||||
|
? blocks.map((block) => ({
|
||||||
|
data: block,
|
||||||
|
id: block.id,
|
||||||
|
sort_order: block.sort_order,
|
||||||
|
start_date: new Date(block.start_date ?? ""),
|
||||||
|
target_date: new Date(block.target_date ?? ""),
|
||||||
|
}))
|
||||||
|
: [];
|
@ -1,138 +1,155 @@
|
|||||||
import { useState, useRef } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
|
|
||||||
export const ChartDraggable = ({ children, block, handleBlock, className }: any) => {
|
// react-beautiful-dnd
|
||||||
const [dragging, setDragging] = useState(false);
|
import { DraggableProvided } from "react-beautiful-dnd";
|
||||||
|
import { useChart } from "../hooks";
|
||||||
|
// types
|
||||||
|
import { IGanttBlock } from "../types";
|
||||||
|
|
||||||
const [chartBlockPositionLeft, setChartBlockPositionLeft] = useState(0);
|
type Props = {
|
||||||
const [blockPositionLeft, setBlockPositionLeft] = useState(0);
|
children: any;
|
||||||
const [dragBlockOffsetX, setDragBlockOffsetX] = useState(0);
|
block: IGanttBlock;
|
||||||
|
handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right") => void;
|
||||||
|
enableLeftDrag: boolean;
|
||||||
|
enableRightDrag: boolean;
|
||||||
|
provided: DraggableProvided;
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseDown = (event: any) => {
|
export const ChartDraggable: React.FC<Props> = ({
|
||||||
const chartBlockPositionLeft: number = block.position.marginLeft;
|
children,
|
||||||
const blockPositionLeft: number = event.target.getBoundingClientRect().left;
|
block,
|
||||||
const dragBlockOffsetX: number = event.clientX - event.target.getBoundingClientRect().left;
|
handleBlock,
|
||||||
|
enableLeftDrag = true,
|
||||||
|
enableRightDrag = true,
|
||||||
|
provided,
|
||||||
|
}) => {
|
||||||
|
const [isLeftResizing, setIsLeftResizing] = useState(false);
|
||||||
|
const [isRightResizing, setIsRightResizing] = useState(false);
|
||||||
|
|
||||||
console.log("--------------------");
|
const parentDivRef = useRef<HTMLDivElement>(null);
|
||||||
console.log("chartBlockPositionLeft", chartBlockPositionLeft);
|
const resizableRef = useRef<HTMLDivElement>(null);
|
||||||
console.log("blockPositionLeft", blockPositionLeft);
|
|
||||||
console.log("dragBlockOffsetX", dragBlockOffsetX);
|
|
||||||
console.log("-->");
|
|
||||||
|
|
||||||
setDragging(true);
|
const { currentViewData } = useChart();
|
||||||
setChartBlockPositionLeft(chartBlockPositionLeft);
|
|
||||||
setBlockPositionLeft(blockPositionLeft);
|
|
||||||
setDragBlockOffsetX(dragBlockOffsetX);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseMove = (event: any) => {
|
const handleDrag = (dragDirection: "left" | "right") => {
|
||||||
if (!dragging) return;
|
if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position)
|
||||||
|
return;
|
||||||
|
|
||||||
const currentBlockPosition = event.clientX - dragBlockOffsetX;
|
const resizableDiv = resizableRef.current;
|
||||||
console.log("currentBlockPosition", currentBlockPosition);
|
const parentDiv = parentDivRef.current;
|
||||||
if (currentBlockPosition <= blockPositionLeft) {
|
|
||||||
const updatedPosition = chartBlockPositionLeft - (blockPositionLeft - currentBlockPosition);
|
|
||||||
console.log("updatedPosition", updatedPosition);
|
|
||||||
handleBlock({ ...block, position: { ...block.position, marginLeft: updatedPosition } });
|
|
||||||
} else {
|
|
||||||
const updatedPosition = chartBlockPositionLeft + (blockPositionLeft - currentBlockPosition);
|
|
||||||
console.log("updatedPosition", updatedPosition);
|
|
||||||
handleBlock({ ...block, position: { ...block.position, marginLeft: updatedPosition } });
|
|
||||||
}
|
|
||||||
console.log("--------------------");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const columnWidth = currentViewData.data.width;
|
||||||
setDragging(false);
|
|
||||||
setChartBlockPositionLeft(0);
|
const blockInitialWidth =
|
||||||
setBlockPositionLeft(0);
|
resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
|
||||||
setDragBlockOffsetX(0);
|
|
||||||
|
let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
|
||||||
|
let initialMarginLeft = block?.position?.marginLeft;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!window) return;
|
||||||
|
|
||||||
|
let delWidth = 0;
|
||||||
|
|
||||||
|
const posFromLeft = e.clientX;
|
||||||
|
const posFromRight = window.innerWidth - e.clientX;
|
||||||
|
|
||||||
|
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
|
||||||
|
const appSidebar = document.querySelector("#app-sidebar") as HTMLElement;
|
||||||
|
|
||||||
|
// manually scroll to left if reached the left end while dragging
|
||||||
|
if (posFromLeft - appSidebar.clientWidth <= 70) {
|
||||||
|
if (e.movementX > 0) return;
|
||||||
|
|
||||||
|
delWidth = dragDirection === "left" ? -5 : 5;
|
||||||
|
|
||||||
|
scrollContainer.scrollBy(-1 * Math.abs(delWidth), 0);
|
||||||
|
} else delWidth = dragDirection === "left" ? -1 * e.movementX : e.movementX;
|
||||||
|
|
||||||
|
// manually scroll to right if reached the right end while dragging
|
||||||
|
if (posFromRight <= 70) {
|
||||||
|
if (e.movementX < 0) return;
|
||||||
|
|
||||||
|
delWidth = dragDirection === "left" ? -5 : 5;
|
||||||
|
|
||||||
|
scrollContainer.scrollBy(Math.abs(delWidth), 0);
|
||||||
|
} else delWidth = dragDirection === "left" ? -1 * e.movementX : e.movementX;
|
||||||
|
|
||||||
|
// calculate new width and update the initialMarginLeft using +=
|
||||||
|
const newWidth = Math.round((initialWidth += delWidth) / columnWidth) * columnWidth;
|
||||||
|
|
||||||
|
// block needs to be at least 1 column wide
|
||||||
|
if (newWidth < columnWidth) return;
|
||||||
|
|
||||||
|
resizableDiv.style.width = `${newWidth}px`;
|
||||||
|
if (block.position) block.position.width = newWidth;
|
||||||
|
|
||||||
|
// update the margin left of the block if dragging from the left end
|
||||||
|
if (dragDirection === "left") {
|
||||||
|
// calculate new marginLeft and update the initial marginLeft using -=
|
||||||
|
const newMarginLeft =
|
||||||
|
Math.round((initialMarginLeft -= delWidth) / columnWidth) * columnWidth;
|
||||||
|
|
||||||
|
parentDiv.style.marginLeft = `${newMarginLeft}px`;
|
||||||
|
if (block.position) block.position.marginLeft = newMarginLeft;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
|
||||||
|
const totalBlockShifts = Math.ceil(
|
||||||
|
(resizableDiv.clientWidth - blockInitialWidth) / columnWidth
|
||||||
|
);
|
||||||
|
|
||||||
|
handleBlock(totalBlockShifts, dragDirection);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onMouseDown={handleMouseDown}
|
id={`block-${block.id}`}
|
||||||
onMouseMove={handleMouseMove}
|
ref={parentDivRef}
|
||||||
onMouseUp={handleMouseUp}
|
className="relative group inline-flex cursor-pointer items-center font-medium transition-all"
|
||||||
className={`${className ? className : ``}`}
|
style={{
|
||||||
|
marginLeft: `${block.position?.marginLeft}px`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{enableLeftDrag && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
onMouseDown={() => handleDrag("left")}
|
||||||
|
onMouseEnter={() => setIsLeftResizing(true)}
|
||||||
|
onMouseLeave={() => setIsLeftResizing(false)}
|
||||||
|
className="absolute top-1/2 -left-2.5 -translate-y-1/2 z-[1] w-6 h-10 bg-brand-backdrop rounded-md cursor-col-resize"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`absolute top-1/2 -translate-y-1/2 w-1 h-4/5 rounded-sm bg-custom-background-80 transition-all duration-300 ${
|
||||||
|
isLeftResizing ? "-left-2.5" : "left-1"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{React.cloneElement(children, { ref: resizableRef, ...provided.dragHandleProps })}
|
||||||
|
{enableRightDrag && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
onMouseDown={() => handleDrag("right")}
|
||||||
|
onMouseEnter={() => setIsRightResizing(true)}
|
||||||
|
onMouseLeave={() => setIsRightResizing(false)}
|
||||||
|
className="absolute top-1/2 -right-2.5 -translate-y-1/2 z-[1] w-6 h-6 bg-brand-backdrop rounded-md cursor-col-resize"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`absolute top-1/2 -translate-y-1/2 w-1 h-4/5 rounded-sm bg-custom-background-80 transition-all duration-300 ${
|
||||||
|
isRightResizing ? "-right-2.5" : "right-1"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// import { useState } from "react";
|
|
||||||
|
|
||||||
// export const ChartDraggable = ({ children, id, className = "", style }: any) => {
|
|
||||||
// const [dragging, setDragging] = useState(false);
|
|
||||||
|
|
||||||
// const [chartBlockPositionLeft, setChartBlockPositionLeft] = useState(0);
|
|
||||||
// const [blockPositionLeft, setBlockPositionLeft] = useState(0);
|
|
||||||
// const [dragBlockOffsetX, setDragBlockOffsetX] = useState(0);
|
|
||||||
|
|
||||||
// const handleDragStart = (event: any) => {
|
|
||||||
// // event.dataTransfer.setData("text/plain", event.target.id);
|
|
||||||
|
|
||||||
// const chartBlockPositionLeft: number = parseInt(event.target.style.left.slice(0, -2));
|
|
||||||
// const blockPositionLeft: number = event.target.getBoundingClientRect().left;
|
|
||||||
// const dragBlockOffsetX: number = event.clientX - event.target.getBoundingClientRect().left;
|
|
||||||
|
|
||||||
// console.log("chartBlockPositionLeft", chartBlockPositionLeft);
|
|
||||||
// console.log("blockPositionLeft", blockPositionLeft);
|
|
||||||
// console.log("dragBlockOffsetX", dragBlockOffsetX);
|
|
||||||
// console.log("--------------------");
|
|
||||||
|
|
||||||
// setDragging(true);
|
|
||||||
// setChartBlockPositionLeft(chartBlockPositionLeft);
|
|
||||||
// setBlockPositionLeft(blockPositionLeft);
|
|
||||||
// setDragBlockOffsetX(dragBlockOffsetX);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleDragEnd = () => {
|
|
||||||
// setDragging(false);
|
|
||||||
// setChartBlockPositionLeft(0);
|
|
||||||
// setBlockPositionLeft(0);
|
|
||||||
// setDragBlockOffsetX(0);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleDragOver = (event: any) => {
|
|
||||||
// event.preventDefault();
|
|
||||||
// if (dragging) {
|
|
||||||
// const scrollContainer = document.getElementById(`block-parent-${id}`) as HTMLElement;
|
|
||||||
// const currentBlockPosition = event.clientX - dragBlockOffsetX;
|
|
||||||
// console.log('currentBlockPosition')
|
|
||||||
// if (currentBlockPosition <= blockPositionLeft) {
|
|
||||||
// const updatedPosition = chartBlockPositionLeft - (blockPositionLeft - currentBlockPosition);
|
|
||||||
// console.log("updatedPosition", updatedPosition);
|
|
||||||
// if (scrollContainer) scrollContainer.style.left = `${updatedPosition}px`;
|
|
||||||
// } else {
|
|
||||||
// const updatedPosition = chartBlockPositionLeft + (blockPositionLeft - currentBlockPosition);
|
|
||||||
// console.log("updatedPosition", updatedPosition);
|
|
||||||
// if (scrollContainer) scrollContainer.style.left = `${updatedPosition}px`;
|
|
||||||
// }
|
|
||||||
// console.log("--------------------");
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleDrop = (event: any) => {
|
|
||||||
// event.preventDefault();
|
|
||||||
// setDragging(false);
|
|
||||||
// setChartBlockPositionLeft(0);
|
|
||||||
// setBlockPositionLeft(0);
|
|
||||||
// setDragBlockOffsetX(0);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div
|
|
||||||
// id={id}
|
|
||||||
// draggable
|
|
||||||
// onDragStart={handleDragStart}
|
|
||||||
// onDragEnd={handleDragEnd}
|
|
||||||
// onDragOver={handleDragOver}
|
|
||||||
// onDrop={handleDrop}
|
|
||||||
// className={`${className} ${dragging ? "dragging" : ""}`}
|
|
||||||
// style={style}
|
|
||||||
// >
|
|
||||||
// {children}
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
1
apps/app/components/gantt-chart/helpers/index.ts
Normal file
1
apps/app/components/gantt-chart/helpers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./block-structure";
|
43
apps/app/components/gantt-chart/hooks/block-update.tsx
Normal file
43
apps/app/components/gantt-chart/hooks/block-update.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { KeyedMutator } from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue } from "types";
|
||||||
|
import { IBlockUpdateData } from "../types";
|
||||||
|
|
||||||
|
export const updateGanttIssue = (
|
||||||
|
issue: IIssue,
|
||||||
|
payload: IBlockUpdateData,
|
||||||
|
mutate: KeyedMutator<any>,
|
||||||
|
user: ICurrentUserResponse | undefined,
|
||||||
|
workspaceSlug: string | undefined
|
||||||
|
) => {
|
||||||
|
if (!issue || !workspaceSlug || !user) return;
|
||||||
|
|
||||||
|
mutate((prevData: IIssue[]) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
const newList = prevData.map((p) => ({
|
||||||
|
...p,
|
||||||
|
...(p.id === issue.id ? payload : {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (payload.sort_order) {
|
||||||
|
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
|
||||||
|
removedElement.sort_order = payload.sort_order.newSortOrder;
|
||||||
|
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newList;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
const newPayload: any = { ...payload };
|
||||||
|
|
||||||
|
if (newPayload.sort_order && payload.sort_order)
|
||||||
|
newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||||
|
|
||||||
|
issuesService
|
||||||
|
.patchIssue(workspaceSlug, issue.project, issue.id, newPayload, user)
|
||||||
|
.finally(() => mutate());
|
||||||
|
};
|
@ -1 +1,5 @@
|
|||||||
|
export * from "./blocks";
|
||||||
|
export * from "./helpers";
|
||||||
|
export * from "./hooks";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
export * from "./types";
|
||||||
|
@ -3,15 +3,20 @@ import { FC } from "react";
|
|||||||
import { ChartViewRoot } from "./chart";
|
import { ChartViewRoot } from "./chart";
|
||||||
// context
|
// context
|
||||||
import { ChartContextProvider } from "./contexts";
|
import { ChartContextProvider } from "./contexts";
|
||||||
|
// types
|
||||||
|
import { IBlockUpdateData, IGanttBlock } from "./types";
|
||||||
|
|
||||||
type GanttChartRootProps = {
|
type GanttChartRootProps = {
|
||||||
border?: boolean;
|
border?: boolean;
|
||||||
title: null | string;
|
title: null | string;
|
||||||
loaderTitle: string;
|
loaderTitle: string;
|
||||||
blocks: any;
|
blocks: IGanttBlock[] | null;
|
||||||
blockUpdateHandler: (data: any) => void;
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
sidebarBlockRender: FC<any>;
|
sidebarBlockRender: FC<any>;
|
||||||
blockRender: FC<any>;
|
blockRender: FC<any>;
|
||||||
|
enableLeftDrag?: boolean;
|
||||||
|
enableRightDrag?: boolean;
|
||||||
|
enableReorder?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GanttChartRoot: FC<GanttChartRootProps> = ({
|
export const GanttChartRoot: FC<GanttChartRootProps> = ({
|
||||||
@ -22,6 +27,9 @@ export const GanttChartRoot: FC<GanttChartRootProps> = ({
|
|||||||
blockUpdateHandler,
|
blockUpdateHandler,
|
||||||
sidebarBlockRender,
|
sidebarBlockRender,
|
||||||
blockRender,
|
blockRender,
|
||||||
|
enableLeftDrag = true,
|
||||||
|
enableRightDrag = true,
|
||||||
|
enableReorder = true,
|
||||||
}) => (
|
}) => (
|
||||||
<ChartContextProvider>
|
<ChartContextProvider>
|
||||||
<ChartViewRoot
|
<ChartViewRoot
|
||||||
@ -32,6 +40,9 @@ export const GanttChartRoot: FC<GanttChartRootProps> = ({
|
|||||||
blockUpdateHandler={blockUpdateHandler}
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
sidebarBlockRender={sidebarBlockRender}
|
sidebarBlockRender={sidebarBlockRender}
|
||||||
blockRender={blockRender}
|
blockRender={blockRender}
|
||||||
|
enableLeftDrag={enableLeftDrag}
|
||||||
|
enableRightDrag={enableRightDrag}
|
||||||
|
enableReorder={enableReorder}
|
||||||
/>
|
/>
|
||||||
</ChartContextProvider>
|
</ChartContextProvider>
|
||||||
);
|
);
|
||||||
|
@ -5,10 +5,32 @@ export type allViewsType = {
|
|||||||
data: Object | null;
|
data: Object | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface IGanttBlock {
|
||||||
|
data: any;
|
||||||
|
id: string;
|
||||||
|
position?: {
|
||||||
|
marginLeft: number;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
sort_order: number;
|
||||||
|
start_date: Date;
|
||||||
|
target_date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBlockUpdateData {
|
||||||
|
sort_order?: {
|
||||||
|
destinationIndex: number;
|
||||||
|
newSortOrder: number;
|
||||||
|
sourceIndex: number;
|
||||||
|
};
|
||||||
|
start_date?: string;
|
||||||
|
target_date?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChartContextData {
|
export interface ChartContextData {
|
||||||
allViews: allViewsType[];
|
allViews: allViewsType[];
|
||||||
currentView: "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year";
|
currentView: "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year";
|
||||||
currentViewData: any;
|
currentViewData: ChartDataType | undefined;
|
||||||
renderView: any;
|
renderView: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// types
|
// types
|
||||||
import { ChartDataType } from "../types";
|
import { ChartDataType, IGanttBlock } from "../types";
|
||||||
// data
|
// data
|
||||||
import { weeks, months } from "../data";
|
import { weeks, months } from "../data";
|
||||||
// helpers
|
// helpers
|
||||||
@ -19,7 +19,35 @@ type GetAllDaysInMonthInMonthViewType = {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
today: boolean;
|
today: boolean;
|
||||||
};
|
};
|
||||||
const getAllDaysInMonthInMonthView = (month: number, year: number) => {
|
|
||||||
|
interface IMonthChild {
|
||||||
|
active: boolean;
|
||||||
|
date: Date;
|
||||||
|
day: number;
|
||||||
|
dayData: {
|
||||||
|
key: number;
|
||||||
|
shortTitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
title: string;
|
||||||
|
today: boolean;
|
||||||
|
weekNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMonthBlock {
|
||||||
|
children: IMonthChild[];
|
||||||
|
month: number;
|
||||||
|
monthData: {
|
||||||
|
key: number;
|
||||||
|
shortTitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
title: string;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
[];
|
||||||
|
|
||||||
|
const getAllDaysInMonthInMonthView = (month: number, year: number): IMonthChild[] => {
|
||||||
const day: GetAllDaysInMonthInMonthViewType[] = [];
|
const day: GetAllDaysInMonthInMonthViewType[] = [];
|
||||||
const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year);
|
const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year);
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
@ -45,7 +73,7 @@ const getAllDaysInMonthInMonthView = (month: number, year: number) => {
|
|||||||
return day;
|
return day;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => {
|
const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number): IMonthBlock => {
|
||||||
const currentMonth: number = month;
|
const currentMonth: number = month;
|
||||||
const currentYear: number = year;
|
const currentYear: number = year;
|
||||||
|
|
||||||
@ -162,7 +190,11 @@ export const getNumberOfDaysBetweenTwoDatesInMonth = (startDate: Date, endDate:
|
|||||||
return daysDifference;
|
return daysDifference;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, itemData: any) => {
|
// calc item scroll position and width
|
||||||
|
export const getMonthChartItemPositionWidthInMonth = (
|
||||||
|
chartData: ChartDataType,
|
||||||
|
itemData: IGanttBlock
|
||||||
|
) => {
|
||||||
let scrollPosition: number = 0;
|
let scrollPosition: number = 0;
|
||||||
let scrollWidth: number = 0;
|
let scrollWidth: number = 0;
|
||||||
|
|
||||||
|
@ -100,8 +100,6 @@ export const generateYearChart = (yearPayload: ChartDataType, side: null | "left
|
|||||||
.map((monthData: any) => monthData.children.length)
|
.map((monthData: any) => monthData.children.length)
|
||||||
.reduce((partialSum: number, a: number) => partialSum + a, 0) * yearPayload.data.width;
|
.reduce((partialSum: number, a: number) => partialSum + a, 0) * yearPayload.data.width;
|
||||||
|
|
||||||
console.log("scrollWidth", scrollWidth);
|
|
||||||
|
|
||||||
return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth };
|
return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,20 +1,27 @@
|
|||||||
import { FC } from "react";
|
|
||||||
// next imports
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// components
|
|
||||||
import { GanttChartRoot } from "components/gantt-chart";
|
|
||||||
// ui
|
|
||||||
import { Tooltip } from "components/ui";
|
|
||||||
// hooks
|
// hooks
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
import useGanttChartIssues from "hooks/gantt-chart/issue-view";
|
import useGanttChartIssues from "hooks/gantt-chart/issue-view";
|
||||||
|
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
|
||||||
|
// components
|
||||||
|
import {
|
||||||
|
GanttChartRoot,
|
||||||
|
IssueGanttBlock,
|
||||||
|
renderIssueBlocksStructure,
|
||||||
|
} from "components/gantt-chart";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {};
|
export const IssueGanttChartView = () => {
|
||||||
|
|
||||||
export const IssueGanttChartView: FC<Props> = ({}) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { orderBy } = useIssuesView();
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const { ganttIssues, mutateGanttIssues } = useGanttChartIssues(
|
const { ganttIssues, mutateGanttIssues } = useGanttChartIssues(
|
||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
projectId as string
|
projectId as string
|
||||||
@ -31,76 +38,19 @@ export const IssueGanttChartView: FC<Props> = ({}) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// rendering issues on gantt card
|
|
||||||
const GanttBlockView = ({ data }: any) => (
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${data?.id}`}>
|
|
||||||
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm font-normal">
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-[4px] h-full"
|
|
||||||
style={{ backgroundColor: data?.state_detail?.color || "rgb(var(--color-primary-100))" }}
|
|
||||||
/>
|
|
||||||
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
|
|
||||||
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
|
|
||||||
{data?.name}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{data.infoToggle && (
|
|
||||||
<Tooltip
|
|
||||||
tooltipContent={`No due-date set, rendered according to last updated date.`}
|
|
||||||
className={`z-[999999]`}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 mx-2 w-[18px] h-[18px] overflow-hidden flex justify-center items-center">
|
|
||||||
<span className="material-symbols-rounded text-custom-text-200 text-[18px]">
|
|
||||||
info
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle gantt issue start date and target date
|
|
||||||
const handleUpdateDates = async (data: any) => {
|
|
||||||
const payload = {
|
|
||||||
id: data?.id,
|
|
||||||
start_date: data?.start_date,
|
|
||||||
target_date: data?.target_date,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const blockFormat = (blocks: any) =>
|
|
||||||
blocks && blocks.length > 0
|
|
||||||
? blocks.map((_block: any) => {
|
|
||||||
let startDate = new Date(_block.created_at);
|
|
||||||
let targetDate = new Date(_block.updated_at);
|
|
||||||
let infoToggle = true;
|
|
||||||
|
|
||||||
if (_block?.start_date && _block.target_date) {
|
|
||||||
startDate = _block?.start_date;
|
|
||||||
targetDate = _block.target_date;
|
|
||||||
infoToggle = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
start_date: new Date(startDate),
|
|
||||||
target_date: new Date(targetDate),
|
|
||||||
infoToggle: infoToggle,
|
|
||||||
data: _block,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
<GanttChartRoot
|
<GanttChartRoot
|
||||||
border={false}
|
border={false}
|
||||||
title="Issues"
|
title="Issues"
|
||||||
loaderTitle="Issues"
|
loaderTitle="Issues"
|
||||||
blocks={ganttIssues ? blockFormat(ganttIssues) : null}
|
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
|
||||||
blockUpdateHandler={handleUpdateDates}
|
blockUpdateHandler={(block, payload) =>
|
||||||
|
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
|
||||||
|
}
|
||||||
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
||||||
blockRender={(data: any) => <GanttBlockView data={data} />}
|
blockRender={(data: any) => <IssueGanttBlock issue={data as IIssue} />}
|
||||||
|
enableReorder={orderBy === "sort_order"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -18,7 +18,7 @@ export const ViewIssueLabel: React.FC<Props> = ({ issue, maxRender = 1 }) => (
|
|||||||
{issue.label_details.map((label, index) => (
|
{issue.label_details.map((label, index) => (
|
||||||
<div
|
<div
|
||||||
key={label.id}
|
key={label.id}
|
||||||
className="flex cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
||||||
>
|
>
|
||||||
<Tooltip position="top" tooltipHeading="Label" tooltipContent={label.name}>
|
<Tooltip position="top" tooltipHeading="Label" tooltipContent={label.name}>
|
||||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||||
@ -35,7 +35,7 @@ export const ViewIssueLabel: React.FC<Props> = ({ issue, maxRender = 1 }) => (
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position="top"
|
position="top"
|
||||||
tooltipHeading="Labels"
|
tooltipHeading="Labels"
|
||||||
|
@ -248,7 +248,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
await addIssueToModule(res.id, payload.module);
|
await addIssueToModule(res.id, payload.module);
|
||||||
|
|
||||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
if (issueView === "calendar") mutate(calendarFetchKey);
|
||||||
if (issueView === "gantt_chart") mutate(ganttFetchKey);
|
if (issueView === "gantt_chart")
|
||||||
|
mutate(ganttFetchKey, {
|
||||||
|
start_target_date: true,
|
||||||
|
order_by: "sort_order",
|
||||||
|
});
|
||||||
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
|
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||||
if (groupedIssues) mutateMyIssues();
|
if (groupedIssues) mutateMyIssues();
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||||
open
|
open
|
||||||
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
||||||
: "text-custom-sidebar-text-200"
|
: "text-custom-sidebar-text-200"
|
||||||
|
@ -93,13 +93,14 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const completedSubIssues = subIssuesResponse
|
const completedSubIssue = subIssuesResponse?.state_distribution.completed ?? 0;
|
||||||
? subIssuesResponse.state_distribution.completed +
|
const cancelledSubIssue = subIssuesResponse?.state_distribution.cancelled ?? 0;
|
||||||
subIssuesResponse.state_distribution.cancelled
|
|
||||||
: 0;
|
const totalCompletedSubIssues = completedSubIssue + cancelledSubIssue;
|
||||||
|
|
||||||
const totalSubIssues = subIssuesResponse ? subIssuesResponse.sub_issues.length : 0;
|
const totalSubIssues = subIssuesResponse ? subIssuesResponse.sub_issues.length : 0;
|
||||||
|
|
||||||
const completionPercentage = (completedSubIssues / totalSubIssues) * 100;
|
const completionPercentage = (totalCompletedSubIssues / totalSubIssues) * 100;
|
||||||
|
|
||||||
const isNotAllowed = memberRole.isGuest || memberRole.isViewer || disabled;
|
const isNotAllowed = memberRole.isGuest || memberRole.isViewer || disabled;
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import useSWR from "swr";
|
|||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
// ui
|
// ui
|
||||||
import { AssigneesList, Avatar, CustomSearchSelect, Tooltip } from "components/ui";
|
import { AssigneesList, Avatar, CustomSearchSelect, Icon, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { UserGroupIcon } from "@heroicons/react/24/outline";
|
import { UserGroupIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
@ -73,11 +73,11 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? (
|
{issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? (
|
||||||
<div className="-my-0.5 flex items-center justify-center gap-2">
|
<div className="-my-0.5 flex items-center justify-center gap-2">
|
||||||
<AssigneesList userIds={issue.assignees} length={5} showLength={true} />
|
<AssigneesList userIds={issue.assignees} length={3} showLength={true} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2 px-1.5 py-1 rounded shadow-sm border border-custom-border-300">
|
||||||
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
|
<Icon iconName="person" className="text-sm !leading-4" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -87,6 +87,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={issue.assignees}
|
value={issue.assignees}
|
||||||
|
buttonClassName="!p-0"
|
||||||
onChange={(data: any) => {
|
onChange={(data: any) => {
|
||||||
const newData = issue.assignees ?? [];
|
const newData = issue.assignees ?? [];
|
||||||
|
|
||||||
|
@ -67,14 +67,8 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
|||||||
noBorder
|
noBorder
|
||||||
? ""
|
? ""
|
||||||
: issue.priority === "urgent"
|
: issue.priority === "urgent"
|
||||||
? "border-red-500/20 bg-red-500/20"
|
? "border-red-500/20 bg-red-500"
|
||||||
: issue.priority === "high"
|
: "border-custom-border-300 bg-custom-background-100"
|
||||||
? "border-orange-500/20 bg-orange-500/20"
|
|
||||||
: issue.priority === "medium"
|
|
||||||
? "border-yellow-500/20 bg-yellow-500/20"
|
|
||||||
: issue.priority === "low"
|
|
||||||
? "border-green-500/20 bg-green-500/20"
|
|
||||||
: "border-custom-border-200 bg-custom-background-80"
|
|
||||||
} items-center`}
|
} items-center`}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -87,7 +81,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
|||||||
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
||||||
`text-sm ${
|
`text-sm ${
|
||||||
issue.priority === "urgent"
|
issue.priority === "urgent"
|
||||||
? "text-red-500"
|
? "text-white"
|
||||||
: issue.priority === "high"
|
: issue.priority === "high"
|
||||||
? "text-orange-500"
|
? "text-orange-500"
|
||||||
: issue.priority === "medium"
|
: issue.priority === "medium"
|
||||||
|
@ -45,7 +45,7 @@ export const ViewStartDateSelect: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<div className="group flex-shrink-0 relative max-w-[6.5rem]">
|
<div className="group flex-shrink-0 relative max-w-[6.5rem]">
|
||||||
<CustomDatePicker
|
<CustomDatePicker
|
||||||
placeholder="Due date"
|
placeholder="Start date"
|
||||||
value={issue?.start_date}
|
value={issue?.start_date}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
partialUpdateIssue(
|
partialUpdateIssue(
|
||||||
|
@ -74,9 +74,9 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
position={tooltipPosition}
|
position={tooltipPosition}
|
||||||
>
|
>
|
||||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||||
<span className="h-4 w-4">
|
<span className="h-3.5 w-3.5">
|
||||||
{selectedOption &&
|
{selectedOption &&
|
||||||
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)}
|
getStateGroupIcon(selectedOption.group, "14", "14", selectedOption.color)}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate">{selectedOption?.name ?? "State"}</span>
|
<span className="truncate">{selectedOption?.name ?? "State"}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -131,6 +131,7 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
onOpen={() => setFetchStates(true)}
|
onOpen={() => setFetchStates(true)}
|
||||||
noChevron
|
noChevron
|
||||||
|
selfPositioned={selfPositioned}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -20,6 +20,7 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
|||||||
import type { ICurrentUserResponse, IIssueLabels, IState } from "types";
|
import type { ICurrentUserResponse, IIssueLabels, IState } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||||
|
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -52,10 +53,15 @@ export const CreateLabelModal: React.FC<Props> = ({
|
|||||||
watch,
|
watch,
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
|
setValue,
|
||||||
} = useForm<IIssueLabels>({
|
} = useForm<IIssueLabels>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) setValue("color", getRandomLabelColor());
|
||||||
|
}, [setValue, isOpen]);
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
handleClose();
|
handleClose();
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
@ -156,6 +162,7 @@ export const CreateLabelModal: React.FC<Props> = ({
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<TwitterPicker
|
<TwitterPicker
|
||||||
color={value}
|
color={value}
|
||||||
|
colors={LABEL_COLOR_OPTIONS}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
onChange(value.hex);
|
onChange(value.hex);
|
||||||
close();
|
close();
|
||||||
|
@ -22,12 +22,14 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
|||||||
import { IIssueLabels } from "types";
|
import { IIssueLabels } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||||
|
import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
labelForm: boolean;
|
labelForm: boolean;
|
||||||
setLabelForm: React.Dispatch<React.SetStateAction<boolean>>;
|
setLabelForm: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
labelToUpdate: IIssueLabels | null;
|
labelToUpdate: IIssueLabels | null;
|
||||||
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<IIssueLabels> = {
|
const defaultValues: Partial<IIssueLabels> = {
|
||||||
@ -35,167 +37,180 @@ const defaultValues: Partial<IIssueLabels> = {
|
|||||||
color: "rgb(var(--color-text-200))",
|
color: "rgb(var(--color-text-200))",
|
||||||
};
|
};
|
||||||
|
|
||||||
type Ref = HTMLDivElement;
|
export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
||||||
|
function CreateUpdateLabelInline(props, ref) {
|
||||||
|
const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props;
|
||||||
|
|
||||||
export const CreateUpdateLabelInline = forwardRef<Ref, Props>(function CreateUpdateLabelInline(
|
const router = useRouter();
|
||||||
{ labelForm, setLabelForm, isUpdating, labelToUpdate },
|
const { workspaceSlug, projectId } = router.query;
|
||||||
ref
|
|
||||||
) {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
const { user } = useUserAuth();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
register,
|
register,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
} = useForm<IIssueLabels>({
|
} = useForm<IIssueLabels>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
|
const handleClose = () => {
|
||||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
setLabelForm(false);
|
||||||
|
reset(defaultValues);
|
||||||
|
if (onClose) onClose();
|
||||||
|
};
|
||||||
|
|
||||||
await issuesService
|
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||||
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user)
|
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||||
.then((res) => {
|
|
||||||
mutate<IIssueLabels[]>(
|
await issuesService
|
||||||
PROJECT_ISSUE_LABELS(projectId as string),
|
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user)
|
||||||
(prevData) => [res, ...(prevData ?? [])],
|
.then((res) => {
|
||||||
false
|
mutate<IIssueLabels[]>(
|
||||||
|
PROJECT_ISSUE_LABELS(projectId as string),
|
||||||
|
(prevData) => [res, ...(prevData ?? [])],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
handleClose();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||||
|
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.patchIssueLabel(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
labelToUpdate?.id ?? "",
|
||||||
|
formData,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
reset(defaultValues);
|
||||||
|
mutate<IIssueLabels[]>(
|
||||||
|
PROJECT_ISSUE_LABELS(projectId as string),
|
||||||
|
(prevData) =>
|
||||||
|
prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
handleClose();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!labelForm && isUpdating) return;
|
||||||
|
|
||||||
|
reset();
|
||||||
|
}, [labelForm, isUpdating, reset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!labelToUpdate) return;
|
||||||
|
|
||||||
|
setValue(
|
||||||
|
"color",
|
||||||
|
labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000"
|
||||||
|
);
|
||||||
|
setValue("name", labelToUpdate.name);
|
||||||
|
}, [labelToUpdate, setValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (labelToUpdate) {
|
||||||
|
setValue(
|
||||||
|
"color",
|
||||||
|
labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000"
|
||||||
);
|
);
|
||||||
reset(defaultValues);
|
return;
|
||||||
setLabelForm(false);
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
|
setValue("color", getRandomLabelColor());
|
||||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
}, [labelToUpdate, setValue]);
|
||||||
|
|
||||||
await issuesService
|
return (
|
||||||
.patchIssueLabel(
|
<div
|
||||||
workspaceSlug as string,
|
className={`flex scroll-m-8 items-center gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 ${
|
||||||
projectId as string,
|
labelForm ? "" : "hidden"
|
||||||
labelToUpdate?.id ?? "",
|
}`}
|
||||||
formData,
|
ref={ref}
|
||||||
user
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
reset(defaultValues);
|
|
||||||
mutate<IIssueLabels[]>(
|
|
||||||
PROJECT_ISSUE_LABELS(projectId as string),
|
|
||||||
(prevData) =>
|
|
||||||
prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
setLabelForm(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!labelForm && isUpdating) return;
|
|
||||||
|
|
||||||
reset();
|
|
||||||
}, [labelForm, isUpdating, reset]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!labelToUpdate) return;
|
|
||||||
|
|
||||||
setValue(
|
|
||||||
"color",
|
|
||||||
labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000"
|
|
||||||
);
|
|
||||||
setValue("name", labelToUpdate.name);
|
|
||||||
}, [labelToUpdate, setValue]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`flex scroll-m-8 items-center gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 ${
|
|
||||||
labelForm ? "" : "hidden"
|
|
||||||
}`}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<Popover className="relative z-10 flex h-full w-full items-center justify-center">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Popover.Button
|
|
||||||
className={`group inline-flex items-center text-base font-medium focus:outline-none ${
|
|
||||||
open ? "text-custom-text-100" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="h-5 w-5 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: watch("color"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ChevronDownIcon
|
|
||||||
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
|
|
||||||
open ? "text-gray-600" : "text-gray-400"
|
|
||||||
}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Popover.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
|
|
||||||
<Controller
|
|
||||||
name="color"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col justify-center">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
id="labelName"
|
|
||||||
name="name"
|
|
||||||
register={register}
|
|
||||||
placeholder="Label title"
|
|
||||||
validations={{
|
|
||||||
required: "Label title is required",
|
|
||||||
}}
|
|
||||||
error={errors.name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<SecondaryButton
|
|
||||||
onClick={() => {
|
|
||||||
reset();
|
|
||||||
setLabelForm(false);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Cancel
|
<div className="flex-shrink-0">
|
||||||
</SecondaryButton>
|
<Popover className="relative z-10 flex h-full w-full items-center justify-center">
|
||||||
{isUpdating ? (
|
{({ open }) => (
|
||||||
<PrimaryButton onClick={handleSubmit(handleLabelUpdate)} loading={isSubmitting}>
|
<>
|
||||||
{isSubmitting ? "Updating" : "Update"}
|
<Popover.Button
|
||||||
</PrimaryButton>
|
className={`group inline-flex items-center text-base font-medium focus:outline-none ${
|
||||||
) : (
|
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
<PrimaryButton onClick={handleSubmit(handleLabelCreate)} loading={isSubmitting}>
|
}`}
|
||||||
{isSubmitting ? "Adding" : "Add"}
|
>
|
||||||
</PrimaryButton>
|
<span
|
||||||
)}
|
className="h-5 w-5 rounded"
|
||||||
</div>
|
style={{
|
||||||
);
|
backgroundColor: watch("color"),
|
||||||
});
|
}}
|
||||||
|
/>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
|
||||||
|
open ? "text-gray-600" : "text-gray-400"
|
||||||
|
}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Popover.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 translate-y-1"
|
||||||
|
enterTo="opacity-100 translate-y-0"
|
||||||
|
leave="transition ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
|
leaveTo="opacity-0 translate-y-1"
|
||||||
|
>
|
||||||
|
<Popover.Panel className="absolute top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
|
||||||
|
<Controller
|
||||||
|
name="color"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<TwitterPicker
|
||||||
|
colors={LABEL_COLOR_OPTIONS}
|
||||||
|
color={value}
|
||||||
|
onChange={(value) => onChange(value.hex)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col justify-center">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="labelName"
|
||||||
|
name="name"
|
||||||
|
register={register}
|
||||||
|
placeholder="Label title"
|
||||||
|
validations={{
|
||||||
|
required: "Label title is required",
|
||||||
|
}}
|
||||||
|
error={errors.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SecondaryButton onClick={() => handleClose()}>Cancel</SecondaryButton>
|
||||||
|
{isUpdating ? (
|
||||||
|
<PrimaryButton onClick={handleSubmit(handleLabelUpdate)} loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Updating" : "Update"}
|
||||||
|
</PrimaryButton>
|
||||||
|
) : (
|
||||||
|
<PrimaryButton onClick={handleSubmit(handleLabelCreate)} loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Adding" : "Add"}
|
||||||
|
</PrimaryButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// next imports
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// components
|
|
||||||
import { GanttChartRoot } from "components/gantt-chart";
|
|
||||||
// ui
|
|
||||||
import { Tooltip } from "components/ui";
|
|
||||||
// hooks
|
// hooks
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view";
|
import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view";
|
||||||
|
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
|
||||||
|
// components
|
||||||
|
import {
|
||||||
|
GanttChartRoot,
|
||||||
|
IssueGanttBlock,
|
||||||
|
renderIssueBlocksStructure,
|
||||||
|
} from "components/gantt-chart";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {};
|
type Props = {};
|
||||||
|
|
||||||
@ -15,6 +22,10 @@ export const ModuleIssuesGanttChartView: FC<Props> = ({}) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const { orderBy } = useIssuesView();
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const { ganttIssues, mutateGanttIssues } = useGanttChartModuleIssues(
|
const { ganttIssues, mutateGanttIssues } = useGanttChartModuleIssues(
|
||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
projectId as string,
|
projectId as string,
|
||||||
@ -32,77 +43,18 @@ export const ModuleIssuesGanttChartView: FC<Props> = ({}) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// rendering issues on gantt card
|
|
||||||
const GanttBlockView = ({ data }: any) => (
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${data?.id}`}>
|
|
||||||
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm">
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-[4px] h-full"
|
|
||||||
style={{ backgroundColor: data?.state_detail?.color || "rgb(var(--color-primary-100))" }}
|
|
||||||
/>
|
|
||||||
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
|
|
||||||
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
|
|
||||||
{data?.name}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{data.infoToggle && (
|
|
||||||
<Tooltip
|
|
||||||
tooltipContent={`No due-date set, rendered according to last updated date.`}
|
|
||||||
className={`z-[999999]`}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 mx-2 w-[18px] h-[18px] overflow-hidden flex justify-center items-center">
|
|
||||||
<span className="material-symbols-rounded text-custom-text-200 text-[18px]">
|
|
||||||
info
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle gantt issue start date and target date
|
|
||||||
const handleUpdateDates = async (data: any) => {
|
|
||||||
const payload = {
|
|
||||||
id: data?.id,
|
|
||||||
start_date: data?.start_date,
|
|
||||||
target_date: data?.target_date,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("payload", payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const blockFormat = (blocks: any) =>
|
|
||||||
blocks && blocks.length > 0
|
|
||||||
? blocks.map((_block: any) => {
|
|
||||||
let startDate = new Date(_block.created_at);
|
|
||||||
let targetDate = new Date(_block.updated_at);
|
|
||||||
let infoToggle = true;
|
|
||||||
|
|
||||||
if (_block?.start_date && _block.target_date) {
|
|
||||||
startDate = _block?.start_date;
|
|
||||||
targetDate = _block.target_date;
|
|
||||||
infoToggle = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
start_date: new Date(startDate),
|
|
||||||
target_date: new Date(targetDate),
|
|
||||||
infoToggle: infoToggle,
|
|
||||||
data: _block,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full p-3">
|
<div className="w-full h-full p-3">
|
||||||
<GanttChartRoot
|
<GanttChartRoot
|
||||||
title="Modules"
|
title="Modules"
|
||||||
loaderTitle="Modules"
|
loaderTitle="Modules"
|
||||||
blocks={ganttIssues ? blockFormat(ganttIssues) : null}
|
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
|
||||||
blockUpdateHandler={handleUpdateDates}
|
blockUpdateHandler={(block, payload) =>
|
||||||
|
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
|
||||||
|
}
|
||||||
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
||||||
blockRender={(data: any) => <GanttBlockView data={data} />}
|
blockRender={(data: any) => <IssueGanttBlock issue={data as IIssue} />}
|
||||||
|
enableReorder={orderBy === "sort_order"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// next imports
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { KeyedMutator } from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import modulesService from "services/modules.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
// components
|
// components
|
||||||
import { GanttChartRoot } from "components/gantt-chart";
|
import { GanttChartRoot, IBlockUpdateData, ModuleGanttBlock } from "components/gantt-chart";
|
||||||
// ui
|
|
||||||
import { Tooltip } from "components/ui";
|
|
||||||
// types
|
// types
|
||||||
import { IModule } from "types";
|
import { IModule } from "types";
|
||||||
// constants
|
// constants
|
||||||
@ -13,11 +17,14 @@ import { MODULE_STATUS } from "constants/module";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
modules: IModule[];
|
modules: IModule[];
|
||||||
|
mutateModules: KeyedMutator<IModule[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModulesListGanttChartView: FC<Props> = ({ modules }) => {
|
export const ModulesListGanttChartView: FC<Props> = ({ modules, mutateModules }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
// rendering issues on gantt sidebar
|
// rendering issues on gantt sidebar
|
||||||
const GanttSidebarBlockView = ({ data }: any) => (
|
const GanttSidebarBlockView = ({ data }: any) => (
|
||||||
@ -32,42 +39,52 @@ export const ModulesListGanttChartView: FC<Props> = ({ modules }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// rendering issues on gantt card
|
const handleModuleUpdate = (module: IModule, payload: IBlockUpdateData) => {
|
||||||
const GanttBlockView = ({ data }: { data: IModule }) => (
|
if (!workspaceSlug || !user) return;
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/modules/${data?.id}`}>
|
|
||||||
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm">
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-[4px] h-full"
|
|
||||||
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === data.status)?.color }}
|
|
||||||
/>
|
|
||||||
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
|
|
||||||
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
|
|
||||||
{data?.name}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle gantt issue start date and target date
|
mutateModules((prevData) => {
|
||||||
const handleUpdateDates = async (data: any) => {
|
if (!prevData) return prevData;
|
||||||
const payload = {
|
|
||||||
id: data?.id,
|
const newList = prevData.map((p) => ({
|
||||||
start_date: data?.start_date,
|
...p,
|
||||||
target_date: data?.target_date,
|
...(p.id === module.id
|
||||||
};
|
? {
|
||||||
|
start_date: payload.start_date ? payload.start_date : p.start_date,
|
||||||
|
target_date: payload.target_date ? payload.target_date : p.target_date,
|
||||||
|
sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (payload.sort_order) {
|
||||||
|
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
|
||||||
|
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newList;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
const newPayload: any = { ...payload };
|
||||||
|
|
||||||
|
if (newPayload.sort_order && payload.sort_order)
|
||||||
|
newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||||
|
|
||||||
|
modulesService
|
||||||
|
.patchModule(workspaceSlug.toString(), module.project, module.id, newPayload, user)
|
||||||
|
.finally(() => mutateModules());
|
||||||
};
|
};
|
||||||
|
|
||||||
const blockFormat = (blocks: any) =>
|
const blockFormat = (blocks: IModule[]) =>
|
||||||
blocks && blocks.length > 0
|
blocks && blocks.length > 0
|
||||||
? blocks.map((_block: any) => {
|
? blocks
|
||||||
if (_block?.start_date && _block.target_date) console.log("_block", _block);
|
.filter((b) => b.start_date && b.target_date)
|
||||||
return {
|
.map((block) => ({
|
||||||
start_date: new Date(_block.created_at),
|
data: block,
|
||||||
target_date: new Date(_block.updated_at),
|
id: block.id,
|
||||||
data: _block,
|
sort_order: block.sort_order,
|
||||||
};
|
start_date: new Date(block.start_date ?? ""),
|
||||||
})
|
target_date: new Date(block.target_date ?? ""),
|
||||||
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -76,9 +93,9 @@ export const ModulesListGanttChartView: FC<Props> = ({ modules }) => {
|
|||||||
title="Modules"
|
title="Modules"
|
||||||
loaderTitle="Modules"
|
loaderTitle="Modules"
|
||||||
blocks={modules ? blockFormat(modules) : null}
|
blocks={modules ? blockFormat(modules) : null}
|
||||||
blockUpdateHandler={handleUpdateDates}
|
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
|
||||||
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
||||||
blockRender={(data: any) => <GanttBlockView data={data} />}
|
blockRender={(data: any) => <ModuleGanttBlock module={data as IModule} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -84,7 +84,7 @@ export const NotificationPopover = () => {
|
|||||||
disabled={!store?.theme?.sidebarCollapsed}
|
disabled={!store?.theme?.sidebarCollapsed}
|
||||||
>
|
>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
className={`relative group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||||
isActive
|
isActive
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||||
@ -93,9 +93,13 @@ export const NotificationPopover = () => {
|
|||||||
<NotificationsOutlined fontSize="small" />
|
<NotificationsOutlined fontSize="small" />
|
||||||
{store?.theme?.sidebarCollapsed ? null : <span>Notifications</span>}
|
{store?.theme?.sidebarCollapsed ? null : <span>Notifications</span>}
|
||||||
{totalNotificationCount && totalNotificationCount > 0 ? (
|
{totalNotificationCount && totalNotificationCount > 0 ? (
|
||||||
<span className="ml-auto bg-custom-primary-300 rounded-full text-xs text-white px-1.5">
|
store?.theme?.sidebarCollapsed ? (
|
||||||
{getNumberCount(totalNotificationCount)}
|
<span className="absolute right-3.5 top-2 h-2 w-2 bg-custom-primary-300 rounded-full" />
|
||||||
</span>
|
) : (
|
||||||
|
<span className="ml-auto bg-custom-primary-300 rounded-full text-xs text-white px-1.5">
|
||||||
|
{getNumberCount(totalNotificationCount)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -108,7 +112,7 @@ export const NotificationPopover = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 top-0 md:w-[36rem] w-[20rem] h-[50vh] border border-custom-border-300 shadow-lg rounded-xl">
|
<Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 -top-44 md:w-[36rem] w-[20rem] h-[75vh] border border-custom-border-300 shadow-lg rounded-xl">
|
||||||
<NotificationHeader
|
<NotificationHeader
|
||||||
notificationCount={notificationCount}
|
notificationCount={notificationCount}
|
||||||
notificationMutate={notificationMutate}
|
notificationMutate={notificationMutate}
|
||||||
|
@ -146,7 +146,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||||
open
|
open
|
||||||
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
||||||
: "text-custom-sidebar-text-200"
|
: "text-custom-sidebar-text-200"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, FC } from "react";
|
import React, { useState, FC, useRef, useEffect } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
@ -6,7 +6,7 @@ import { mutate } from "swr";
|
|||||||
// react-beautiful-dnd
|
// react-beautiful-dnd
|
||||||
import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Disclosure } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useTheme from "hooks/use-theme";
|
import useTheme from "hooks/use-theme";
|
||||||
@ -36,6 +36,10 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
const [projectToDelete, setProjectToDelete] = useState<IProject | null>(null);
|
const [projectToDelete, setProjectToDelete] = useState<IProject | null>(null);
|
||||||
|
|
||||||
// router
|
// router
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -99,9 +103,7 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
? (projectsList[destination.index + 1].sort_order as number)
|
? (projectsList[destination.index + 1].sort_order as number)
|
||||||
: (projectsList[destination.index - 1].sort_order as number);
|
: (projectsList[destination.index - 1].sort_order as number);
|
||||||
|
|
||||||
updatedSortOrder = Math.round(
|
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
||||||
(destinationSortingOrder + relativeDestinationSortingOrder) / 2
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mutate<IProject[]>(
|
mutate<IProject[]>(
|
||||||
@ -126,6 +128,27 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const scrollTop = containerRef.current.scrollTop;
|
||||||
|
setIsScrolled(scrollTop > 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentContainerRef = containerRef.current;
|
||||||
|
|
||||||
|
if (currentContainerRef) {
|
||||||
|
currentContainerRef.addEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentContainerRef) {
|
||||||
|
currentContainerRef.removeEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteProjectModal
|
<DeleteProjectModal
|
||||||
@ -134,7 +157,12 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
data={projectToDelete}
|
data={projectToDelete}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<div className="h-full overflow-y-auto px-4 space-y-3 pt-3 border-t border-custom-sidebar-border-300">
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`h-full overflow-y-auto px-4 space-y-3 pt-3 ${
|
||||||
|
isScrolled ? "border-t border-custom-sidebar-border-300" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<Droppable droppableId="favorite-projects">
|
<Droppable droppableId="favorite-projects">
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
@ -147,7 +175,7 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap"
|
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-400 text-left hover:bg-custom-sidebar-background-80 rounded w-full whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Favorites
|
Favorites
|
||||||
<Icon
|
<Icon
|
||||||
@ -199,37 +227,57 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
{!store?.theme?.sidebarCollapsed && (
|
{!store?.theme?.sidebarCollapsed && (
|
||||||
<Disclosure.Button
|
<div className="group flex justify-between items-center text-xs px-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 w-full">
|
||||||
as="button"
|
<Disclosure.Button
|
||||||
type="button"
|
as="button"
|
||||||
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap"
|
type="button"
|
||||||
>
|
className="flex items-center gap-1 font-semibold text-left whitespace-nowrap"
|
||||||
Projects
|
>
|
||||||
<Icon
|
Projects
|
||||||
iconName={open ? "arrow_drop_down" : "arrow_right"}
|
<Icon
|
||||||
className="group-hover:opacity-100 opacity-0 !text-lg"
|
iconName={open ? "arrow_drop_down" : "arrow_right"}
|
||||||
/>
|
className="group-hover:opacity-100 opacity-0 !text-lg"
|
||||||
</Disclosure.Button>
|
/>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<button
|
||||||
|
className="group-hover:opacity-100 opacity-0"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "p" });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="add" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Disclosure.Panel as="div" className="space-y-2">
|
<Transition
|
||||||
{orderedJoinedProjects.map((project, index) => (
|
enter="transition duration-100 ease-out"
|
||||||
<Draggable key={project.id} draggableId={project.id} index={index}>
|
enterFrom="transform scale-95 opacity-0"
|
||||||
{(provided, snapshot) => (
|
enterTo="transform scale-100 opacity-100"
|
||||||
<div ref={provided.innerRef} {...provided.draggableProps}>
|
leave="transition duration-75 ease-out"
|
||||||
<SingleSidebarProject
|
leaveFrom="transform scale-100 opacity-100"
|
||||||
key={project.id}
|
leaveTo="transform scale-95 opacity-0"
|
||||||
project={project}
|
>
|
||||||
sidebarCollapse={store?.theme?.sidebarCollapsed}
|
<Disclosure.Panel as="div" className="space-y-2">
|
||||||
provided={provided}
|
{orderedJoinedProjects.map((project, index) => (
|
||||||
snapshot={snapshot}
|
<Draggable key={project.id} draggableId={project.id} index={index}>
|
||||||
handleDeleteProject={() => handleDeleteProject(project)}
|
{(provided, snapshot) => (
|
||||||
handleCopyText={() => handleCopyText(project.id)}
|
<div ref={provided.innerRef} {...provided.draggableProps}>
|
||||||
/>
|
<SingleSidebarProject
|
||||||
</div>
|
key={project.id}
|
||||||
)}
|
project={project}
|
||||||
</Draggable>
|
sidebarCollapse={store?.theme?.sidebarCollapsed}
|
||||||
))}
|
provided={provided}
|
||||||
</Disclosure.Panel>
|
snapshot={snapshot}
|
||||||
|
handleDeleteProject={() => handleDeleteProject(project)}
|
||||||
|
handleCopyText={() => handleCopyText(project.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Transition>
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -239,43 +287,7 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
{otherProjects && otherProjects.length > 0 && (
|
|
||||||
<Disclosure
|
|
||||||
as="div"
|
|
||||||
className="flex flex-col space-y-2"
|
|
||||||
defaultOpen={projectId && otherProjects.find((p) => p.id === projectId) ? true : false}
|
|
||||||
>
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
{!store?.theme?.sidebarCollapsed && (
|
|
||||||
<Disclosure.Button
|
|
||||||
as="button"
|
|
||||||
type="button"
|
|
||||||
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Other Projects
|
|
||||||
<Icon
|
|
||||||
iconName={open ? "arrow_drop_down" : "arrow_right"}
|
|
||||||
className="group-hover:opacity-100 opacity-0 !text-lg"
|
|
||||||
/>
|
|
||||||
</Disclosure.Button>
|
|
||||||
)}
|
|
||||||
<Disclosure.Panel as="div" className="space-y-2">
|
|
||||||
{otherProjects?.map((project, index) => (
|
|
||||||
<SingleSidebarProject
|
|
||||||
key={project.id}
|
|
||||||
project={project}
|
|
||||||
sidebarCollapse={store?.theme?.sidebarCollapsed}
|
|
||||||
handleDeleteProject={() => handleDeleteProject(project)}
|
|
||||||
handleCopyText={() => handleCopyText(project.id)}
|
|
||||||
shortContextMenu
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
)}
|
|
||||||
{allProjects && allProjects.length === 0 && (
|
{allProjects && allProjects.length === 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -210,7 +210,7 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
|
|||||||
{project.is_favorite ? (
|
{project.is_favorite ? (
|
||||||
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
|
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<StarIcon className="h-4 w-4" />
|
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||||
<span>Remove from favorites</span>
|
<span>Remove from favorites</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
|
@ -12,7 +12,7 @@ import projectService from "services/project.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Tooltip } from "components/ui";
|
import { CustomMenu, Icon, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { EllipsisVerticalIcon, LinkIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { EllipsisVerticalIcon, LinkIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
import {
|
import {
|
||||||
@ -90,6 +90,8 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const isAdmin = project.member_role === 20;
|
||||||
|
|
||||||
const handleAddToFavorites = () => {
|
const handleAddToFavorites = () => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
@ -152,7 +154,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 ${
|
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 text-custom-sidebar-text-400 ${
|
||||||
sidebarCollapse ? "" : "group-hover:!flex"
|
sidebarCollapse ? "" : "group-hover:!flex"
|
||||||
} ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""}`}
|
} ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""}`}
|
||||||
{...provided?.dragHandleProps}
|
{...provided?.dragHandleProps}
|
||||||
@ -204,15 +206,19 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
fontSize="small"
|
fontSize="small"
|
||||||
className={`flex-shrink-0 ${
|
className={`flex-shrink-0 ${
|
||||||
open ? "rotate-180" : ""
|
open ? "rotate-180" : ""
|
||||||
} !hidden group-hover:!block text-custom-sidebar-text-200 duration-300`}
|
} !hidden group-hover:!block text-custom-sidebar-text-400 duration-300`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{!sidebarCollapse && (
|
{!sidebarCollapse && (
|
||||||
<CustomMenu className="hidden group-hover:block flex-shrink-0" ellipsis>
|
<CustomMenu
|
||||||
{!shortContextMenu && (
|
className="hidden group-hover:block flex-shrink-0"
|
||||||
|
buttonClassName="!text-custom-sidebar-text-400 hover:text-custom-sidebar-text-400"
|
||||||
|
ellipsis
|
||||||
|
>
|
||||||
|
{!shortContextMenu && isAdmin && (
|
||||||
<CustomMenu.MenuItem onClick={handleDeleteProject}>
|
<CustomMenu.MenuItem onClick={handleDeleteProject}>
|
||||||
<span className="flex items-center justify-start gap-2 ">
|
<span className="flex items-center justify-start gap-2 ">
|
||||||
<TrashIcon className="h-4 w-4" />
|
<TrashIcon className="h-4 w-4" />
|
||||||
@ -231,7 +237,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
{project.is_favorite && (
|
{project.is_favorite && (
|
||||||
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
|
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<StarIcon className="h-4 w-4" />
|
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||||
<span>Remove from favorites</span>
|
<span>Remove from favorites</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
@ -254,6 +260,14 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<Icon iconName="settings" className="!text-base !leading-4" />
|
||||||
|
<span>Settings</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,8 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// component
|
||||||
|
import { Icon } from "components/ui";
|
||||||
// services
|
// services
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// icons
|
// icons
|
||||||
@ -23,12 +25,14 @@ type AvatarProps = {
|
|||||||
export const Avatar: React.FC<AvatarProps> = ({
|
export const Avatar: React.FC<AvatarProps> = ({
|
||||||
user,
|
user,
|
||||||
index,
|
index,
|
||||||
height = "20px",
|
height = "24px",
|
||||||
width = "20px",
|
width = "24px",
|
||||||
fontSize = "12px",
|
fontSize = "12px",
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
className={`relative rounded-full ${index && index !== 0 ? "-ml-3.5" : ""}`}
|
className={`relative rounded border-[0.5px] ${
|
||||||
|
index && index !== 0 ? "-ml-3.5 border-custom-border-200" : "border-transparent"
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
@ -36,8 +40,8 @@ export const Avatar: React.FC<AvatarProps> = ({
|
|||||||
>
|
>
|
||||||
{user && user.avatar && user.avatar !== "" ? (
|
{user && user.avatar && user.avatar !== "" ? (
|
||||||
<div
|
<div
|
||||||
className={`rounded-full border-2 ${
|
className={`rounded border-[0.5px] ${
|
||||||
index ? "border-custom-border-200 bg-custom-background-80" : "border-transparent"
|
index ? "border-custom-border-200 bg-custom-background-100" : "border-transparent"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
height: height,
|
height: height,
|
||||||
@ -46,13 +50,13 @@ export const Avatar: React.FC<AvatarProps> = ({
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
|
className="absolute top-0 left-0 h-full w-full object-cover rounded"
|
||||||
alt={user.display_name}
|
alt={user.display_name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="grid place-items-center rounded-full border-2 border-custom-border-200 bg-gray-700 text-xs capitalize text-white"
|
className="grid place-items-center text-xs capitalize text-white rounded bg-gray-700 border-[0.5px] border-custom-border-200"
|
||||||
style={{
|
style={{
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
@ -75,7 +79,7 @@ type AsigneesListProps = {
|
|||||||
export const AssigneesList: React.FC<AsigneesListProps> = ({
|
export const AssigneesList: React.FC<AsigneesListProps> = ({
|
||||||
users,
|
users,
|
||||||
userIds,
|
userIds,
|
||||||
length = 5,
|
length = 3,
|
||||||
showLength = true,
|
showLength = true,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -88,7 +92,7 @@ export const AssigneesList: React.FC<AsigneesListProps> = ({
|
|||||||
|
|
||||||
if ((users && users.length === 0) || (userIds && userIds.length === 0))
|
if ((users && users.length === 0) || (userIds && userIds.length === 0))
|
||||||
return (
|
return (
|
||||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-custom-background-80">
|
<div className="h-5 w-5 rounded border-[0.5px] border-custom-border-200 bg-custom-background-80">
|
||||||
<Image src={User} height="100%" width="100%" className="rounded-full" alt="No user" />
|
<Image src={User} height="100%" width="100%" className="rounded-full" alt="No user" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -100,7 +104,14 @@ export const AssigneesList: React.FC<AsigneesListProps> = ({
|
|||||||
{users.slice(0, length).map((user, index) => (
|
{users.slice(0, length).map((user, index) => (
|
||||||
<Avatar key={user?.id} user={user} index={index} />
|
<Avatar key={user?.id} user={user} index={index} />
|
||||||
))}
|
))}
|
||||||
{users.length > length ? <span>+{users.length - length}</span> : null}
|
{users.length > length ? (
|
||||||
|
<div className="-ml-3.5 relative h-6 w-6 rounded">
|
||||||
|
<div className="grid place-items-center rounded bg-custom-background-80 text-xs capitalize h-6 w-6 text-custom-text-200 border-[0.5px] border-custom-border-300">
|
||||||
|
<Icon iconName="add" className="text-xs !leading-3 -mr-0.5" />
|
||||||
|
{users.length - length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{userIds && (
|
{userIds && (
|
||||||
@ -112,7 +123,12 @@ export const AssigneesList: React.FC<AsigneesListProps> = ({
|
|||||||
})}
|
})}
|
||||||
{showLength ? (
|
{showLength ? (
|
||||||
userIds.length > length ? (
|
userIds.length > length ? (
|
||||||
<span>+{userIds.length - length}</span>
|
<div className="-ml-3.5 relative h-6 w-6 rounded">
|
||||||
|
<div className="flex items-center rounded bg-custom-background-80 text-xs capitalize h-6 w-6 text-custom-text-200 border-[0.5px] border-custom-border-300">
|
||||||
|
<Icon iconName="add" className="text-xs !leading-3 -mr-0.5" />
|
||||||
|
{userIds.length - length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : null
|
) : null
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// next imports
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// components
|
|
||||||
import { GanttChartRoot } from "components/gantt-chart";
|
|
||||||
// ui
|
|
||||||
import { Tooltip } from "components/ui";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useGanttChartViewIssues from "hooks/gantt-chart/view-issues-view";
|
import useGanttChartViewIssues from "hooks/gantt-chart/view-issues-view";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
|
||||||
|
// components
|
||||||
|
import {
|
||||||
|
GanttChartRoot,
|
||||||
|
IssueGanttBlock,
|
||||||
|
renderIssueBlocksStructure,
|
||||||
|
} from "components/gantt-chart";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {};
|
type Props = {};
|
||||||
|
|
||||||
@ -15,6 +21,8 @@ export const ViewIssuesGanttChartView: FC<Props> = ({}) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, viewId } = router.query;
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const { ganttIssues, mutateGanttIssues } = useGanttChartViewIssues(
|
const { ganttIssues, mutateGanttIssues } = useGanttChartViewIssues(
|
||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
projectId as string,
|
projectId as string,
|
||||||
@ -32,77 +40,17 @@ export const ViewIssuesGanttChartView: FC<Props> = ({}) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// rendering issues on gantt card
|
|
||||||
const GanttBlockView = ({ data }: any) => (
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${data?.id}`}>
|
|
||||||
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm">
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-[4px] h-full"
|
|
||||||
style={{ backgroundColor: data?.state_detail?.color || "rgb(var(--color-primary-100))" }}
|
|
||||||
/>
|
|
||||||
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
|
|
||||||
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
|
|
||||||
{data?.name}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{data.infoToggle && (
|
|
||||||
<Tooltip
|
|
||||||
tooltipContent={`No due-date set, rendered according to last updated date.`}
|
|
||||||
className={`z-[999999]`}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 mx-2 w-[18px] h-[18px] overflow-hidden flex justify-center items-center">
|
|
||||||
<span className="material-symbols-rounded text-custom-text-200 text-[18px]">
|
|
||||||
info
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle gantt issue start date and target date
|
|
||||||
const handleUpdateDates = async (data: any) => {
|
|
||||||
const payload = {
|
|
||||||
id: data?.id,
|
|
||||||
start_date: data?.start_date,
|
|
||||||
target_date: data?.target_date,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("payload", payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const blockFormat = (blocks: any) =>
|
|
||||||
blocks && blocks.length > 0
|
|
||||||
? blocks.map((_block: any) => {
|
|
||||||
let startDate = new Date(_block.created_at);
|
|
||||||
let targetDate = new Date(_block.updated_at);
|
|
||||||
let infoToggle = true;
|
|
||||||
|
|
||||||
if (_block?.start_date && _block.target_date) {
|
|
||||||
startDate = _block?.start_date;
|
|
||||||
targetDate = _block.target_date;
|
|
||||||
infoToggle = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
start_date: new Date(startDate),
|
|
||||||
target_date: new Date(targetDate),
|
|
||||||
infoToggle: infoToggle,
|
|
||||||
data: _block,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full p-3">
|
<div className="w-full h-full p-3">
|
||||||
<GanttChartRoot
|
<GanttChartRoot
|
||||||
title="Issue Views"
|
title="Issue Views"
|
||||||
loaderTitle="Issue Views"
|
loaderTitle="Issue Views"
|
||||||
blocks={ganttIssues ? blockFormat(ganttIssues) : null}
|
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
|
||||||
blockUpdateHandler={handleUpdateDates}
|
blockUpdateHandler={(block, payload) =>
|
||||||
|
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
|
||||||
|
}
|
||||||
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
|
||||||
blockRender={(data: any) => <GanttBlockView data={data} />}
|
blockRender={(data: any) => <IssueGanttBlock issue={data as IIssue} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -50,8 +50,10 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
|
|||||||
const canDelete = confirmWorkspaceName === data?.name && confirmDeleteMyWorkspace;
|
const canDelete = confirmWorkspaceName === data?.name && confirmDeleteMyWorkspace;
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
setConfirmWorkspaceName("");
|
||||||
|
setConfirmDeleteMyWorkspace(false);
|
||||||
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleDeletion = async () => {
|
||||||
|
@ -9,3 +9,4 @@ export * from "./issues-stats";
|
|||||||
export * from "./settings-header";
|
export * from "./settings-header";
|
||||||
export * from "./sidebar-dropdown";
|
export * from "./sidebar-dropdown";
|
||||||
export * from "./sidebar-menu";
|
export * from "./sidebar-menu";
|
||||||
|
export * from "./sidebar-quick-action";
|
||||||
|
@ -146,13 +146,12 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
className="fixed left-4 z-20 mt-1 flex flex-col w-full max-w-[17rem] origin-top-left rounded-md
|
className="fixed left-4 z-20 mt-1 flex flex-col w-full max-w-[17rem] origin-top-left rounded-md
|
||||||
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 shadow-lg outline-none"
|
border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 shadow-lg outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-start justify-start gap-3 p-3">
|
<div className="flex flex-col items-start justify-start gap-3 p-3">
|
||||||
<div className="text-sm text-custom-sidebar-text-200">{user?.display_name}</div>
|
<span className="text-sm font-medium text-custom-sidebar-text-200">Workspace</span>
|
||||||
<span className="text-sm font-semibold text-custom-sidebar-text-200">Workspace</span>
|
|
||||||
{workspaces ? (
|
{workspaces ? (
|
||||||
<div className="flex h-full w-full flex-col items-start justify-start gap-3.5">
|
<div className="flex h-full w-full flex-col items-start justify-start gap-1.5">
|
||||||
{workspaces.length > 0 ? (
|
{workspaces.length > 0 ? (
|
||||||
workspaces.map((workspace) => (
|
workspaces.map((workspace) => (
|
||||||
<Menu.Item key={workspace.id}>
|
<Menu.Item key={workspace.id}>
|
||||||
@ -160,7 +159,7 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleWorkspaceNavigation(workspace)}
|
onClick={() => handleWorkspaceNavigation(workspace)}
|
||||||
className="flex w-full items-center justify-between gap-1 rounded-md text-sm text-custom-sidebar-text-100"
|
className="flex w-full items-center justify-between gap-1 p-1 rounded-md text-sm text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-start gap-2.5">
|
<div className="flex items-center justify-start gap-2.5">
|
||||||
<span className="relative flex h-6 w-6 items-center justify-center rounded bg-gray-700 p-2 text-xs uppercase text-white">
|
<span className="relative flex h-6 w-6 items-center justify-center rounded bg-gray-700 p-2 text-xs uppercase text-white">
|
||||||
@ -186,9 +185,7 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
<span className="p-1">
|
<span className="p-1">
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={`h-3 w-3.5 text-custom-sidebar-text-100 ${
|
className={`h-3 w-3.5 text-custom-sidebar-text-100 ${
|
||||||
active || workspace.id === activeWorkspace?.id
|
workspace.id === activeWorkspace?.id ? "opacity-100" : "opacity-0"
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@ -205,9 +202,9 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push("/create-workspace");
|
router.push("/create-workspace");
|
||||||
}}
|
}}
|
||||||
className="flex w-full items-center gap-1 text-sm text-custom-sidebar-text-200"
|
className="flex w-full items-center gap-2 px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-3 w-3" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Create Workspace
|
Create Workspace
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
@ -263,16 +260,17 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 origin-top-left rounded-md
|
className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 origin-top-left rounded-md
|
||||||
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 p-2 divide-y divide-custom-sidebar-border-200 shadow-lg text-xs outline-none"
|
border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-200 shadow-lg text-xs outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col space-y-2 pb-2">
|
<div className="flex flex-col gap-2.5 pb-2">
|
||||||
|
<span className="px-2 text-custom-sidebar-text-200">{user?.email}</span>
|
||||||
{profileLinks(workspaceSlug?.toString() ?? "", user?.id ?? "").map(
|
{profileLinks(workspaceSlug?.toString() ?? "", user?.id ?? "").map(
|
||||||
(link, index) => (
|
(link, index) => (
|
||||||
<Menu.Item key={index} as="button" type="button">
|
<Menu.Item key={index} as="button" type="button">
|
||||||
<Link href={link.link}>
|
<Link href={link.link}>
|
||||||
<a className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
|
<a className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
|
||||||
<Icon iconName={link.icon} className="!text-base" />
|
<Icon iconName={link.icon} className="!text-lg !leading-5" />
|
||||||
{link.name}
|
{link.name}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -287,7 +285,7 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||||
onClick={handleSignOut}
|
onClick={handleSignOut}
|
||||||
>
|
>
|
||||||
<Icon iconName="logout" className="!text-base" />
|
<Icon iconName="logout" className="!text-lg !leading-5" />
|
||||||
Sign out
|
Sign out
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,7 +51,7 @@ export const WorkspaceSidebarMenu = () => {
|
|||||||
const { collapsed: sidebarCollapse } = useTheme();
|
const { collapsed: sidebarCollapse } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full cursor-pointer space-y-2 px-4 mt-5 pb-5">
|
<div className="w-full cursor-pointer space-y-1 p-4">
|
||||||
{workspaceLinks(workspaceSlug as string).map((link, index) => {
|
{workspaceLinks(workspaceSlug as string).map((link, index) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
link.name === "Settings"
|
link.name === "Settings"
|
||||||
|
47
apps/app/components/workspace/sidebar-quick-action.tsx
Normal file
47
apps/app/components/workspace/sidebar-quick-action.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// ui
|
||||||
|
import { Icon } from "components/ui";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
|
export const WorkspaceSidebarQuickAction = () => {
|
||||||
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
|
||||||
|
store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={`flex items-center gap-2 flex-grow rounded flex-shrink-0 py-2 ${
|
||||||
|
store?.theme?.sidebarCollapsed
|
||||||
|
? "px-2 hover:bg-custom-sidebar-background-80"
|
||||||
|
: "px-3 shadow border-[0.5px] border-custom-border-300"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="edit_square" className="!text-xl !leading-5 text-custom-sidebar-text-300" />
|
||||||
|
{!store?.theme?.sidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${
|
||||||
|
store?.theme?.sidebarCollapsed
|
||||||
|
? "hover:bg-custom-sidebar-background-80"
|
||||||
|
: "shadow border-[0.5px] border-custom-border-300"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "k", ctrlKey: true, metaKey: true });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="search" className="!text-xl !leading-5 text-custom-sidebar-text-300" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -2,13 +2,23 @@ import { objToQueryParams } from "helpers/string.helper";
|
|||||||
import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "types";
|
import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "types";
|
||||||
|
|
||||||
const paramsToKey = (params: any) => {
|
const paramsToKey = (params: any) => {
|
||||||
const { state, priority, assignees, created_by, labels, target_date, sub_issue } = params;
|
const {
|
||||||
|
state,
|
||||||
|
priority,
|
||||||
|
assignees,
|
||||||
|
created_by,
|
||||||
|
labels,
|
||||||
|
target_date,
|
||||||
|
sub_issue,
|
||||||
|
start_target_date,
|
||||||
|
} = params;
|
||||||
|
|
||||||
let stateKey = state ? state.split(",") : [];
|
let stateKey = state ? state.split(",") : [];
|
||||||
let priorityKey = priority ? priority.split(",") : [];
|
let priorityKey = priority ? priority.split(",") : [];
|
||||||
let assigneesKey = assignees ? assignees.split(",") : [];
|
let assigneesKey = assignees ? assignees.split(",") : [];
|
||||||
let createdByKey = created_by ? created_by.split(",") : [];
|
let createdByKey = created_by ? created_by.split(",") : [];
|
||||||
let labelsKey = labels ? labels.split(",") : [];
|
let labelsKey = labels ? labels.split(",") : [];
|
||||||
|
const startTargetDate = start_target_date ? `${start_target_date}`.toUpperCase() : "FALSE";
|
||||||
const targetDateKey = target_date ?? "";
|
const targetDateKey = target_date ?? "";
|
||||||
const type = params.type ? params.type.toUpperCase() : "NULL";
|
const type = params.type ? params.type.toUpperCase() : "NULL";
|
||||||
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
||||||
@ -21,7 +31,7 @@ const paramsToKey = (params: any) => {
|
|||||||
createdByKey = createdByKey.sort().join("_");
|
createdByKey = createdByKey.sort().join("_");
|
||||||
labelsKey = labelsKey.sort().join("_");
|
labelsKey = labelsKey.sort().join("_");
|
||||||
|
|
||||||
return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}_${sub_issue}`;
|
return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}_${sub_issue}_${startTargetDate}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const inboxParamsToKey = (params: any) => {
|
const inboxParamsToKey = (params: any) => {
|
||||||
|
17
apps/app/constants/label.ts
Normal file
17
apps/app/constants/label.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export const LABEL_COLOR_OPTIONS = [
|
||||||
|
"#FF6900",
|
||||||
|
"#FCB900",
|
||||||
|
"#7BDCB5",
|
||||||
|
"#00D084",
|
||||||
|
"#8ED1FC",
|
||||||
|
"#0693E3",
|
||||||
|
"#ABB8C3",
|
||||||
|
"#EB144C",
|
||||||
|
"#F78DA7",
|
||||||
|
"#9900EF",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getRandomLabelColor = () => {
|
||||||
|
const randomIndex = Math.floor(Math.random() * LABEL_COLOR_OPTIONS.length);
|
||||||
|
return LABEL_COLOR_OPTIONS[randomIndex];
|
||||||
|
};
|
@ -43,6 +43,14 @@ export const SPREADSHEET_COLUMN = [
|
|||||||
ascendingOrder: "labels__name",
|
ascendingOrder: "labels__name",
|
||||||
descendingOrder: "-labels__name",
|
descendingOrder: "-labels__name",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
propertyName: "start_date",
|
||||||
|
colName: "Start Date",
|
||||||
|
colSize: "128px",
|
||||||
|
icon: CalendarDaysIcon,
|
||||||
|
ascendingOrder: "-start_date",
|
||||||
|
descendingOrder: "start_date",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
propertyName: "due_date",
|
propertyName: "due_date",
|
||||||
colName: "Due Date",
|
colName: "Due Date",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user