Compare commits

..

3 Commits

Author SHA1 Message Date
NarayanBavisetti 45b5f6d198 fix: added draft filter 2023-09-19 16:54:21 +05:30
NarayanBavisetti fcc56cce81 Merge branch 'fix/cycle_module_stats' of github.com:makeplane/plane into fix/cycle_module_stats 2023-09-19 14:42:00 +05:30
pablohashescobar 069387c242 fix: cycle and module stats when issues are archived 2023-09-14 14:45:03 +05:30
472 changed files with 8349 additions and 20477 deletions
@@ -33,9 +33,14 @@ jobs:
deploy:
- space/**
- name: Setup .npmrc for repository
run: |
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
- name: Build Plane's Main App
if: steps.changed-files.outputs.web_any_changed == 'true'
run: |
mv ./.npmrc ./web
cd web
yarn
yarn build
@@ -22,6 +22,10 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup .npmrc for repository
run: |
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaFrontend
uses: docker/metadata-action@v4.3.0
+11
View File
@@ -59,6 +59,17 @@ chmod +x setup.sh
> If running in a cloud env replace localhost with public facing IP address of the VM
- Setup Tiptap Pro
Visit [Tiptap Pro](https://collab.tiptap.dev/pro-extensions) and signup (it is free).
Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro.
```
@tiptap-pro:registry=https://registry.tiptap.dev/
//registry.tiptap.dev/:_authToken=YOUR_REGISTRY_TOKEN
```
- Run Docker compose up
```bash
-1
View File
@@ -1,7 +1,6 @@
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted"
# Error logs
SENTRY_DSN=""
+1 -1
View File
@@ -23,7 +23,7 @@ from .project import (
ProjectPublicMemberSerializer
)
from .state import StateSerializer, StateLiteSerializer
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
from .asset import FileAssetSerializer
from .issue import (
+1 -30
View File
@@ -5,39 +5,10 @@ from rest_framework import serializers
from .base import BaseSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
from plane.db.models import IssueView, IssueViewFavorite
from plane.utils.issue_filters import issue_filters
class GlobalViewSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = GlobalView
fields = "__all__"
read_only_fields = [
"workspace",
"query",
]
def create(self, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
return GlobalView.objects.create(**validated_data)
def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
class IssueViewSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
+7 -38
View File
@@ -102,8 +102,6 @@ from plane.api.views import (
BulkEstimatePointEndpoint,
## End Estimates
# Views
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewViewSet,
ViewIssuesEndpoint,
IssueViewFavoriteViewSet,
@@ -186,6 +184,7 @@ from plane.api.views import (
## Exporter
ExportIssuesEndpoint,
## End Exporter
)
@@ -242,11 +241,7 @@ urlpatterns = [
UpdateUserTourCompletedEndpoint.as_view(),
name="user-tour",
),
path(
"users/workspaces/<str:slug>/activities/",
UserActivityEndpoint.as_view(),
name="user-activities",
),
path("users/workspaces/<str:slug>/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
# user workspaces
path(
"users/me/workspaces/",
@@ -654,37 +649,6 @@ urlpatterns = [
ViewIssuesEndpoint.as_view(),
name="project-view-issues",
),
path(
"workspaces/<str:slug>/views/",
GlobalViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="global-view",
),
path(
"workspaces/<str:slug>/views/<uuid:pk>/",
GlobalViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="global-view",
),
path(
"workspaces/<str:slug>/issues/",
GlobalViewIssuesViewSet.as_view(
{
"get": "list",
}
),
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
@@ -803,6 +767,11 @@ urlpatterns = [
),
name="project-issue",
),
path(
"workspaces/<str:slug>/issues/",
WorkSpaceIssuesEndpoint.as_view(),
name="workspace-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
LabelViewSet.as_view(
+1 -1
View File
@@ -56,7 +56,7 @@ from .workspace import (
LeaveWorkspaceEndpoint,
)
from .state import StateViewSet
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
from .cycle import (
CycleViewSet,
CycleIssueViewSet,
+108 -23
View File
@@ -80,7 +80,6 @@ class CycleViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -102,48 +101,84 @@ class CycleViewSet(BaseViewSet):
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.annotate(total_issues=Count("issue_cycle"))
.annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="completed"),
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="cancelled"),
filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="started"),
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="unstarted"),
filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="backlog"),
filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="completed"),
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="started"),
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.prefetch_related(
@@ -196,17 +231,30 @@ class CycleViewSet(BaseViewSet):
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("display_name", "assignee_id", "avatar")
.annotate(total_issues=Count("assignee_id"))
.annotate(
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
completed_issues=Count(
"assignee_id",
filter=Q(completed_at__isnull=False),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"assignee_id",
filter=Q(completed_at__isnull=True),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
@@ -222,17 +270,30 @@ class CycleViewSet(BaseViewSet):
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(total_issues=Count("label_id"))
.annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate(
completed_issues=Count(
"label_id",
filter=Q(completed_at__isnull=False),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"label_id",
filter=Q(completed_at__isnull=True),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
@@ -385,17 +446,30 @@ class CycleViewSet(BaseViewSet):
.values(
"first_name", "last_name", "assignee_id", "avatar", "display_name"
)
.annotate(total_issues=Count("assignee_id"))
.annotate(
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
completed_issues=Count(
"assignee_id",
filter=Q(completed_at__isnull=False),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"assignee_id",
filter=Q(completed_at__isnull=True),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("first_name", "last_name")
@@ -412,17 +486,30 @@ class CycleViewSet(BaseViewSet):
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(total_issues=Count("label_id"))
.annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
completed_issues=Count(
"label_id",
filter=Q(completed_at__isnull=False),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"label_id",
filter=Q(completed_at__isnull=True),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
@@ -488,7 +575,6 @@ class CycleIssueViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -664,7 +750,6 @@ class CycleIssueViewSet(BaseViewSet):
),
}
),
epoch = int(timezone.now().timestamp())
)
# Return all Cycle Issues
-4
View File
@@ -213,7 +213,6 @@ class InboxIssueViewSet(BaseViewSet):
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
# create an inbox issue
InboxIssue.objects.create(
@@ -278,7 +277,6 @@ class InboxIssueViewSet(BaseViewSet):
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
issue_serializer.save()
else:
@@ -520,7 +518,6 @@ class InboxIssuePublicViewSet(BaseViewSet):
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
# create an inbox issue
InboxIssue.objects.create(
@@ -585,7 +582,6 @@ class InboxIssuePublicViewSet(BaseViewSet):
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
issue_serializer.save()
return Response(issue_serializer.data, status=status.HTTP_200_OK)
+1 -38
View File
@@ -4,7 +4,6 @@ import random
from itertools import chain
# Django imports
from django.utils import timezone
from django.db.models import (
Prefetch,
OuterRef,
@@ -130,7 +129,6 @@ class IssueViewSet(BaseViewSet):
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
epoch = int(timezone.now().timestamp())
)
return super().perform_update(serializer)
@@ -151,7 +149,6 @@ class IssueViewSet(BaseViewSet):
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -318,7 +315,6 @@ class IssueViewSet(BaseViewSet):
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -330,12 +326,7 @@ class IssueViewSet(BaseViewSet):
def retrieve(self, request, slug, project_id, pk=None):
try:
issue = 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")
).get(
issue = Issue.issue_objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
@@ -577,7 +568,6 @@ class IssueCommentViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
def perform_update(self, serializer):
@@ -596,7 +586,6 @@ class IssueCommentViewSet(BaseViewSet):
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
return super().perform_update(serializer)
@@ -618,7 +607,6 @@ class IssueCommentViewSet(BaseViewSet):
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -902,7 +890,6 @@ class IssueLinkViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
def perform_update(self, serializer):
@@ -921,7 +908,6 @@ class IssueLinkViewSet(BaseViewSet):
IssueLinkSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
return super().perform_update(serializer)
@@ -943,7 +929,6 @@ class IssueLinkViewSet(BaseViewSet):
IssueLinkSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -1022,7 +1007,6 @@ class IssueAttachmentEndpoint(BaseAPIView):
serializer.data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1045,7 +1029,6 @@ class IssueAttachmentEndpoint(BaseAPIView):
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1248,7 +1231,6 @@ class IssueArchiveViewSet(BaseViewSet):
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
@@ -1453,7 +1435,6 @@ class IssueReactionViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
def destroy(self, request, slug, project_id, issue_id, reaction_code):
@@ -1477,7 +1458,6 @@ class IssueReactionViewSet(BaseViewSet):
"identifier": str(issue_reaction.id),
}
),
epoch = int(timezone.now().timestamp())
)
issue_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1526,7 +1506,6 @@ class CommentReactionViewSet(BaseViewSet):
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
def destroy(self, request, slug, project_id, comment_id, reaction_code):
@@ -1551,7 +1530,6 @@ class CommentReactionViewSet(BaseViewSet):
"comment_id": str(comment_id),
}
),
epoch = int(timezone.now().timestamp())
)
comment_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1648,7 +1626,6 @@ class IssueCommentPublicViewSet(BaseViewSet):
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
if not ProjectMember.objects.filter(
project_id=project_id,
@@ -1698,7 +1675,6 @@ class IssueCommentPublicViewSet(BaseViewSet):
IssueCommentSerializer(comment).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1732,7 +1708,6 @@ class IssueCommentPublicViewSet(BaseViewSet):
IssueCommentSerializer(comment).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
comment.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1807,7 +1782,6 @@ class IssueReactionPublicViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1852,7 +1826,6 @@ class IssueReactionPublicViewSet(BaseViewSet):
"identifier": str(issue_reaction.id),
}
),
epoch = int(timezone.now().timestamp())
)
issue_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1926,7 +1899,6 @@ class CommentReactionPublicViewSet(BaseViewSet):
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1978,7 +1950,6 @@ class CommentReactionPublicViewSet(BaseViewSet):
"comment_id": str(comment_id),
}
),
epoch = int(timezone.now().timestamp())
)
comment_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -2042,7 +2013,6 @@ class IssueVotePublicViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
serializer = IssueVoteSerializer(issue_vote)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -2077,7 +2047,6 @@ class IssueVotePublicViewSet(BaseViewSet):
"identifier": str(issue_vote.id),
}
),
epoch = int(timezone.now().timestamp())
)
issue_vote.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -2111,7 +2080,6 @@ class IssueRelationViewSet(BaseViewSet):
IssueRelationSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -2145,7 +2113,6 @@ class IssueRelationViewSet(BaseViewSet):
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
if relation == "blocking":
@@ -2190,8 +2157,6 @@ class IssueRelationViewSet(BaseViewSet):
.select_related("issue")
.distinct()
)
class IssueRetrievePublicEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
@@ -2417,7 +2382,6 @@ class IssueDraftViewSet(BaseViewSet):
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
epoch = int(timezone.now().timestamp())
)
return super().perform_update(serializer)
@@ -2602,7 +2566,6 @@ class IssueDraftViewSet(BaseViewSet):
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+76 -18
View File
@@ -2,7 +2,6 @@
import json
# Django Imports
from django.utils import timezone
from django.db import IntegrityError
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
from django.core import serializers
@@ -40,6 +39,7 @@ from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
from plane.utils.analytics_plot import burndown_plot
class ModuleViewSet(BaseViewSet):
model = Module
permission_classes = [
@@ -78,35 +78,63 @@ class ModuleViewSet(BaseViewSet):
queryset=ModuleLink.objects.select_related("module", "created_by"),
)
)
.annotate(total_issues=Count("issue_module"))
.annotate(
total_issues=Count(
"issue_module",
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
"issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="completed"),
filter=Q(
issue_module__issue__state__group="completed",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
)
)
.annotate(
cancelled_issues=Count(
"issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="cancelled"),
filter=Q(
issue_module__issue__state__group="cancelled",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
)
)
.annotate(
started_issues=Count(
"issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="started"),
filter=Q(
issue_module__issue__state__group="started",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
)
)
.annotate(
unstarted_issues=Count(
"issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="unstarted"),
filter=Q(
issue_module__issue__state__group="unstarted",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
)
)
.annotate(
backlog_issues=Count(
"issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="backlog"),
filter=Q(
issue_module__issue__state__group="backlog",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
)
)
.order_by(order_by, "name")
@@ -130,7 +158,6 @@ class ModuleViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -179,18 +206,36 @@ class ModuleViewSet(BaseViewSet):
.annotate(assignee_id=F("assignees__id"))
.annotate(display_name=F("assignees__display_name"))
.annotate(avatar=F("assignees__avatar"))
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
.annotate(total_issues=Count("assignee_id"))
.values(
"first_name", "last_name", "assignee_id", "avatar", "display_name"
)
.annotate(
total_issues=Count(
"assignee_id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
completed_issues=Count(
"assignee_id",
filter=Q(completed_at__isnull=False),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"assignee_id",
filter=Q(completed_at__isnull=True),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("first_name", "last_name")
@@ -206,17 +251,33 @@ class ModuleViewSet(BaseViewSet):
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(total_issues=Count("label_id"))
.annotate(
total_issues=Count(
"label_id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
"label_id",
filter=Q(completed_at__isnull=False),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"label_id",
filter=Q(completed_at__isnull=True),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
@@ -279,7 +340,6 @@ class ModuleIssueViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -447,7 +507,6 @@ class ModuleIssueViewSet(BaseViewSet):
),
}
),
epoch = int(timezone.now().timestamp())
)
return Response(
@@ -494,7 +553,6 @@ class ModuleLinkViewSet(BaseViewSet):
class ModuleFavoriteViewSet(BaseViewSet):
serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite
+1 -189
View File
@@ -1,18 +1,4 @@
# Django imports
from django.db.models import (
Prefetch,
OuterRef,
Func,
F,
Case,
Value,
CharField,
When,
Exists,
Max,
)
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.db import IntegrityError
from django.db.models import Prefetch, OuterRef, Exists
@@ -24,192 +10,18 @@ from sentry_sdk import capture_exception
# Module imports
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import (
GlobalViewSerializer,
IssueViewSerializer,
IssueLiteSerializer,
IssueViewFavoriteSerializer,
)
from plane.api.permissions import WorkspaceEntityPermission, ProjectEntityPermission
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
Workspace,
GlobalView,
IssueView,
Issue,
IssueViewFavorite,
IssueReaction,
IssueLink,
IssueAttachment,
)
from plane.utils.issue_filters import issue_filters
from plane.utils.grouper import group_results
class GlobalViewViewSet(BaseViewSet):
serializer_class = GlobalViewSerializer
model = GlobalView
permission_classes = [
WorkspaceEntityPermission,
]
def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
serializer.save(workspace_id=workspace.id)
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace")
.order_by("-created_at")
.distinct()
)
class GlobalViewIssuesViewSet(BaseViewSet):
permission_classes = [
WorkspaceEntityPermission,
]
def get_queryset(self):
return (
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(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
)
@method_decorator(gzip_page)
def list(self, request, slug):
try:
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
self.get_queryset()
.filter(**filters)
.filter(project__project_projectmember__member=self.request.user)
.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
## Grouping the results
group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False)
if sub_group_by and sub_group_by == group_by:
return Response(
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
return Response(
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
)
return Response(issues, 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 IssueViewViewSet(BaseViewSet):
+14 -2
View File
@@ -1239,13 +1239,21 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
.annotate(
created_issues=Count(
"project_issue",
filter=Q(project_issue__created_by_id=user_id),
filter=Q(
project_issue__created_by_id=user_id,
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
)
)
.annotate(
assigned_issues=Count(
"project_issue",
filter=Q(project_issue__assignees__in=[user_id]),
filter=Q(
project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
)
)
.annotate(
@@ -1254,6 +1262,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
filter=Q(
project_issue__completed_at__isnull=False,
project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
)
)
@@ -1267,6 +1277,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
"started",
],
project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
)
)
+30 -96
View File
@@ -39,7 +39,6 @@ def track_name(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("name") != requested_data.get("name"):
issue_activities.append(
@@ -53,7 +52,6 @@ def track_name(
project=project,
workspace=project.workspace,
comment=f"updated the name to {requested_data.get('name')}",
epoch=epoch,
)
)
@@ -66,7 +64,6 @@ def track_parent(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("parent") != requested_data.get("parent"):
if requested_data.get("parent") == None:
@@ -84,7 +81,6 @@ def track_parent(
comment=f"updated the parent issue to None",
old_identifier=old_parent.id,
new_identifier=None,
epoch=epoch,
)
)
else:
@@ -105,7 +101,6 @@ def track_parent(
comment=f"updated the parent issue to {new_parent.name}",
old_identifier=old_parent.id if old_parent is not None else None,
new_identifier=new_parent.id,
epoch=epoch,
)
)
@@ -118,7 +113,6 @@ def track_priority(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("priority") != requested_data.get("priority"):
if requested_data.get("priority") == None:
@@ -133,7 +127,6 @@ def track_priority(
project=project,
workspace=project.workspace,
comment=f"updated the priority to None",
epoch=epoch,
)
)
else:
@@ -148,7 +141,6 @@ def track_priority(
project=project,
workspace=project.workspace,
comment=f"updated the priority to {requested_data.get('priority')}",
epoch=epoch,
)
)
@@ -161,7 +153,6 @@ def track_state(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("state") != requested_data.get("state"):
new_state = State.objects.get(pk=requested_data.get("state", None))
@@ -180,7 +171,6 @@ def track_state(
comment=f"updated the state to {new_state.name}",
old_identifier=old_state.id,
new_identifier=new_state.id,
epoch=epoch,
)
)
@@ -193,7 +183,6 @@ def track_description(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("description_html") != requested_data.get(
"description_html"
@@ -214,7 +203,6 @@ def track_description(
project=project,
workspace=project.workspace,
comment=f"updated the description to {requested_data.get('description_html')}",
epoch=epoch,
)
)
@@ -227,7 +215,6 @@ def track_target_date(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("target_date") != requested_data.get("target_date"):
if requested_data.get("target_date") == None:
@@ -242,7 +229,6 @@ def track_target_date(
project=project,
workspace=project.workspace,
comment=f"updated the target date to None",
epoch=epoch,
)
)
else:
@@ -257,7 +243,6 @@ def track_target_date(
project=project,
workspace=project.workspace,
comment=f"updated the target date to {requested_data.get('target_date')}",
epoch=epoch,
)
)
@@ -270,7 +255,6 @@ def track_start_date(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("start_date") != requested_data.get("start_date"):
if requested_data.get("start_date") == None:
@@ -285,7 +269,6 @@ def track_start_date(
project=project,
workspace=project.workspace,
comment=f"updated the start date to None",
epoch=epoch,
)
)
else:
@@ -300,7 +283,6 @@ def track_start_date(
project=project,
workspace=project.workspace,
comment=f"updated the start date to {requested_data.get('start_date')}",
epoch=epoch,
)
)
@@ -313,7 +295,6 @@ def track_labels(
project,
actor,
issue_activities,
epoch
):
# Label Addition
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
@@ -333,7 +314,6 @@ def track_labels(
comment=f"added label {label.name}",
new_identifier=label.id,
old_identifier=None,
epoch=epoch,
)
)
@@ -355,7 +335,6 @@ def track_labels(
comment=f"removed label {label.name}",
old_identifier=label.id,
new_identifier=None,
epoch=epoch,
)
)
@@ -368,7 +347,6 @@ def track_assignees(
project,
actor,
issue_activities,
epoch
):
# Assignee Addition
if len(requested_data.get("assignees_list")) > len(
@@ -389,7 +367,6 @@ def track_assignees(
workspace=project.workspace,
comment=f"added assignee {assignee.display_name}",
new_identifier=assignee.id,
epoch=epoch,
)
)
@@ -412,13 +389,12 @@ def track_assignees(
workspace=project.workspace,
comment=f"removed assignee {assignee.display_name}",
old_identifier=assignee.id,
epoch=epoch,
)
)
def create_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
@@ -428,13 +404,12 @@ def create_issue_activity(
comment=f"created the issue",
verb="created",
actor=actor,
epoch=epoch,
)
)
def track_estimate_points(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
if requested_data.get("estimate_point") == None:
@@ -449,7 +424,6 @@ def track_estimate_points(
project=project,
workspace=project.workspace,
comment=f"updated the estimate point to None",
epoch=epoch,
)
)
else:
@@ -464,13 +438,12 @@ def track_estimate_points(
project=project,
workspace=project.workspace,
comment=f"updated the estimate point to {requested_data.get('estimate_point')}",
epoch=epoch,
)
)
def track_archive_at(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if requested_data.get("archived_at") is None:
issue_activities.append(
@@ -484,7 +457,6 @@ def track_archive_at(
field="archived_at",
old_value="archive",
new_value="restore",
epoch=epoch,
)
)
else:
@@ -499,13 +471,12 @@ def track_archive_at(
field="archived_at",
old_value=None,
new_value="archive",
epoch=epoch,
)
)
def track_closed_to(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if requested_data.get("closed_to") is not None:
updated_state = State.objects.get(
@@ -525,13 +496,12 @@ def track_closed_to(
comment=f"Plane updated the state to {updated_state.name}",
old_identifier=None,
new_identifier=updated_state.id,
epoch=epoch,
)
)
def update_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
ISSUE_ACTIVITY_MAPPER = {
"name": track_name,
@@ -548,11 +518,6 @@ def update_issue_activity(
"closed_to": track_closed_to,
}
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
for key in requested_data:
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
if func is not None:
@@ -563,12 +528,11 @@ def update_issue_activity(
project,
actor,
issue_activities,
epoch
)
def delete_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
@@ -578,13 +542,12 @@ def delete_issue_activity(
verb="deleted",
actor=actor,
field="issue",
epoch=epoch,
)
)
def create_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -603,13 +566,12 @@ def create_comment_activity(
new_value=requested_data.get("comment_html", ""),
new_identifier=requested_data.get("id", None),
issue_comment_id=requested_data.get("id", None),
epoch=epoch,
)
)
def update_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -631,13 +593,12 @@ def update_comment_activity(
new_value=requested_data.get("comment_html", ""),
new_identifier=current_instance.get("id", None),
issue_comment_id=current_instance.get("id", None),
epoch=epoch,
)
)
def delete_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
@@ -648,13 +609,12 @@ def delete_comment_activity(
verb="deleted",
actor=actor,
field="comment",
epoch=epoch,
)
)
def create_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -686,7 +646,6 @@ def create_cycle_issue_activity(
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
old_identifier=old_cycle.id,
new_identifier=new_cycle.id,
epoch=epoch,
)
)
@@ -707,13 +666,12 @@ def create_cycle_issue_activity(
workspace=project.workspace,
comment=f"added cycle {cycle.name}",
new_identifier=cycle.id,
epoch=epoch,
)
)
def delete_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -737,13 +695,12 @@ def delete_cycle_issue_activity(
workspace=project.workspace,
comment=f"removed this issue from {cycle.name if cycle is not None else None}",
old_identifier=cycle.id if cycle is not None else None,
epoch=epoch,
)
)
def create_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -775,7 +732,6 @@ def create_module_issue_activity(
comment=f"updated module from {old_module.name} to {new_module.name}",
old_identifier=old_module.id,
new_identifier=new_module.id,
epoch=epoch,
)
)
@@ -795,13 +751,12 @@ def create_module_issue_activity(
workspace=project.workspace,
comment=f"added module {module.name}",
new_identifier=module.id,
epoch=epoch,
)
)
def delete_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -825,13 +780,12 @@ def delete_module_issue_activity(
workspace=project.workspace,
comment=f"removed this issue from {module.name if module is not None else None}",
old_identifier=module.id if module is not None else None,
epoch=epoch,
)
)
def create_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -849,13 +803,12 @@ def create_link_activity(
field="link",
new_value=requested_data.get("url", ""),
new_identifier=requested_data.get("id", None),
epoch=epoch,
)
)
def update_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -876,13 +829,12 @@ def update_link_activity(
old_identifier=current_instance.get("id"),
new_value=requested_data.get("url", ""),
new_identifier=current_instance.get("id", None),
epoch=epoch,
)
)
def delete_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
current_instance = (
@@ -899,14 +851,13 @@ def delete_link_activity(
actor=actor,
field="link",
old_value=current_instance.get("url", ""),
new_value="",
epoch=epoch,
new_value=""
)
)
def create_attachment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -924,13 +875,12 @@ def create_attachment_activity(
field="attachment",
new_value=current_instance.get("asset", ""),
new_identifier=current_instance.get("id", None),
epoch=epoch,
)
)
def delete_attachment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
@@ -941,12 +891,11 @@ def delete_attachment_activity(
verb="deleted",
actor=actor,
field="attachment",
epoch=epoch,
)
)
def create_issue_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
if requested_data and requested_data.get("reaction") is not None:
@@ -965,13 +914,12 @@ def create_issue_reaction_activity(
comment="added the reaction",
old_identifier=None,
new_identifier=issue_reaction,
epoch=epoch,
)
)
def delete_issue_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
current_instance = (
json.loads(current_instance) if current_instance is not None else None
@@ -990,13 +938,12 @@ def delete_issue_reaction_activity(
comment="removed the reaction",
old_identifier=current_instance.get("identifier"),
new_identifier=None,
epoch=epoch,
)
)
def create_comment_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
if requested_data and requested_data.get("reaction") is not None:
@@ -1016,13 +963,12 @@ def create_comment_reaction_activity(
comment="added the reaction",
old_identifier=None,
new_identifier=comment_reaction_id,
epoch=epoch,
)
)
def delete_comment_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
current_instance = (
json.loads(current_instance) if current_instance is not None else None
@@ -1043,13 +989,12 @@ def delete_comment_reaction_activity(
comment="removed the reaction",
old_identifier=current_instance.get("identifier"),
new_identifier=None,
epoch=epoch,
)
)
def create_issue_vote_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
if requested_data and requested_data.get("vote") is not None:
@@ -1066,13 +1011,12 @@ def create_issue_vote_activity(
comment="added the vote",
old_identifier=None,
new_identifier=None,
epoch=epoch,
)
)
def delete_issue_vote_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
current_instance = (
json.loads(current_instance) if current_instance is not None else None
@@ -1091,13 +1035,12 @@ def delete_issue_vote_activity(
comment="removed the vote",
old_identifier=current_instance.get("identifier"),
new_identifier=None,
epoch=epoch,
)
)
def create_issue_relation_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -1137,13 +1080,12 @@ def create_issue_relation_activity(
workspace=project.workspace,
comment=f'added {issue_relation.get("relation_type")} relation',
old_identifier=issue_relation.get("related_issue"),
epoch=epoch,
)
)
def delete_issue_relation_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -1167,7 +1109,6 @@ def delete_issue_relation_activity(
workspace=project.workspace,
comment=f'deleted {relation_type} relation',
old_identifier=current_instance.get("issue"),
epoch=epoch,
)
)
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
@@ -1183,13 +1124,12 @@ def delete_issue_relation_activity(
workspace=project.workspace,
comment=f'deleted {current_instance.get("relation_type")} relation',
old_identifier=current_instance.get("related_issue"),
epoch=epoch,
)
)
def create_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
@@ -1200,13 +1140,12 @@ def create_draft_issue_activity(
field="draft",
verb="created",
actor=actor,
epoch=epoch,
)
)
def update_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -1221,7 +1160,6 @@ def update_draft_issue_activity(
comment=f"created the issue",
verb="updated",
actor=actor,
epoch=epoch,
)
)
else:
@@ -1234,14 +1172,13 @@ def update_draft_issue_activity(
field="draft",
verb="updated",
actor=actor,
epoch=epoch,
)
)
def delete_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
@@ -1251,7 +1188,6 @@ def delete_draft_issue_activity(
field="draft",
verb="deleted",
actor=actor,
epoch=epoch,
)
)
@@ -1264,7 +1200,6 @@ def issue_activity(
issue_id,
actor_id,
project_id,
epoch,
subscriber=True,
):
try:
@@ -1341,7 +1276,6 @@ def issue_activity(
project,
actor,
issue_activities,
epoch,
)
# Save all the values to database
@@ -77,7 +77,6 @@ def archive_old_issues():
project_id=project_id,
current_instance=None,
subscriber=False,
epoch = int(timezone.now().timestamp())
)
for issue in updated_issues
]
@@ -149,7 +148,6 @@ def close_old_issues():
project_id=project_id,
current_instance=None,
subscriber=False,
epoch = int(timezone.now().timestamp())
)
for issue in updated_issues
]
@@ -2,7 +2,6 @@
from django.db import migrations
def update_issue_activity(apps, schema_editor):
IssueActivityModel = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
@@ -1,53 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-19 14:21
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
def update_epoch(apps, schema_editor):
IssueActivity = apps.get_model('db', 'IssueActivity')
updated_issue_activity = []
for obj in IssueActivity.objects.all():
obj.epoch = int(obj.created_at.timestamp())
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0045_auto_20230915_0655'),
]
operations = [
migrations.CreateModel(
name='GlobalView',
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)),
('name', models.CharField(max_length=255, verbose_name='View Name')),
('description', models.TextField(blank=True, verbose_name='View Description')),
('query', models.JSONField(verbose_name='View Query')),
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
('query_data', models.JSONField(default=dict)),
('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')),
('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='global_views', to='db.workspace')),
],
options={
'verbose_name': 'Global View',
'verbose_name_plural': 'Global Views',
'db_table': 'global_views',
'ordering': ('-created_at',),
},
),
migrations.AddField(
model_name='issueactivity',
name='epoch',
field=models.FloatField(null=True),
),
migrations.RunPython(update_epoch),
]
+1 -1
View File
@@ -50,7 +50,7 @@ from .state import State
from .cycle import Cycle, CycleIssue, CycleFavorite
from .view import GlobalView, IssueView, IssueViewFavorite
from .view import IssueView, IssueViewFavorite
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
-1
View File
@@ -309,7 +309,6 @@ class IssueActivity(ProjectBaseModel):
)
old_identifier = models.UUIDField(null=True)
new_identifier = models.UUIDField(null=True)
epoch = models.FloatField(null=True)
class Meta:
verbose_name = "Issue Activity"
+1 -24
View File
@@ -3,30 +3,7 @@ from django.db import models
from django.conf import settings
# Module import
from . import ProjectBaseModel, BaseModel
class GlobalView(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
)
name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True)
query = models.JSONField(verbose_name="View Query")
access = models.PositiveSmallIntegerField(
default=1, choices=((0, "Private"), (1, "Public"))
)
query_data = models.JSONField(default=dict)
class Meta:
verbose_name = "Global View"
verbose_name_plural = "Global Views"
db_table = "global_views"
ordering = ("-created_at",)
def __str__(self):
"""Return name of the View"""
return f"{self.name} <{self.workspace.name}>"
from . import ProjectBaseModel
class IssueView(ProjectBaseModel):
+115 -76
View File
@@ -1,8 +1,10 @@
"""Production settings and globals."""
from urllib.parse import urlparse
import ssl
import certifi
import dj_database_url
from urllib.parse import urlparse
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
@@ -89,89 +91,112 @@ if bool(os.environ.get("SENTRY_DSN", False)):
profiles_sample_rate=1.0,
)
# The AWS region to connect to.
AWS_REGION = os.environ.get("AWS_REGION", "")
if DOCKERIZED and USE_MINIO:
INSTALLED_APPS += ("storages",)
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
# The AWS secret access key to use.
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
# The name of the bucket to store files in.
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = os.environ.get(
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
)
# Default permissions
AWS_DEFAULT_ACL = "public-read"
AWS_QUERYSTRING_AUTH = False
AWS_S3_FILE_OVERWRITE = False
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
# Custom Domain settings
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
else:
# The AWS region to connect to.
AWS_REGION = os.environ.get("AWS_REGION", "")
# The AWS secret access key to use.
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
# The optional AWS session token to use.
# AWS_SESSION_TOKEN = ""
# The AWS secret access key to use.
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
# The name of the bucket to store files in.
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
# The optional AWS session token to use.
# AWS_SESSION_TOKEN = ""
# How to construct S3 URLs ("auto", "path", "virtual").
AWS_S3_ADDRESSING_STYLE = "auto"
# The name of the bucket to store files in.
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# How to construct S3 URLs ("auto", "path", "virtual").
AWS_S3_ADDRESSING_STYLE = "auto"
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = ""
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
# and their permissions will be set to "public-read".
AWS_S3_BUCKET_AUTH = False
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = ""
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
# is True. It also affects the "Cache-Control" header of the files.
# Important: Changing this setting will not affect existing files.
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
# and their permissions will be set to "public-read".
AWS_S3_BUCKET_AUTH = False
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
# cannot be used with `AWS_S3_BUCKET_AUTH`.
AWS_S3_PUBLIC_URL = ""
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
# is True. It also affects the "Cache-Control" header of the files.
# Important: Changing this setting will not affect existing files.
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
# understand the consequences before enabling.
# Important: Changing this setting will not affect existing files.
AWS_S3_REDUCED_REDUNDANCY = False
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
# cannot be used with `AWS_S3_BUCKET_AUTH`.
AWS_S3_PUBLIC_URL = ""
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_DISPOSITION = ""
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
# understand the consequences before enabling.
# Important: Changing this setting will not affect existing files.
AWS_S3_REDUCED_REDUNDANCY = False
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_LANGUAGE = ""
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_DISPOSITION = ""
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_METADATA = {}
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_LANGUAGE = ""
# If True, then files will be stored using AES256 server-side encryption.
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
# Otherwise, server-side encryption is not be enabled.
# Important: Changing this setting will not affect existing files.
AWS_S3_ENCRYPT_KEY = False
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_METADATA = {}
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
# If True, then files will be stored using AES256 server-side encryption.
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
# Otherwise, server-side encryption is not be enabled.
# Important: Changing this setting will not affect existing files.
AWS_S3_ENCRYPT_KEY = False
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
# compressed size is smaller than their uncompressed size.
# Important: Changing this setting will not affect existing files.
AWS_S3_GZIP = True
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
# The signature version to use for S3 requests.
AWS_S3_SIGNATURE_VERSION = None
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
# compressed size is smaller than their uncompressed size.
# Important: Changing this setting will not affect existing files.
AWS_S3_GZIP = True
# If True, then files with the same name will overwrite each other. By default it's set to False to have
# extra characters appended.
AWS_S3_FILE_OVERWRITE = False
# The signature version to use for S3 requests.
AWS_S3_SIGNATURE_VERSION = None
STORAGES["default"] = {
"BACKEND": "django_s3_storage.storage.S3Storage",
}
# If True, then files with the same name will overwrite each other. By default it's set to False to have
# extra characters appended.
AWS_S3_FILE_OVERWRITE = False
STORAGES["default"] = {
"BACKEND": "django_s3_storage.storage.S3Storage",
}
# AWS Settings End
@@ -193,16 +218,27 @@ CSRF_COOKIE_SECURE = True
REDIS_URL = os.environ.get("REDIS_URL")
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
},
if DOCKERIZED:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
else:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
},
}
}
}
WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so")
@@ -225,16 +261,19 @@ broker_url = (
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
)
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
if DOCKERIZED:
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
else:
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
# Enable or Disable signups
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
# Scout Settings
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
SCOUT_NAME = "Plane"
-128
View File
@@ -1,128 +0,0 @@
"""Self hosted settings and globals."""
from urllib.parse import urlparse
import dj_database_url
from urllib.parse import urlparse
from .common import * # noqa
# Database
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
# Docker configurations
DOCKERIZED = 1
USE_MINIO = 1
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "plane",
"USER": os.environ.get("PGUSER", ""),
"PASSWORD": os.environ.get("PGPASSWORD", ""),
"HOST": os.environ.get("PGHOST", ""),
}
}
# Parse database configuration from $DATABASE_URL
DATABASES["default"] = dj_database_url.config()
SITE_ID = 1
# File size limit
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
CORS_ALLOW_METHODS = [
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
]
CORS_ALLOW_HEADERS = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = True
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
INSTALLED_APPS += ("storages",)
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
# The AWS secret access key to use.
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
# The name of the bucket to store files in.
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = os.environ.get(
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
)
# Default permissions
AWS_DEFAULT_ACL = "public-read"
AWS_QUERYSTRING_AUTH = False
AWS_S3_FILE_OVERWRITE = False
# Custom Domain settings
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Allow all host headers
ALLOWED_HOSTS = [
"*",
]
# Security settings
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Redis URL
REDIS_URL = os.environ.get("REDIS_URL")
# Caches
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
# URL used for email redirects
WEB_URL = os.environ.get("WEB_URL", "http://localhost")
# Celery settings
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
# Enable or Disable signups
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
# Analytics
ANALYTICS_BASE_API = False
# OPEN AI Settings
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
+1 -1
View File
@@ -4,7 +4,7 @@ x-api-and-worker-env:
&api-and-worker-env
DEBUG: ${DEBUG}
SENTRY_DSN: ${SENTRY_DSN}
DJANGO_SETTINGS_MODULE: plane.settings.selfhosted
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
REDIS_URL: redis://plane-redis:6379/
EMAIL_HOST: ${EMAIL_HOST}
+2 -3
View File
@@ -4,9 +4,8 @@
"extends": "./base.json",
"compilerOptions": {
"jsx": "react",
"lib": ["ES2015", "DOM"],
"lib": ["ES2015"],
"module": "ESNext",
"target": "es6",
"sourceMap": true
"target": "es6"
}
}
@@ -1,5 +1,3 @@
import * as React from "react";
export const Button = () => {
return <button>button</button>;
};
-41
View File
@@ -1,41 +0,0 @@
import * as React from 'react';
import { FC } from 'react';
declare const Button: () => JSX.Element;
interface InputProps {
type: string;
id: string;
value: string;
name: string;
onChange: () => void;
className?: string;
mode?: "primary" | "transparent" | "true-transparent";
size?: "sm" | "md" | "lg";
hasError?: boolean;
placeholder?: string;
disabled?: boolean;
}
declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
interface TextAreaProps {
id: string;
name: string;
placeholder?: string;
value?: string;
rows?: number;
cols?: number;
disabled?: boolean;
onChange: () => void;
mode?: "primary" | "transparent";
hasError?: boolean;
className?: string;
}
declare const TextArea: React.ForwardRefExoticComponent<TextAreaProps & React.RefAttributes<HTMLTextAreaElement>>;
interface IRadialProgressBar {
progress: number;
}
declare const RadialProgressBar: FC<IRadialProgressBar>;
export { Button, Input, InputProps, RadialProgressBar, TextArea, TextAreaProps };
-157
View File
@@ -1,157 +0,0 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.tsx
var src_exports = {};
__export(src_exports, {
Button: () => Button,
Input: () => Input,
RadialProgressBar: () => RadialProgressBar,
TextArea: () => TextArea
});
module.exports = __toCommonJS(src_exports);
// src/buttons/index.tsx
var React = __toESM(require("react"));
var Button = () => {
return /* @__PURE__ */ React.createElement("button", null, "button");
};
// src/form-fields/input.tsx
var React2 = __toESM(require("react"));
var Input = React2.forwardRef((props, ref) => {
const {
id,
type,
value,
name,
onChange,
className = "",
mode = "primary",
size = "md",
hasError = false,
placeholder = "",
disabled = false
} = props;
return /* @__PURE__ */ React2.createElement("input", {
id,
ref,
type,
value,
name,
onChange,
placeholder,
disabled,
className: `block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" : mode === "true-transparent" ? "rounded border-none bg-transparent ring-0" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${size === "sm" ? "px-3 py-2" : size === "lg" ? "p-3" : ""} ${className}`
});
});
Input.displayName = "form-input-field";
// src/form-fields/textarea.tsx
var React3 = __toESM(require("react"));
var useAutoSizeTextArea = (textAreaRef, value) => {
React3.useEffect(() => {
if (textAreaRef) {
textAreaRef.style.height = "0px";
const scrollHeight = textAreaRef.scrollHeight;
textAreaRef.style.height = scrollHeight + "px";
}
}, [textAreaRef, value]);
};
var TextArea = React3.forwardRef(
(props, ref) => {
const {
id,
name,
placeholder = "",
value = "",
rows = 1,
cols = 1,
disabled,
onChange,
mode = "primary",
hasError = false,
className = ""
} = props;
const textAreaRef = React3.useRef(ref);
ref && useAutoSizeTextArea(textAreaRef == null ? void 0 : textAreaRef.current, value);
return /* @__PURE__ */ React3.createElement("textarea", {
id,
name,
ref: textAreaRef,
placeholder,
value,
rows,
cols,
disabled,
onChange,
className: `no-scrollbar w-full bg-transparent placeholder-custom-text-400 px-3 py-2 outline-none ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-100" : ""} ${className}`
});
}
);
// src/progress/radial-progress.tsx
var import_react = __toESM(require("react"));
var RadialProgressBar = (props) => {
const { progress } = props;
const [circumference, setCircumference] = (0, import_react.useState)(0);
(0, import_react.useEffect)(() => {
const radius = 40;
const circumference2 = 2 * Math.PI * radius;
setCircumference(circumference2);
}, []);
const progressOffset = (100 - progress) / 100 * circumference;
return /* @__PURE__ */ import_react.default.createElement("div", {
className: "relative h-4 w-4"
}, /* @__PURE__ */ import_react.default.createElement("svg", {
className: "absolute top-0 left-0",
viewBox: "0 0 100 100"
}, /* @__PURE__ */ import_react.default.createElement("circle", {
className: "stroke-current opacity-10",
cx: "50",
cy: "50",
r: "40",
strokeWidth: "12",
fill: "none",
strokeDasharray: `${circumference} ${circumference}`
}), /* @__PURE__ */ import_react.default.createElement("circle", {
className: `stroke-current`,
cx: "50",
cy: "50",
r: "40",
strokeWidth: "12",
fill: "none",
strokeDasharray: `${circumference} ${circumference}`,
strokeDashoffset: progressOffset,
transform: "rotate(-90 50 50)"
})));
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Button,
Input,
RadialProgressBar,
TextArea
});
-121
View File
@@ -1,121 +0,0 @@
// src/buttons/index.tsx
import * as React from "react";
var Button = () => {
return /* @__PURE__ */ React.createElement("button", null, "button");
};
// src/form-fields/input.tsx
import * as React2 from "react";
var Input = React2.forwardRef((props, ref) => {
const {
id,
type,
value,
name,
onChange,
className = "",
mode = "primary",
size = "md",
hasError = false,
placeholder = "",
disabled = false
} = props;
return /* @__PURE__ */ React2.createElement("input", {
id,
ref,
type,
value,
name,
onChange,
placeholder,
disabled,
className: `block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" : mode === "true-transparent" ? "rounded border-none bg-transparent ring-0" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${size === "sm" ? "px-3 py-2" : size === "lg" ? "p-3" : ""} ${className}`
});
});
Input.displayName = "form-input-field";
// src/form-fields/textarea.tsx
import * as React3 from "react";
var useAutoSizeTextArea = (textAreaRef, value) => {
React3.useEffect(() => {
if (textAreaRef) {
textAreaRef.style.height = "0px";
const scrollHeight = textAreaRef.scrollHeight;
textAreaRef.style.height = scrollHeight + "px";
}
}, [textAreaRef, value]);
};
var TextArea = React3.forwardRef(
(props, ref) => {
const {
id,
name,
placeholder = "",
value = "",
rows = 1,
cols = 1,
disabled,
onChange,
mode = "primary",
hasError = false,
className = ""
} = props;
const textAreaRef = React3.useRef(ref);
ref && useAutoSizeTextArea(textAreaRef == null ? void 0 : textAreaRef.current, value);
return /* @__PURE__ */ React3.createElement("textarea", {
id,
name,
ref: textAreaRef,
placeholder,
value,
rows,
cols,
disabled,
onChange,
className: `no-scrollbar w-full bg-transparent placeholder-custom-text-400 px-3 py-2 outline-none ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-100" : ""} ${className}`
});
}
);
// src/progress/radial-progress.tsx
import React4, { useState, useEffect as useEffect2 } from "react";
var RadialProgressBar = (props) => {
const { progress } = props;
const [circumference, setCircumference] = useState(0);
useEffect2(() => {
const radius = 40;
const circumference2 = 2 * Math.PI * radius;
setCircumference(circumference2);
}, []);
const progressOffset = (100 - progress) / 100 * circumference;
return /* @__PURE__ */ React4.createElement("div", {
className: "relative h-4 w-4"
}, /* @__PURE__ */ React4.createElement("svg", {
className: "absolute top-0 left-0",
viewBox: "0 0 100 100"
}, /* @__PURE__ */ React4.createElement("circle", {
className: "stroke-current opacity-10",
cx: "50",
cy: "50",
r: "40",
strokeWidth: "12",
fill: "none",
strokeDasharray: `${circumference} ${circumference}`
}), /* @__PURE__ */ React4.createElement("circle", {
className: `stroke-current`,
cx: "50",
cy: "50",
r: "40",
strokeWidth: "12",
fill: "none",
strokeDasharray: `${circumference} ${circumference}`,
strokeDashoffset: progressOffset,
transform: "rotate(-90 50 50)"
})));
};
export {
Button,
Input,
RadialProgressBar,
TextArea
};
+17
View File
@@ -0,0 +1,17 @@
// import * as React from "react";
// components
// export * from "./breadcrumbs";
// export * from "./button";
// export * from "./custom-listbox";
// export * from "./custom-menu";
// export * from "./custom-select";
// export * from "./empty-space";
// export * from "./header-button";
// export * from "./input";
// export * from "./loader";
// export * from "./outline-button";
// export * from "./select";
// export * from "./spinner";
// export * from "./text-area";
// export * from "./tooltip";
export * from "./button";
+10 -20
View File
@@ -1,33 +1,23 @@
{
"name": "@plane/ui",
"version": "0.0.1",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"sideEffects": false,
"name": "ui",
"version": "0.0.0",
"main": "./index.tsx",
"types": "./index.tsx",
"license": "MIT",
"files": [
"dist/**"
],
"scripts": {
"build": "tsup src/index.tsx --format esm,cjs --dts --external react",
"dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react",
"lint": "eslint src/",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
"lint": "eslint *.ts*"
},
"devDependencies": {
"@types/node": "^20.5.2",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"classnames": "^2.3.2",
"eslint": "^7.32.0",
"eslint-config-custom": "*",
"next": "12.3.2",
"react": "^18.2.0",
"tsconfig": "*",
"tailwind-config-custom": "*",
"tsup": "^5.10.1",
"typescript": "4.7.4"
},
"publishConfig": {
"access": "public"
}
}
-2
View File
@@ -1,2 +0,0 @@
export * from "./input";
export * from "./textarea";
-61
View File
@@ -1,61 +0,0 @@
import * as React from "react";
export interface InputProps {
type: string;
id: string;
value: string;
name: string;
onChange: () => void;
className?: string;
mode?: "primary" | "transparent" | "true-transparent";
size?: "sm" | "md" | "lg";
hasError?: boolean;
placeholder?: string;
disabled?: boolean;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const {
id,
type,
value,
name,
onChange,
className = "",
mode = "primary",
size = "md",
hasError = false,
placeholder = "",
disabled = false,
} = props;
return (
<input
id={id}
ref={ref}
type={type}
value={value}
name={name}
onChange={onChange}
placeholder={placeholder}
disabled={disabled}
className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${
mode === "primary"
? "rounded-md border border-custom-border-200"
: mode === "transparent"
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary"
: mode === "true-transparent"
? "rounded border-none bg-transparent ring-0"
: ""
} ${hasError ? "border-red-500" : ""} ${
hasError && mode === "primary" ? "bg-red-500/20" : ""
} ${
size === "sm" ? "px-3 py-2" : size === "lg" ? "p-3" : ""
} ${className}`}
/>
);
});
Input.displayName = "form-input-field";
export { Input };
-80
View File
@@ -1,80 +0,0 @@
import * as React from "react";
export interface TextAreaProps {
id: string;
name: string;
placeholder?: string;
value?: string;
rows?: number;
cols?: number;
disabled?: boolean;
onChange: () => void;
mode?: "primary" | "transparent";
hasError?: boolean;
className?: string;
}
// Updates the height of a <textarea> when the value changes.
const useAutoSizeTextArea = (
textAreaRef: HTMLTextAreaElement | null,
value: any
) => {
React.useEffect(() => {
if (textAreaRef) {
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
textAreaRef.style.height = "0px";
const scrollHeight = textAreaRef.scrollHeight;
// We then set the height directly, outside of the render loop
// Trying to set this with state or a ref will product an incorrect value.
textAreaRef.style.height = scrollHeight + "px";
}
}, [textAreaRef, value]);
};
const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
(props, ref) => {
const {
id,
name,
placeholder = "",
value = "",
rows = 1,
cols = 1,
disabled,
onChange,
mode = "primary",
hasError = false,
className = "",
} = props;
const textAreaRef = React.useRef<any>(ref);
ref && useAutoSizeTextArea(textAreaRef?.current, value);
return (
<textarea
id={id}
name={name}
ref={textAreaRef}
placeholder={placeholder}
value={value}
rows={rows}
cols={cols}
disabled={disabled}
onChange={onChange}
className={`no-scrollbar w-full bg-transparent placeholder-custom-text-400 px-3 py-2 outline-none ${
mode === "primary"
? "rounded-md border border-custom-border-200"
: mode === "transparent"
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme"
: ""
} ${hasError ? "border-red-500" : ""} ${
hasError && mode === "primary" ? "bg-red-100" : ""
} ${className}`}
/>
);
}
);
export { TextArea };
-3
View File
@@ -1,3 +0,0 @@
export * from "./buttons";
export * from "./form-fields";
export * from "./progress";
-1
View File
@@ -1 +0,0 @@
export * from "./radial-progress";
@@ -1,45 +0,0 @@
import React, { useState, useEffect, FC } from "react";
interface IRadialProgressBar {
progress: number;
}
export const RadialProgressBar: FC<IRadialProgressBar> = (props) => {
const { progress } = props;
const [circumference, setCircumference] = useState(0);
useEffect(() => {
const radius = 40;
const circumference = 2 * Math.PI * radius;
setCircumference(circumference);
}, []);
const progressOffset = ((100 - progress) / 100) * circumference;
return (
<div className="relative h-4 w-4">
<svg className="absolute top-0 left-0" viewBox="0 0 100 100">
<circle
className={"stroke-current opacity-10"}
cx="50"
cy="50"
r="40"
strokeWidth="12"
fill="none"
strokeDasharray={`${circumference} ${circumference}`}
/>
<circle
className={`stroke-current`}
cx="50"
cy="50"
r="40"
strokeWidth="12"
fill="none"
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={progressOffset}
transform="rotate(-90 50 50)"
/>
</svg>
</div>
);
};
-3
View File
@@ -1,8 +1,5 @@
{
"extends": "tsconfig/react-library.json",
"compilerOptions": {
"jsx": "react"
},
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}
+12 -1
View File
@@ -10,4 +10,15 @@ cp ./space/.env.example ./space/.env
cp ./apiserver/.env.example ./apiserver/.env
# Generate the SECRET_KEY that will be used by django
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
# Generate Prompt for taking tiptap auth key
echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n"
echo -e "\e[1;38m 1. Head over to TipTap cloud's Pro Extensions Page, https://collab.tiptap.dev/pro-extensions \e[0m"
echo -e "\e[1;38m 2. Copy the token given to you under the first paragraph, after 'Here it is' \e[0m \n"
read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken
echo "@tiptap-pro:registry=https://registry.tiptap.dev/
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc
@@ -18,6 +18,7 @@ import Gapcursor from "@tiptap/extension-gapcursor";
import ts from "highlight.js/lib/languages/typescript";
import "highlight.js/styles/github-dark.css";
import UniqueID from "@tiptap-pro/extension-unique-id";
import UpdatedImage from "./updated-image";
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
import { CustomTableCell } from "./table/table-cell";
@@ -120,6 +121,9 @@ export const TiptapExtensions = (
},
includeChildren: true,
}),
UniqueID.configure({
types: ["image"],
}),
SlashCommand(workspaceSlug, setIsSubmitting),
TiptapUnderline,
TextStyle,
+1
View File
@@ -17,6 +17,7 @@
"@heroicons/react": "^2.0.12",
"@mui/icons-material": "^5.14.1",
"@mui/material": "^5.14.1",
"@tiptap-pro/extension-unique-id": "^2.1.0",
"@tiptap/extension-code-block-lowlight": "^2.0.4",
"@tiptap/extension-color": "^2.0.4",
"@tiptap/extension-gapcursor": "^2.1.7",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"printWidth": 120,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "es5"
}
@@ -9,6 +9,7 @@ import { findStringWithMostCharacters } from "helpers/array.helper";
import { generateBarColor } from "helpers/analytics.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse } from "types";
// constants
type Props = {
analytics: IAnalyticsResponse;
@@ -7,14 +7,19 @@ import analyticsService from "services/analytics.service";
import projectService from "services/project.service";
import cyclesService from "services/cycles.service";
import modulesService from "services/modules.service";
import trackEventServices from "services/track_event.service";
import trackEventServices from "services/track-event.service";
// hooks
import useProjects from "hooks/use-projects";
import useToast from "hooks/use-toast";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { ArrowDownTrayIcon, ArrowPathIcon, CalendarDaysIcon, UserGroupIcon } from "@heroicons/react/24/outline";
import {
ArrowDownTrayIcon,
ArrowPathIcon,
CalendarDaysIcon,
UserGroupIcon,
} from "@heroicons/react/24/outline";
import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
// helpers
import { renderShortDate } from "helpers/date-time.helper";
@@ -41,7 +46,13 @@ type Props = {
user: ICurrentUserResponse | undefined;
};
export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScreen, isProjectLevel = false, user }) => {
export const AnalyticsSidebar: React.FC<Props> = ({
analytics,
params,
fullScreen,
isProjectLevel = false,
user,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@@ -50,7 +61,9 @@ export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScree
const { setToastAlert } = useToast();
const { data: projectDetails } = useSWR(
workspaceSlug && projectId && !(cycleId || moduleId) ? PROJECT_DETAILS(projectId.toString()) : null,
workspaceSlug && projectId && !(cycleId || moduleId)
? PROJECT_DETAILS(projectId.toString())
: null,
workspaceSlug && projectId && !(cycleId || moduleId)
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
: null
@@ -59,14 +72,24 @@ export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScree
const { data: cycleDetails } = useSWR(
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
workspaceSlug && projectId && cycleId
? () => cyclesService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
? () =>
cyclesService.getCycleDetails(
workspaceSlug.toString(),
projectId.toString(),
cycleId.toString()
)
: null
);
const { data: moduleDetails } = useSWR(
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
workspaceSlug && projectId && moduleId
? () => modulesService.getModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
? () =>
modulesService.getModuleDetails(
workspaceSlug.toString(),
projectId.toString(),
moduleId.toString()
)
: null
);
@@ -155,7 +178,8 @@ export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScree
);
};
const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
const selectedProjects =
params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
return (
<div
@@ -212,7 +236,9 @@ export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScree
)}
<h5 className="flex items-center gap-1">
<p className="break-words">{truncateText(project.name, 20)}</p>
<span className="text-custom-text-200 text-xs ml-1">({project.identifier})</span>
<span className="text-custom-text-200 text-xs ml-1">
({project.identifier})
</span>
</h5>
</div>
<div className="mt-4 space-y-3 pl-2 w-full">
@@ -318,7 +344,10 @@ export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScree
<div className="space-y-4 mt-4">
<div className="flex items-center gap-2 text-xs">
<h6 className="text-custom-text-200">Network</h6>
<span>{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ?? ""}</span>
<span>
{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ??
""}
</span>
</div>
</div>
</div>
+28 -10
View File
@@ -13,11 +13,15 @@ import analyticsService from "services/analytics.service";
import projectService from "services/project.service";
import cyclesService from "services/cycles.service";
import modulesService from "services/modules.service";
import trackEventServices from "services/track_event.service";
import trackEventServices from "services/track-event.service";
// components
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
// icons
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, XMarkIcon } from "@heroicons/react/24/outline";
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
// types
import { IAnalyticsParams, IWorkspace } from "types";
// fetch-keys
@@ -63,7 +67,9 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
);
const { data: projectDetails } = useSWR(
workspaceSlug && projectId && !(cycleId || moduleId) ? PROJECT_DETAILS(projectId.toString()) : null,
workspaceSlug && projectId && !(cycleId || moduleId)
? PROJECT_DETAILS(projectId.toString())
: null,
workspaceSlug && projectId && !(cycleId || moduleId)
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
: null
@@ -72,14 +78,24 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
const { data: cycleDetails } = useSWR(
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
workspaceSlug && projectId && cycleId
? () => cyclesService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
? () =>
cyclesService.getCycleDetails(
workspaceSlug.toString(),
projectId.toString(),
cycleId.toString()
)
: null
);
const { data: moduleDetails } = useSWR(
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
workspaceSlug && projectId && moduleId
? () => modulesService.getModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
? () =>
modulesService.getModuleDetails(
workspaceSlug.toString(),
projectId.toString(),
moduleId.toString()
)
: null
);
@@ -118,7 +134,8 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
eventPayload.moduleName = moduleDetails.name;
}
const eventType = tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS";
const eventType =
tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS";
trackEventServices.trackAnalyticsEvent(
eventPayload,
@@ -133,9 +150,9 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
return (
<div
className={`absolute top-0 z-30 h-full bg-custom-background-90 ${fullScreen ? "p-2 w-full" : "w-1/2"} ${
isOpen ? "right-0" : "-right-full"
} duration-300 transition-all`}
className={`absolute top-0 z-30 h-full bg-custom-background-90 ${
fullScreen ? "p-2 w-full" : "w-1/2"
} ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`}
>
<div
className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${
@@ -144,7 +161,8 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
>
<div className="flex items-center justify-between gap-4 bg-custom-background-100 px-5 py-4 text-sm">
<h3 className="break-words">
Analytics for {cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
Analytics for{" "}
{cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
</h3>
<div className="flex items-center gap-2">
<button
@@ -20,15 +20,18 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
{
id: "issues_closed",
color: "rgb(var(--color-primary-100))",
data: Object.entries(MONTHS_LIST).map(([index, month]) => ({
x: month.shortTitle,
data: MONTHS_LIST.map((month) => ({
x: month.label.substring(0, 3),
y:
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10))?.count ||
0,
defaultAnalytics.issue_completed_month_wise.find(
(data) => data.month === month.value
)?.count || 0,
})),
},
]}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map(
(data) => data.count
)}
height="300px"
colors={(datum) => datum.color}
curve="monotoneX"
@@ -12,7 +12,7 @@ import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { StateGroupIcon } from "components/icons";
import { ArchiveX } from "lucide-react";
// services
import stateService from "services/project_state.service";
import stateService from "services/state.service";
// constants
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
import { STATES_LIST } from "constants/fetch-keys";
@@ -34,7 +34,9 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const states = getStatesList(stateGroups);
@@ -55,7 +57,9 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null;
const selectedOption = states?.find((s) => s.id === projectDetails?.default_state ?? defaultState);
const selectedOption = states?.find(
(s) => s.id === projectDetails?.default_state ?? defaultState
);
const currentDefaultState = states?.find((s) => s.id === defaultState);
const initialValues: Partial<IProject> = {
@@ -101,11 +105,15 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
<div className="ml-12">
<div className="flex flex-col rounded bg-custom-background-90 border border-custom-border-200 p-2">
<div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
<div className="w-1/2 text-sm font-medium">Auto-close issues that are inactive for</div>
<div className="w-1/2 text-sm font-medium">
Auto-close issues that are inactive for
</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.close_in}
label={`${projectDetails?.close_in} ${projectDetails?.close_in === 1 ? "Month" : "Months"}`}
label={`${projectDetails?.close_in} ${
projectDetails?.close_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ close_in: val });
}}
@@ -134,7 +142,9 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
<div className="w-1/2 ">
<CustomSearchSelect
value={projectDetails?.default_state ? projectDetails?.default_state : defaultState}
value={
projectDetails?.default_state ? projectDetails?.default_state : defaultState
}
label={
<div className="flex items-center gap-2">
{selectedOption ? (
@@ -156,7 +166,9 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
)}
{selectedOption?.name
? selectedOption.name
: currentDefaultState?.name ?? <span className="text-custom-text-200">State</span>}
: currentDefaultState?.name ?? (
<span className="text-custom-text-200">State</span>
)}
</div>
}
onChange={(val: string) => {
+42 -14
View File
@@ -10,7 +10,7 @@ import { Command } from "cmdk";
import { Dialog, Transition } from "@headlessui/react";
// services
import workspaceService from "services/workspace.service";
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
import inboxService from "services/inbox.service";
// hooks
import useProjectDetails from "hooks/use-project-details";
@@ -79,13 +79,16 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () => issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
? () =>
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
: null
);
const { data: inboxList } = useSWR(
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
workspaceSlug && projectId ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string) : null
workspaceSlug && projectId
? () => inboxService.getInboxes(workspaceSlug as string, projectId as string)
: null
);
const updateIssue = useCallback(
@@ -269,7 +272,8 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
>
{issueDetails && (
<div className="overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name}
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}{" "}
{issueDetails.name}
</div>
)}
{projectId && (
@@ -320,9 +324,12 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</h5>
)}
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-custom-text-200">No results found.</div>
)}
{!isLoading &&
resultsCount === 0 &&
searchTerm !== "" &&
debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-custom-text-200">No results found.</div>
)}
{(isLoading || isSearching) && (
<Command.Loading>
@@ -355,7 +362,9 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
>
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
<Icon iconName={currentSection.icon} />
<p className="block flex-1 truncate">{currentSection.itemName(item)}</p>
<p className="block flex-1 truncate">
{currentSection.itemName(item)}
</p>
</div>
</Command.Item>
))}
@@ -568,7 +577,9 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
redirect(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`);
redirect(
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`
);
}}
className="focus:outline-none"
>
@@ -661,7 +672,10 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
window.open(
"https://github.com/makeplane/plane/issues/new/choose",
"_blank"
);
}}
className="focus:outline-none"
>
@@ -745,15 +759,29 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</>
)}
{page === "change-issue-state" && issueDetails && (
<ChangeIssueState issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
<ChangeIssueState
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
)}
{page === "change-issue-priority" && issueDetails && (
<ChangeIssuePriority issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
<ChangeIssuePriority
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
)}
{page === "change-issue-assignee" && issueDetails && (
<ChangeIssueAssignee issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
<ChangeIssueAssignee
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
)}
{page === "change-interface-theme" && (
<ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />
)}
{page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />}
</Command.List>
</Command>
</Dialog.Panel>
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// hooks
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
@@ -17,11 +17,14 @@ import { CreateUpdatePageModal } from "components/pages";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// services
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
import inboxService from "services/inbox.service";
// fetch keys
import { ISSUE_DETAILS } from "constants/fetch-keys";
import { INBOX_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { observable } from "mobx";
import { observer } from "mobx-react-lite";
export const CommandPalette: React.FC = observer(() => {
const store: any = useMobxStore();
@@ -47,7 +50,8 @@ export const CommandPalette: React.FC = observer(() => {
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () => issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
? () =>
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
: null
);
@@ -72,7 +76,7 @@ export const CommandPalette: React.FC = observer(() => {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const { key, ctrlKey, metaKey, altKey } = e;
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
if (!key) return;
const keyPressed = key.toLowerCase();
@@ -137,7 +141,11 @@ export const CommandPalette: React.FC = observer(() => {
<>
<ShortcutsModal isOpen={isShortcutsModalOpen} setIsOpen={setIsShortcutsModalOpen} />
{workspaceSlug && (
<CreateProjectModal isOpen={isProjectModalOpen} setIsOpen={setIsProjectModalOpen} user={user} />
<CreateProjectModal
isOpen={isProjectModalOpen}
setIsOpen={setIsProjectModalOpen}
user={user}
/>
)}
{projectId && (
<>
@@ -176,7 +184,11 @@ export const CommandPalette: React.FC = observer(() => {
handleClose={() => setIsIssueModalOpen(false)}
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
prePopulateData={
cycleId ? { cycle: cycleId.toString() } : moduleId ? { module: moduleId.toString() } : undefined
cycleId
? { cycle: cycleId.toString() }
: moduleId
? { module: moduleId.toString() }
: undefined
}
/>
<BulkDeleteIssuesModal
@@ -184,7 +196,11 @@ export const CommandPalette: React.FC = observer(() => {
setIsOpen={setIsBulkDeleteIssuesModalOpen}
user={user}
/>
<CommandK deleteIssue={deleteIssue} isPaletteOpen={isPaletteOpen} setIsPaletteOpen={setIsPaletteOpen} />
<CommandK
deleteIssue={deleteIssue}
isPaletteOpen={isPaletteOpen}
setIsPaletteOpen={setIsPaletteOpen}
/>
</>
);
});
@@ -7,7 +7,7 @@ import { mutate } from "swr";
// cmdk
import { Command } from "cmdk";
// services
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
// hooks
import useProjectMembers from "hooks/use-project-members";
// constants
@@ -7,7 +7,7 @@ import { mutate } from "swr";
// cmdk
import { Command } from "cmdk";
// services
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
// types
import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
// constants
@@ -64,7 +64,11 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
return (
<>
{PRIORITIES.map((priority) => (
<Command.Item key={priority} onSelect={() => handleIssueState(priority)} className="focus:outline-none">
<Command.Item
key={priority}
onSelect={() => handleIssueState(priority)}
className="focus:outline-none"
>
<div className="flex items-center space-x-3">
<PriorityIcon priority={priority} />
<span className="capitalize">{priority ?? "None"}</span>
@@ -7,8 +7,8 @@ import useSWR, { mutate } from "swr";
// cmdk
import { Command } from "cmdk";
// services
import issuesService from "services/issue.service";
import stateService from "services/project_state.service";
import issuesService from "services/issues.service";
import stateService from "services/state.service";
// ui
import { Spinner } from "components/ui";
// icons
@@ -32,7 +32,9 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
const { data: stateGroups, mutate: mutateIssueDetails } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const states = getStatesList(stateGroups);
@@ -76,9 +78,18 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
{states ? (
states.length > 0 ? (
states.map((state) => (
<Command.Item key={state.id} onSelect={() => handleIssueState(state.id)} className="focus:outline-none">
<Command.Item
key={state.id}
onSelect={() => handleIssueState(state.id)}
className="focus:outline-none"
>
<div className="flex items-center space-x-3">
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
<StateGroupIcon
stateGroup={state.group}
color={state.color}
height="16px"
width="16px"
/>
<p>{state.name}</p>
</div>
<div>{state.id === issue.state && <CheckIcon className="h-3 w-3" />}</div>
+47 -39
View File
@@ -1,9 +1,5 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import issuesService from "services/issue.service";
// icons
import { Icon, Tooltip } from "components/ui";
import { CopyPlus } from "lucide-react";
@@ -14,22 +10,26 @@ import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper";
// types
import { IIssueActivity } from "types";
// fetch-keys
import { WORKSPACE_LABELS } from "constants/fetch-keys";
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<Tooltip tooltipContent={activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}>
<Tooltip
tooltipContent={
activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"
}
>
<a
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"}
{activity.issue_detail
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
: "Issue"}
<Icon iconName="launch" className="!text-xs" />
</a>
</Tooltip>
@@ -52,29 +52,13 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
);
};
const LabelPill = ({ labelId }: { labelId: string }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: labels } = useSWR(
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null
);
return (
<span
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: labels?.find((l) => l.id === labelId)?.color ?? "#000000",
}}
aria-hidden="true"
/>
);
};
const activityDetails: {
[key: string]: {
message: (activity: IIssueActivity, showIssue: boolean, workspaceSlug: string) => React.ReactNode;
message: (
activity: IIssueActivity,
showIssue: boolean,
workspaceSlug: string
) => React.ReactNode;
icon: React.ReactNode;
};
} = {
@@ -167,7 +151,8 @@ const activityDetails: {
else
return (
<>
removed the blocking issue <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
removed the blocking issue{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
@@ -223,7 +208,8 @@ const activityDetails: {
else
return (
<>
removed the relation from <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
removed the relation from{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
@@ -312,7 +298,8 @@ const activityDetails: {
else
return (
<>
set the estimate point to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
set the estimate point to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
@@ -338,8 +325,14 @@ const activityDetails: {
return (
<>
added a new label{" "}
<span className="inline-flex items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
<LabelPill labelId={activity.new_identifier ?? ""} />
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
<span
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: "#000000",
}}
aria-hidden="true"
/>
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
</span>
{showIssue && (
@@ -355,7 +348,13 @@ const activityDetails: {
<>
removed the label{" "}
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
<LabelPill labelId={activity.old_identifier ?? ""} />
<span
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: "#000000",
}}
aria-hidden="true"
/>
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
</span>
{showIssue && (
@@ -510,7 +509,8 @@ const activityDetails: {
if (!activity.new_value)
return (
<>
removed the parent <span className="font-medium text-custom-text-100">{activity.old_value}</span>
removed the parent{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
{showIssue && (
<>
{" "}
@@ -523,7 +523,8 @@ const activityDetails: {
else
return (
<>
set the parent to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
set the parent to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
@@ -591,7 +592,8 @@ const activityDetails: {
state: {
message: (activity, showIssue) => (
<>
set the state to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
set the state to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
@@ -643,7 +645,13 @@ export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
);
export const ActivityMessage = ({ activity, showIssue = false }: { activity: IIssueActivity; showIssue?: boolean }) => {
export const ActivityMessage = ({
activity,
showIssue = false,
}: {
activity: IIssueActivity;
showIssue?: boolean;
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -1,8 +1,11 @@
import { Fragment } from "react";
import { Controller, useForm } from "react-hook-form";
import DatePicker from "react-datepicker";
import { Dialog, Transition } from "@headlessui/react";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// react-datepicker
import DatePicker from "react-datepicker";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// components
import { DateFilterSelect } from "./date-filter-select";
// ui
@@ -11,12 +14,15 @@ import { PrimaryButton, SecondaryButton } from "components/ui";
import { XMarkIcon } from "@heroicons/react/20/solid";
// helpers
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { IIssueFilterOptions } from "types";
type Props = {
title: string;
field: keyof IIssueFilterOptions;
filters: IIssueFilterOptions;
handleClose: () => void;
isOpen: boolean;
onSelect: (val: string[]) => void;
onSelect: (option: any) => void;
};
type TFormValues = {
@@ -31,7 +37,14 @@ const defaultValues: TFormValues = {
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
};
export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, onSelect }) => {
export const DateFilterModal: React.FC<Props> = ({
title,
field,
filters,
handleClose,
isOpen,
onSelect,
}) => {
const { handleSubmit, watch, control } = useForm<TFormValues>({
defaultValues,
});
@@ -39,13 +52,32 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
const handleFormSubmit = (formData: TFormValues) => {
const { filterType, date1, date2 } = formData;
if (filterType === "range") onSelect([`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`]);
else onSelect([`${renderDateFormat(date1)};${filterType}`]);
if (filterType === "range") {
onSelect({
key: field,
value: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`],
});
} else {
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
if (item?.includes(filterType)) return false;
return true;
});
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
if (filterOne)
onSelect({ key: field, value: [filterOne, `${renderDateFormat(date1)};${filterType}`] });
else
onSelect({
key: field,
value: [`${renderDateFormat(date1)};${filterType}`],
});
}
handleClose();
};
const isInvalid = watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
const isInvalid =
watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
const nextDay = new Date(watch("date1"));
nextDay.setDate(nextDay.getDate() + 1);
@@ -85,7 +117,10 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
<DateFilterSelect title={title} value={value} onChange={onChange} />
)}
/>
<XMarkIcon className="border-base h-4 w-4 cursor-pointer" onClick={handleClose} />
<XMarkIcon
className="border-base h-4 w-4 cursor-pointer"
onClick={handleClose}
/>
</div>
<div className="flex w-full justify-between gap-4">
<Controller
@@ -130,7 +165,11 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
<SecondaryButton className="flex items-center gap-2" onClick={handleClose}>
Cancel
</SecondaryButton>
<PrimaryButton type="submit" className="flex items-center gap-2" disabled={isInvalid}>
<PrimaryButton
type="submit"
className="flex items-center gap-2"
disabled={isInvalid}
>
Apply
</PrimaryButton>
</div>
@@ -25,11 +25,11 @@ import {
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { Properties, TIssueLayouts } from "types";
import { Properties, TIssueViewOptions } from "types";
// constants
import { ISSUE_GROUP_BY_OPTIONS, ISSUE_ORDER_BY_OPTIONS, ISSUE_FILTER_OPTIONS } from "constants/issue";
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
{
type: "list",
Icon: FormatListBulletedOutlined,
@@ -52,7 +52,7 @@ const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
},
];
const issueViewForDraftIssues: { type: TIssueLayouts; Icon: any }[] = [
const issueViewForDraftIssues: { type: TIssueViewOptions; Icon: any }[] = [
{
type: "list",
Icon: FormatListBulletedOutlined,
@@ -69,10 +69,19 @@ export const IssuesFilterView: React.FC = () => {
const isArchivedIssues = router.pathname.includes("archived-issues");
const isDraftIssues = router.pathname.includes("draft-issues");
const { displayFilters, setDisplayFilters, filters, setFilters, resetFilterToDefault, setNewFilterDefaultView } =
useIssuesView();
const {
displayFilters,
setDisplayFilters,
filters,
setFilters,
resetFilterToDefault,
setNewFilterDefaultView,
} = useIssuesView();
const [properties, setProperties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const [properties, setProperties] = useIssuesProperties(
workspaceSlug as string,
projectId as string
);
const { isEstimateActive } = useEstimateOption();
@@ -83,7 +92,9 @@ export const IssuesFilterView: React.FC = () => {
{issueViewOptions.map((option) => (
<Tooltip
key={option.type}
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
position="bottom"
>
<button
@@ -111,7 +122,9 @@ export const IssuesFilterView: React.FC = () => {
{issueViewForDraftIssues.map((option) => (
<Tooltip
key={option.type}
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
position="bottom"
>
<button
@@ -151,7 +164,9 @@ export const IssuesFilterView: React.FC = () => {
if (valueExists)
setFilters(
{
[option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
},
!Boolean(viewId)
);
@@ -172,7 +187,9 @@ export const IssuesFilterView: React.FC = () => {
<>
<Popover.Button
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 ? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100" : "text-custom-sidebar-text-200"
open
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"
}`}
>
Display
@@ -199,24 +216,24 @@ export const IssuesFilterView: React.FC = () => {
<div className="w-28">
<CustomMenu
label={
ISSUE_GROUP_BY_OPTIONS.find((option) => option.key === displayFilters.group_by)
?.title ?? "Select"
GROUP_BY_OPTIONS.find(
(option) => option.key === displayFilters.group_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ISSUE_GROUP_BY_OPTIONS.map((option) => {
if (displayFilters.layout === "kanban" && option.key === null) return null;
{GROUP_BY_OPTIONS.map((option) => {
if (displayFilters.layout === "kanban" && option.key === null)
return null;
if (option.key === "project") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
// setDisplayFilters({ group_by: option.key })
}}
onClick={() => setDisplayFilters({ group_by: option.key })}
>
{option.title}
{option.name}
</CustomMenu.MenuItem>
);
})}
@@ -224,71 +241,79 @@ export const IssuesFilterView: React.FC = () => {
</div>
</div>
)}
{displayFilters.layout !== "calendar" && displayFilters.layout !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ISSUE_ORDER_BY_OPTIONS.find((option) => option.key === displayFilters.order_by)?.title ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ISSUE_ORDER_BY_OPTIONS.map((option) =>
displayFilters.group_by === "priority" && option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
// setDisplayFilters({ order_by: option.key });
}}
>
{option.title}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find(
(option) => option.key === displayFilters.order_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) =>
displayFilters.group_by === "priority" &&
option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
</div>
</div>
)}
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu
label={
ISSUE_FILTER_OPTIONS.find((option) => option.key === displayFilters.type)?.title ?? "Select"
FILTER_ISSUE_OPTIONS.find(
(option) => option.key === displayFilters.type
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ISSUE_FILTER_OPTIONS.map((option) => (
{FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
// setDisplayFilters({
// type: option.key,
// })
}}
onClick={() =>
setDisplayFilters({
type: option.key,
})
}
>
{option.title}
{option.name}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
</div>
{displayFilters.layout !== "calendar" && displayFilters.layout !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show sub-issues</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters.sub_issue ?? true}
onChange={() => setDisplayFilters({ sub_issue: !displayFilters.sub_issue })}
/>
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show sub-issues</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters.sub_issue ?? true}
onChange={() =>
setDisplayFilters({ sub_issue: !displayFilters.sub_issue })
}
/>
</div>
</div>
</div>
)}
)}
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" &&
displayFilters.layout !== "gantt_chart" && (
@@ -333,11 +358,16 @@ export const IssuesFilterView: React.FC = () => {
if (
displayFilters.layout === "spreadsheet" &&
(key === "attachment_count" || key === "link" || key === "sub_issue_count")
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
)
return null;
if (displayFilters.layout !== "spreadsheet" && (key === "created_on" || key === "updated_on"))
if (
displayFilters.layout !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;
return (
@@ -9,10 +9,11 @@ import { SubmitHandler, useForm } from "react-hook-form";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// services
import issuesServices from "services/issue.service";
import issuesServices from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
// ui
import { DangerButton, SecondaryButton } from "components/ui";
// icons
@@ -48,12 +49,17 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { data: issues } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
workspaceSlug && projectId ? () => issuesServices.getIssues(workspaceSlug as string, projectId as string) : null
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
: null
);
const { setToastAlert } = useToast();
const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { order_by, group_by, ...viewGanttParams } = params;
const {
@@ -88,6 +94,14 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids];
const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
: viewId
? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams);
const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId
@@ -112,7 +126,8 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
message: "Issues deleted successfully!",
});
if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
else {
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
@@ -140,7 +155,9 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
: issues?.filter(
(issue) =>
issue.name.toLowerCase().includes(query.toLowerCase()) ||
`${issue.project_detail.identifier}-${issue.sequence_id}`.toLowerCase().includes(query.toLowerCase())
`${issue.project_detail.identifier}-${issue.sequence_id}`
.toLowerCase()
.includes(query.toLowerCase())
) ?? [];
return (
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
// services
import aiService from "services/ai.service";
import trackEventServices from "services/track_event.service";
import trackEventServices from "services/track-event.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
@@ -110,7 +110,9 @@ export const GptAssistantModal: React.FC<Props> = ({
setToastAlert({
type: "error",
title: "Error!",
message: error || "You have reached the maximum number of requests of 50 requests per month per user.",
message:
error ||
"You have reached the maximum number of requests of 50 requests per month per user.",
});
else
setToastAlert({
@@ -164,7 +166,8 @@ export const GptAssistantModal: React.FC<Props> = ({
)}
{invalidResponse && (
<div className="text-sm text-red-500">
No response could be generated. This may be due to insufficient content or task information. Please try again.
No response could be generated. This may be due to insufficient content or task
information. Please try again.
</div>
)}
<Input
@@ -172,7 +175,9 @@ export const GptAssistantModal: React.FC<Props> = ({
name="task"
register={register}
placeholder={`${
content && content !== "" ? "Tell AI what action to perform on this content..." : "Ask AI anything..."
content && content !== ""
? "Tell AI what action to perform on this content..."
: "Ask AI anything..."
}`}
autoComplete="off"
/>
@@ -182,8 +187,18 @@ export const GptAssistantModal: React.FC<Props> = ({
onClick={() => {
onResponse(response);
onClose();
if (block) trackEventServices.trackUseGPTResponseEvent(block, "USE_GPT_RESPONSE_IN_PAGE_BLOCK", user);
else if (issue) trackEventServices.trackUseGPTResponseEvent(issue, "USE_GPT_RESPONSE_IN_ISSUE", user);
if (block)
trackEventServices.trackUseGPTResponseEvent(
block,
"USE_GPT_RESPONSE_IN_PAGE_BLOCK",
user
);
else if (issue)
trackEventServices.trackUseGPTResponseEvent(
issue,
"USE_GPT_RESPONSE_IN_ISSUE",
user
);
}}
>
Use this response
@@ -191,8 +206,16 @@ export const GptAssistantModal: React.FC<Props> = ({
)}
<div className="flex items-center gap-2">
<SecondaryButton onClick={onClose}>Close</SecondaryButton>
<PrimaryButton type="button" onClick={handleSubmit(handleResponse)} loading={isSubmitting}>
{isSubmitting ? "Generating response..." : response === "" ? "Generate response" : "Generate again"}
<PrimaryButton
type="button"
onClick={handleSubmit(handleResponse)}
loading={isSubmitting}
>
{isSubmitting
? "Generating response..."
: response === ""
? "Generate response"
: "Generate again"}
</PrimaryButton>
</div>
</div>
+210 -52
View File
@@ -1,66 +1,224 @@
import React from "react";
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// react-beautiful-dnd
import { DragDropContext, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services
import stateService from "services/state.service";
// hooks
import useUser from "hooks/use-user";
import { useProjectMyMembership } from "contexts/project-member.context";
// components
import {
AppliedFiltersRoot,
ListLayout,
CalendarLayout,
GanttLayout,
KanBanLayout,
SpreadsheetLayout,
} from "components/issues";
AllLists,
AllBoards,
CalendarView,
SpreadsheetView,
GanttChartView,
} from "components/core";
// ui
import { EmptyState, Spinner } from "components/ui";
// icons
import { TrashIcon } from "@heroicons/react/24/outline";
// images
import emptyIssue from "public/empty-state/issue.svg";
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
import { IIssue, IIssueViewProps } from "types";
// fetch-keys
import { STATES_LIST } from "constants/fetch-keys";
export const AllViews: React.FC = observer(() => {
type Props = {
addIssueToDate: (date: string) => void;
addIssueToGroup: (groupTitle: string) => void;
disableUserActions: boolean;
dragDisabled?: boolean;
emptyState: {
title: string;
description?: string;
primaryButton?: {
icon: any;
text: string;
onClick: () => void;
};
secondaryButton?: React.ReactNode;
};
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
handleOnDragEnd: (result: DropResult) => Promise<void>;
openIssuesListModal: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
disableAddIssueOption?: boolean;
trashBox: boolean;
setTrashBox: React.Dispatch<React.SetStateAction<boolean>>;
viewProps: IIssueViewProps;
};
export const AllViews: React.FC<Props> = ({
addIssueToDate,
addIssueToGroup,
disableUserActions,
dragDisabled = false,
emptyState,
handleIssueAction,
handleDraftIssueAction,
handleOnDragEnd,
openIssuesListModal,
removeIssue,
disableAddIssueOption = false,
trashBox,
setTrashBox,
viewProps,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query as {
workspaceSlug: string;
projectId: string;
cycleId: string;
moduleId: string;
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const [myIssueProjectId, setMyIssueProjectId] = useState<string | null>(null);
const { user } = useUser();
const { memberRole } = useProjectMyMembership();
const { groupedIssues, isEmpty, displayFilters } = viewProps;
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const states = getStatesList(stateGroups);
const handleMyIssueOpen = (issue: IIssue) => {
setMyIssueProjectId(issue.project);
};
const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES` : null,
async () => {
if (workspaceSlug && projectId) {
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
await projectStore.fetchProjectStates(workspaceSlug, projectId);
await projectStore.fetchProjectLabels(workspaceSlug, projectId);
await projectStore.fetchProjectMembers(workspaceSlug, projectId);
await projectStore.fetchProjectEstimates(workspaceSlug, projectId);
await issueStore.fetchIssues(workspaceSlug, projectId);
}
const handleTrashBox = useCallback(
(isDragging: boolean) => {
if (isDragging && !trashBox) setTrashBox(true);
},
{ revalidateOnFocus: false }
[trashBox, setTrashBox]
);
const activeLayout = issueFilterStore.userDisplayFilters.layout;
return (
<div className="relative w-full h-full flex flex-col overflow-auto">
<AppliedFiltersRoot />
<div className="w-full h-full">
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<SpreadsheetLayout />
) : null}
</div>
</div>
<DragDropContext onDragEnd={handleOnDragEnd}>
<StrictModeDroppable droppableId="trashBox">
{(provided, snapshot) => (
<div
className={`${
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
} fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
} transition duration-300`}
ref={provided.innerRef}
{...provided.droppableProps}
>
<TrashIcon className="h-4 w-4" />
Drop here to delete the issue.
</div>
)}
</StrictModeDroppable>
{groupedIssues ? (
!isEmpty ||
displayFilters?.layout === "kanban" ||
displayFilters?.layout === "calendar" ||
displayFilters?.layout === "gantt_chart" ? (
<>
{displayFilters?.layout === "list" ? (
<AllLists
states={states}
addIssueToGroup={addIssueToGroup}
handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
removeIssue={removeIssue}
myIssueProjectId={myIssueProjectId}
handleMyIssueOpen={handleMyIssueOpen}
disableUserActions={disableUserActions}
disableAddIssueOption={disableAddIssueOption}
user={user}
userAuth={memberRole}
viewProps={viewProps}
/>
) : displayFilters?.layout === "kanban" ? (
<AllBoards
addIssueToGroup={addIssueToGroup}
disableUserActions={disableUserActions}
disableAddIssueOption={disableAddIssueOption}
dragDisabled={dragDisabled}
handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
handleTrashBox={handleTrashBox}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
myIssueProjectId={myIssueProjectId}
handleMyIssueOpen={handleMyIssueOpen}
removeIssue={removeIssue}
states={states}
user={user}
userAuth={memberRole}
viewProps={viewProps}
/>
) : displayFilters?.layout === "calendar" ? (
<CalendarView
handleIssueAction={handleIssueAction}
addIssueToDate={addIssueToDate}
disableUserActions={disableUserActions}
user={user}
userAuth={memberRole}
/>
) : displayFilters?.layout === "spreadsheet" ? (
<SpreadsheetView
handleIssueAction={handleIssueAction}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
disableUserActions={disableUserActions}
user={user}
userAuth={memberRole}
/>
) : (
displayFilters?.layout === "gantt_chart" && (
<GanttChartView disableUserActions={disableUserActions} />
)
)}
</>
) : router.pathname.includes("archived-issues") ? (
<EmptyState
title="Archived Issues will be shown here"
description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
image={emptyIssueArchive}
primaryButton={{
text: "Go to Automation Settings",
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
},
}}
/>
) : (
<EmptyState
title={emptyState.title}
description={emptyState.description}
image={emptyIssue}
primaryButton={
emptyState.primaryButton
? {
icon: emptyState.primaryButton.icon,
text: emptyState.primaryButton.text,
onClick: emptyState.primaryButton.onClick,
}
: undefined
}
secondaryButton={emptyState.secondaryButton}
/>
)
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</DragDropContext>
);
});
};
@@ -68,18 +68,23 @@ export const AllBoards: React.FC<Props> = ({
return (
<>
<IssuePeekOverview
handleMutation={() => (isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues())}
handleMutation={() =>
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
}
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
{groupedIssues ? (
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-5 bg-custom-background-90">
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0) return null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
return null;
return (
<SingleBoard
@@ -108,13 +113,15 @@ export const AllBoards: React.FC<Props> = ({
<div className="space-y-3">
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
if (groupedIssues[singleGroup].length === 0)
return (
<div
key={index}
className="flex items-center justify-between gap-2 rounded bg-custom-background-100 p-2 shadow-custom-shadow-2xs"
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
>
<div className="flex items-center gap-2">
{currentState && (
@@ -5,7 +5,7 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
import projectService from "services/project.service";
// hooks
import useProjects from "hooks/use-projects";
@@ -50,6 +50,8 @@ export const BoardHeader: React.FC<Props> = ({
const { displayFilters, groupedIssues } = viewProps;
console.log("dF", displayFilters);
const { data: issueLabels } = useSWR(
workspaceSlug && projectId && displayFilters?.group_by === "labels"
? PROJECT_ISSUE_LABELS(projectId.toString())
@@ -104,7 +106,12 @@ export const BoardHeader: React.FC<Props> = ({
switch (displayFilters?.group_by) {
case "state":
icon = currentState && (
<StateGroupIcon stateGroup={currentState.group} color={currentState.color} height="16px" width="16px" />
<StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
);
break;
case "state_detail.group":
@@ -131,8 +138,14 @@ export const BoardHeader: React.FC<Props> = ({
: null);
break;
case "labels":
const labelColor = issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = <span className="h-3.5 w-3.5 flex-shrink-0 rounded-full" style={{ backgroundColor: labelColor }} />;
const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = (
<span
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
style={{ backgroundColor: labelColor }}
/>
);
break;
case "assignees":
case "created_by":
@@ -183,7 +196,10 @@ export const BoardHeader: React.FC<Props> = ({
}}
>
{isCollapsed ? (
<Icon iconName="close_fullscreen" className="text-base font-medium text-custom-text-900" />
<Icon
iconName="close_fullscreen"
className="text-base font-medium text-custom-text-900"
/>
) : (
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
)}
@@ -5,17 +5,27 @@ import { useRouter } from "next/router";
import { mutate } from "swr";
// react-beautiful-dnd
import { DraggableProvided, DraggableStateSnapshot, DraggingStyle, NotDraggingStyle } from "react-beautiful-dnd";
import {
DraggableProvided,
DraggableStateSnapshot,
DraggingStyle,
NotDraggingStyle,
} from "react-beautiful-dnd";
// services
import issuesService from "services/issue.service";
import trackEventServices from "services/track_event.service";
import issuesService from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
import { MembersSelect, LabelSelect, PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
import {
ViewAssigneeSelect,
ViewDueDateSelect,
ViewEstimateSelect,
ViewIssueLabel,
ViewPrioritySelect,
ViewStartDateSelect,
ViewStateSelect,
} from "components/issues";
// ui
import { ContextMenu, CustomMenu, Tooltip } from "components/ui";
// icons
@@ -31,18 +41,10 @@ import {
} from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons";
// helpers
import { handleIssuesMutation } from "helpers/issue.helper";
import { handleIssuesMutation } from "constants/issue";
import { copyTextToClipboard } from "helpers/string.helper";
// types
import {
ICurrentUserResponse,
IIssue,
IIssueViewProps,
IState,
ISubIssueResponse,
TIssuePriorities,
UserAuth,
} from "types";
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
// fetch-keys
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
@@ -145,17 +147,22 @@ export const SingleBoardIssue: React.FC<Props> = ({
);
}
issuesService.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user).then(() => {
mutateIssues();
issuesService
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
.then(() => {
mutateIssues();
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
});
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
});
},
[displayFilters, workspaceSlug, cycleId, moduleId, groupTitle, index, mutateIssues, user]
);
const getStyle = (style: DraggingStyle | NotDraggingStyle | undefined, snapshot: DraggableStateSnapshot) => {
const getStyle = (
style: DraggingStyle | NotDraggingStyle | undefined,
snapshot: DraggableStateSnapshot
) => {
if (displayFilters?.order_by === "sort_order") return style;
if (!snapshot.isDragging) return {};
if (!snapshot.isDropAnimating) return style;
@@ -167,8 +174,11 @@ export const SingleBoardIssue: React.FC<Props> = ({
};
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
@@ -178,86 +188,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
});
};
const handleStateChange = (data: string, states: IState[] | undefined) => {
const oldState = states?.find((s) => s.id === issue.state);
const newState = states?.find((s) => s.id === data);
partialUpdateIssue(
{
state: data,
state_detail: newState,
},
issue
);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_STATE",
user
);
if (oldState?.group !== "completed" && newState?.group !== "completed") {
trackEventServices.trackIssueMarkedAsDoneEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
user
);
}
};
const handleAssigneeChange = (data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
user
);
};
const handleLabelChange = (data: any) => {
partialUpdateIssue({ labels_list: data }, issue);
};
const handlePriorityChange = (data: TIssuePriorities) => {
partialUpdateIssue({ priority: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY",
user
);
};
useEffect(() => {
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
}, [snapshot, handleTrashBox]);
@@ -323,7 +253,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
target="_blank"
rel="noreferrer noopener"
>
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>Open issue in new tab</ContextMenu.Item>
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab
</ContextMenu.Item>
</a>
)}
</ContextMenu>
@@ -345,7 +277,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
{!isNotAllowed && (
<div
ref={actionSectionRef}
className={`z-1 absolute top-1.5 right-1.5 hidden group-hover/card:!flex ${isMenuActive ? "!flex" : ""}`}
className={`z-1 absolute top-1.5 right-1.5 hidden group-hover/card:!flex ${
isMenuActive ? "!flex" : ""
}`}
>
{type && !isNotAllowed && (
<CustomMenu
@@ -409,30 +343,37 @@ export const SingleBoardIssue: React.FC<Props> = ({
)}
<button
type="button"
className="text-sm text-left break-words line-clamp-2"
onClick={() => {
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
else openPeekOverview();
}}
>
<span className="text-sm text-left break-words line-clamp-2">{issue.name}</span>
{issue.name}
</button>
</div>
<div className={`flex items-center gap-2 text-xs ${isDropdownActive ? "" : "overflow-x-scroll"}`}>
<div
className={`flex items-center gap-2 text-xs ${
isDropdownActive ? "" : "overflow-x-scroll"
}`}
>
{properties.priority && (
<PrioritySelect
value={issue.priority}
onChange={handlePriorityChange}
hideDropdownArrow
disabled={isNotAllowed}
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
user={user}
selfPositioned
/>
)}
{properties.state && (
<StateSelect
value={issue.state_detail}
onChange={handleStateChange}
hideDropdownArrow
disabled={isNotAllowed}
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
user={user}
selfPositioned
/>
)}
{properties.start_date && issue.start_date && (
@@ -456,22 +397,16 @@ export const SingleBoardIssue: React.FC<Props> = ({
/>
)}
{properties.labels && issue.labels.length > 0 && (
<LabelSelect
value={issue.labels}
onChange={handleLabelChange}
labelsDetails={issue.label_details}
hideDropdownArrow
user={user}
disabled={isNotAllowed}
/>
<ViewIssueLabel labelDetails={issue.label_details} maxRender={2} />
)}
{properties.assignee && (
<MembersSelect
value={issue.assignees}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
hideDropdownArrow
disabled={isNotAllowed}
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
customButton
user={user}
selfPositioned
/>
)}
{properties.estimate && issue.estimate_point !== null && (
@@ -7,7 +7,9 @@ import { mutate } from "swr";
// react-beautiful-dnd
import { DragDropContext, DropResult } from "react-beautiful-dnd";
// services
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
// hooks
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
// components
import { SingleCalendarDate, CalendarHeader } from "components/core";
import { IssuePeekOverview } from "components/issues";
@@ -15,7 +17,13 @@ import { IssuePeekOverview } from "components/issues";
import { Spinner } from "components/ui";
// helpers
import { renderDateFormat } from "helpers/date-time.helper";
import { startOfWeek, lastDayOfWeek, eachDayOfInterval, weekDayInterval, formatDate } from "helpers/calendar.helper";
import {
startOfWeek,
lastDayOfWeek,
eachDayOfInterval,
weekDayInterval,
formatDate,
} from "helpers/calendar.helper";
// types
import { ICalendarRange, ICurrentUserResponse, IIssue, UserAuth } from "types";
// fetch-keys
@@ -53,7 +61,8 @@ export const CalendarView: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } = useCalendarIssuesView();
const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } =
useCalendarIssuesView();
const totalDate = eachDayOfInterval({
start: calendarDates.startDate,
@@ -71,7 +80,8 @@ export const CalendarView: React.FC<Props> = ({
const filterIssue =
calendarIssues.length > 0
? calendarIssues.filter(
(issue) => issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
(issue) =>
issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
)
: [];
return {
@@ -145,16 +155,18 @@ export const CalendarView: React.FC<Props> = ({
});
setDisplayFilters({
calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat(endDate)};before`,
calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat(
endDate
)};before`,
});
};
useEffect(() => {
if (!displayFilters || displayFilters.calendar_date_range === "")
setDisplayFilters({
calendar_date_range: `${renderDateFormat(startOfWeek(currentDate))};after,${renderDateFormat(
lastDayOfWeek(currentDate)
)};before`,
calendar_date_range: `${renderDateFormat(
startOfWeek(currentDate)
)};after,${renderDateFormat(lastDayOfWeek(currentDate))};before`,
});
}, [currentDate, displayFilters, setDisplayFilters]);
@@ -202,7 +214,11 @@ export const CalendarView: React.FC<Props> = ({
: ""
}`}
>
<span>{isMonthlyView ? formatDate(date, "eee").substring(0, 3) : formatDate(date, "eee")}</span>
<span>
{isMonthlyView
? formatDate(date, "eee").substring(0, 3)
: formatDate(date, "eee")}
</span>
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
</div>
))}
@@ -7,23 +7,29 @@ import { mutate } from "swr";
// react-beautiful-dnd
import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd";
// services
import issuesService from "services/issue.service";
import trackEventServices from "services/track_event.service";
import issuesService from "services/issues.service";
// hooks
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useIssuesProperties from "hooks/use-issue-properties";
import useToast from "hooks/use-toast";
// components
import { CustomMenu, Tooltip } from "components/ui";
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
import {
ViewAssigneeSelect,
ViewDueDateSelect,
ViewEstimateSelect,
ViewLabelSelect,
ViewPrioritySelect,
ViewStartDateSelect,
ViewStateSelect,
} from "components/issues";
// icons
import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons";
// helper
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// type
import { ICurrentUserResponse, IIssue, IState, ISubIssueResponse, TIssuePriorities } from "types";
import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types";
// fetch-keys
import {
CYCLE_ISSUES_WITH_PARAMS,
@@ -59,7 +65,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
const { setToastAlert } = useToast();
const params = {};
const { params } = useCalendarIssuesView();
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
@@ -116,7 +122,13 @@ export const SingleCalendarIssue: React.FC<Props> = ({
}
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id as string, formData, user)
.patchIssue(
workspaceSlug as string,
projectId as string,
issue.id as string,
formData,
user
)
.then(() => {
mutate(fetchKey);
})
@@ -128,8 +140,11 @@ export const SingleCalendarIssue: React.FC<Props> = ({
);
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
@@ -138,87 +153,9 @@ export const SingleCalendarIssue: React.FC<Props> = ({
});
};
const handleStateChange = (data: string, states: IState[] | undefined) => {
const oldState = states?.find((s) => s.id === issue.state);
const newState = states?.find((s) => s.id === data);
partialUpdateIssue(
{
state: data,
state_detail: newState,
},
issue
);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_STATE",
user
);
if (oldState?.group !== "completed" && newState?.group !== "completed") {
trackEventServices.trackIssueMarkedAsDoneEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
user
);
}
};
const handleAssigneeChange = (data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
user
);
};
const handleLabelChange = (data: any) => {
partialUpdateIssue({ labels_list: data }, issue);
};
const handlePriorityChange = (data: TIssuePriorities) => {
partialUpdateIssue({ priority: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY",
user
);
};
const displayProperties = properties ? Object.values(properties).some((value) => value === true) : false;
const displayProperties = properties
? Object.values(properties).some((value) => value === true)
: false;
const openPeekOverview = () => {
const { query } = router;
@@ -288,19 +225,22 @@ export const SingleCalendarIssue: React.FC<Props> = ({
{displayProperties && (
<div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
{properties.priority && (
<PrioritySelect
value={issue.priority}
onChange={handlePriorityChange}
hideDropdownArrow
disabled={isNotAllowed}
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.state && (
<StateSelect
value={issue.state_detail}
onChange={handleStateChange}
hideDropdownArrow
disabled={isNotAllowed}
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
className="max-w-full"
isNotAllowed={isNotAllowed}
user={user}
/>
)}
{properties.start_date && issue.start_date && (
@@ -320,23 +260,21 @@ export const SingleCalendarIssue: React.FC<Props> = ({
/>
)}
{properties.labels && issue.labels.length > 0 && (
<LabelSelect
value={issue.labels}
onChange={handleLabelChange}
labelsDetails={issue.label_details}
hideDropdownArrow
maxRender={1}
<ViewLabelSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
user={user}
disabled={isNotAllowed}
isNotAllowed={isNotAllowed}
/>
)}
{properties.assignee && (
<MembersSelect
value={issue.assignees}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
hideDropdownArrow
disabled={isNotAllowed}
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.estimate && issue.estimate_point !== null && (
+65 -35
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
@@ -7,10 +7,10 @@ import useSWR, { mutate } from "swr";
// react-beautiful-dnd
import { DropResult } from "react-beautiful-dnd";
// services
import issuesService from "services/issue.service";
import stateService from "services/project_state.service";
import issuesService from "services/issues.service";
import stateService from "services/state.service";
import modulesService from "services/modules.service";
import trackEventServices from "services/track_event.service";
import trackEventServices from "services/track-event.service";
// hooks
import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view";
@@ -51,7 +51,10 @@ type Props = {
disableUserActions?: boolean;
};
export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserActions = false }) => {
export const IssuesView: React.FC<Props> = ({
openIssuesListModal,
disableUserActions = false,
}) => {
// create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false);
const [createViewModal, setCreateViewModal] = useState<any>(null);
@@ -61,7 +64,9 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
// update issue modal
const [editIssueModal, setEditIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<(IIssue & { actionType: "edit" | "delete" }) | undefined>(undefined);
const [issueToEdit, setIssueToEdit] = useState<
(IIssue & { actionType: "edit" | "delete" }) | undefined
>(undefined);
// delete issue modal
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
@@ -82,13 +87,15 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
const { setToastAlert } = useToast();
const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params, setDisplayFilters } =
const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params } =
useIssuesView();
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null
workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const states = getStatesList(stateGroups);
@@ -101,17 +108,6 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
useEffect(() => {
if (!isDraftIssues) return;
if (
displayFilters.layout === "calendar" ||
displayFilters.layout === "gantt_chart" ||
displayFilters.layout === "spreadsheet"
)
setDisplayFilters({ layout: "list" });
}, [isDraftIssues, displayFilters, setDisplayFilters]);
const handleDeleteIssue = useCallback(
(issue: IIssue) => {
setDeleteIssueModal(true);
@@ -145,10 +141,12 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
// check if dropping in the same group
if (source.droppableId === destination.droppableId) {
// check if dropping at beginning
if (destination.index === 0) newSortOrder = destinationGroupArray[0].sort_order - 10000;
if (destination.index === 0)
newSortOrder = destinationGroupArray[0].sort_order - 10000;
// check if dropping at last
else if (destination.index === destinationGroupArray.length - 1)
newSortOrder = destinationGroupArray[destinationGroupArray.length - 1].sort_order + 10000;
newSortOrder =
destinationGroupArray[destinationGroupArray.length - 1].sort_order + 10000;
else {
if (destination.index > source.index)
newSortOrder =
@@ -163,10 +161,12 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
}
} else {
// check if dropping at beginning
if (destination.index === 0) newSortOrder = destinationGroupArray[0].sort_order - 10000;
if (destination.index === 0)
newSortOrder = destinationGroupArray[0].sort_order - 10000;
// check if dropping at last
else if (destination.index === destinationGroupArray.length)
newSortOrder = destinationGroupArray[destinationGroupArray.length - 1].sort_order + 10000;
newSortOrder =
destinationGroupArray[destinationGroupArray.length - 1].sort_order + 10000;
else
newSortOrder =
(destinationGroupArray[destination.index - 1].sort_order +
@@ -180,14 +180,18 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
const destinationGroup = destination.droppableId; // destination group id
if (displayFilters.order_by === "sort_order" || source.droppableId !== destination.droppableId) {
if (
displayFilters.order_by === "sort_order" ||
source.droppableId !== destination.droppableId
) {
// different group/column;
// source.droppableId !== destination.droppableId -> even if order by is not sort_order,
// if the issue is moved to a different group, then we will change the group of the
// dragged item(or issue)
if (displayFilters.group_by === "priority") draggedItem.priority = destinationGroup as TIssuePriorities;
if (displayFilters.group_by === "priority")
draggedItem.priority = destinationGroup as TIssuePriorities;
else if (displayFilters.group_by === "state") {
draggedItem.state = destinationGroup;
draggedItem.state_detail = states?.find((s) => s.id === destinationGroup) as IState;
@@ -215,8 +219,14 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
return {
...prevData,
[sourceGroup]: orderArrayBy(sourceGroupArray, displayFilters.order_by ?? "-created_at"),
[destinationGroup]: orderArrayBy(destinationGroupArray, displayFilters.order_by ?? "-created_at"),
[sourceGroup]: orderArrayBy(
sourceGroupArray,
displayFilters.order_by ?? "-created_at"
),
[destinationGroup]: orderArrayBy(
destinationGroupArray,
displayFilters.order_by ?? "-created_at"
),
};
},
false
@@ -236,9 +246,14 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
user
)
.then((response) => {
const sourceStateBeforeDrag = states?.find((state) => state.name === source.droppableId);
const sourceStateBeforeDrag = states?.find(
(state) => state.name === source.droppableId
);
if (sourceStateBeforeDrag?.group !== "completed" && response?.state_detail?.group === "completed")
if (
sourceStateBeforeDrag?.group !== "completed" &&
response?.state_detail?.group === "completed"
)
trackEventServices.trackIssueMarkedAsDoneEvent(
{
workspaceSlug,
@@ -372,7 +387,12 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
);
issuesService
.removeIssueFromCycle(workspaceSlug as string, projectId as string, cycleId as string, bridgeId)
.removeIssueFromCycle(
workspaceSlug as string,
projectId as string,
cycleId as string,
bridgeId
)
.then(() => {
setToastAlert({
title: "Success",
@@ -410,7 +430,12 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
);
modulesService
.removeIssueFromModule(workspaceSlug as string, projectId as string, moduleId as string, bridgeId)
.removeIssueFromModule(
workspaceSlug as string,
projectId as string,
moduleId as string,
bridgeId
)
.then(() => {
setToastAlert({
title: "Success",
@@ -425,9 +450,12 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
[displayFilters.group_by, workspaceSlug, projectId, moduleId, params, setToastAlert]
);
const nullFilters = Object.keys(filters).filter((key) => filters[key as keyof IIssueFilterOptions] === null);
const nullFilters = Object.keys(filters).filter(
(key) => filters[key as keyof IIssueFilterOptions] === null
);
const areFiltersApplied = Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length;
const areFiltersApplied =
Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length;
return (
<>
@@ -490,7 +518,6 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
labels: null,
priority: null,
state: null,
state_group: null,
start_date: null,
target_date: null,
})
@@ -554,7 +581,10 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
: undefined,
secondaryButton:
cycleId || moduleId ? (
<SecondaryButton className="flex items-center gap-1.5" onClick={openIssuesListModal ?? (() => {})}>
<SecondaryButton
className="flex items-center gap-1.5"
onClick={openIssuesListModal ?? (() => {})}
>
<PlusIcon className="h-4 w-4" />
Add an existing issue
</SecondaryButton>
@@ -5,14 +5,20 @@ import { useRouter } from "next/router";
import { mutate } from "swr";
// services
import issuesService from "services/issue.service";
import trackEventServices from "services/track_event.service";
import issuesService from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
// components
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
import {
ViewAssigneeSelect,
ViewDueDateSelect,
ViewEstimateSelect,
ViewIssueLabel,
ViewPrioritySelect,
ViewStartDateSelect,
ViewStateSelect,
CreateUpdateDraftIssueModal,
} from "components/issues";
// ui
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
// icons
@@ -28,20 +34,23 @@ import {
import { LayerDiagonalIcon } from "components/icons";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
import { handleIssuesMutation } from "helpers/issue.helper";
import { handleIssuesMutation } from "constants/issue";
// types
import {
ICurrentUserResponse,
IIssue,
IIssueViewProps,
IState,
ISubIssueResponse,
IUserProfileProjectSegregation,
TIssuePriorities,
UserAuth,
} from "types";
// fetch-keys
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES, USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
import {
CYCLE_DETAILS,
MODULE_DETAILS,
SUB_ISSUES,
USER_PROFILE_PROJECT_SEGREGATION,
} from "constants/fetch-keys";
type Props = {
type?: string;
@@ -131,24 +140,39 @@ export const SingleListIssue: React.FC<Props> = ({
);
}
issuesService.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user).then(() => {
mutateIssues();
issuesService
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
.then(() => {
mutateIssues();
if (userId)
mutate<IUserProfileProjectSegregation>(
USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
);
if (userId)
mutate<IUserProfileProjectSegregation>(
USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
);
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
});
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
});
},
[displayFilters, workspaceSlug, cycleId, moduleId, userId, groupTitle, index, mutateIssues, user]
[
displayFilters,
workspaceSlug,
cycleId,
moduleId,
userId,
groupTitle,
index,
mutateIssues,
user,
]
);
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
@@ -157,86 +181,6 @@ export const SingleListIssue: React.FC<Props> = ({
});
};
const handleStateChange = (data: string, states: IState[] | undefined) => {
const oldState = states?.find((s) => s.id === issue.state);
const newState = states?.find((s) => s.id === data);
partialUpdateIssue(
{
state: data,
state_detail: newState,
},
issue
);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_STATE",
user
);
if (oldState?.group !== "completed" && newState?.group !== "completed") {
trackEventServices.trackIssueMarkedAsDoneEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
user
);
}
};
const handleAssigneeChange = (data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
user
);
};
const handleLabelChange = (data: any) => {
partialUpdateIssue({ labels_list: data }, issue);
};
const handlePriorityChange = (data: TIssuePriorities) => {
partialUpdateIssue({ priority: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY",
user
);
};
const issuePath = isArchivedIssues
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
: isDraftIssues
@@ -253,7 +197,8 @@ export const SingleListIssue: React.FC<Props> = ({
});
};
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
const isNotAllowed =
userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
return (
<>
@@ -296,7 +241,9 @@ export const SingleListIssue: React.FC<Props> = ({
Copy issue link
</ContextMenu.Item>
<a href={issuePath} target="_blank" rel="noreferrer noopener">
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>Open issue in new tab</ContextMenu.Item>
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab
</ContextMenu.Item>
</a>
</>
)}
@@ -337,21 +284,27 @@ export const SingleListIssue: React.FC<Props> = ({
</div>
</div>
<div className={`flex flex-shrink-0 items-center gap-2 text-xs ${isArchivedIssues ? "opacity-60" : ""}`}>
<div
className={`flex flex-shrink-0 items-center gap-2 text-xs ${
isArchivedIssues ? "opacity-60" : ""
}`}
>
{properties.priority && (
<PrioritySelect
value={issue.priority}
onChange={handlePriorityChange}
hideDropdownArrow
disabled={isNotAllowed}
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.state && (
<StateSelect
value={issue.state_detail}
onChange={handleStateChange}
hideDropdownArrow
disabled={isNotAllowed}
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.start_date && issue.start_date && (
@@ -370,24 +323,14 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed}
/>
)}
{properties.labels && (
<LabelSelect
value={issue.labels}
onChange={handleLabelChange}
labelsDetails={issue.label_details}
hideDropdownArrow
maxRender={3}
user={user}
disabled={isNotAllowed}
/>
)}
{properties.labels && <ViewIssueLabel labelDetails={issue.label_details} maxRender={3} />}
{properties.assignee && (
<MembersSelect
value={issue.assignees}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
hideDropdownArrow
disabled={isNotAllowed}
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.estimate && issue.estimate_point !== null && (
@@ -5,7 +5,7 @@ import useSWR from "swr";
// headless ui
import { Disclosure, Transition } from "@headlessui/react";
// services
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
import projectService from "services/project.service";
// hooks
import useProjects from "hooks/use-projects";
@@ -77,7 +77,9 @@ export const SingleList: React.FC<Props> = ({
const { data: issueLabels } = useSWR<IIssueLabels[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) : null
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const { data: members } = useSWR(
@@ -118,7 +120,12 @@ export const SingleList: React.FC<Props> = ({
switch (displayFilters?.group_by) {
case "state":
icon = currentState && (
<StateGroupIcon stateGroup={currentState.group} color={currentState.color} height="16px" width="16px" />
<StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
);
break;
case "state_detail.group":
@@ -145,8 +152,14 @@ export const SingleList: React.FC<Props> = ({
: null);
break;
case "labels":
const labelColor = issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = <span className="h-3 w-3 flex-shrink-0 rounded-full" style={{ backgroundColor: labelColor }} />;
const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = (
<span
className="h-3 w-3 flex-shrink-0 rounded-full"
style={{ backgroundColor: labelColor }}
/>
);
break;
case "assignees":
case "created_by":
@@ -168,7 +181,9 @@ export const SingleList: React.FC<Props> = ({
<div className="flex items-center justify-between px-4 py-2.5 bg-custom-background-90">
<Disclosure.Button>
<div className="flex items-center gap-x-3">
{displayFilters?.group_by !== null && <div className="flex items-center">{getGroupIcon()}</div>}
{displayFilters?.group_by !== null && (
<div className="flex items-center">{getGroupIcon()}</div>
)}
{displayFilters?.group_by !== null ? (
<h2
className={`text-sm font-semibold leading-6 text-custom-text-100 ${
@@ -211,7 +226,9 @@ export const SingleList: React.FC<Props> = ({
>
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>Add an existing issue</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
@@ -239,14 +256,19 @@ export const SingleList: React.FC<Props> = ({
makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
handleDraftIssueSelect={
handleDraftIssueAction ? () => handleDraftIssueAction(issue, "edit") : undefined
handleDraftIssueAction
? () => handleDraftIssueAction(issue, "edit")
: undefined
}
handleDraftIssueDelete={
handleDraftIssueAction ? () => handleDraftIssueAction(issue, "delete") : undefined
handleDraftIssueAction
? () => handleDraftIssueAction(issue, "delete")
: undefined
}
handleMyIssueOpen={handleMyIssueOpen}
removeIssue={() => {
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id, issue.id);
if (removeIssue !== null && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
}}
disableUserActions={disableUserActions}
user={user}
@@ -255,7 +277,9 @@ export const SingleList: React.FC<Props> = ({
/>
))
) : (
<p className="bg-custom-background-100 px-4 py-2.5 text-sm text-custom-text-200">No issues.</p>
<p className="bg-custom-background-100 px-4 py-2.5 text-sm text-custom-text-200">
No issues.
</p>
)
) : (
<div className="flex h-full w-full items-center justify-center">Loading...</div>
@@ -1,27 +1,48 @@
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import { Popover2 } from "@blueprintjs/popover2";
import { useRouter } from "next/router";
import { mutate } from "swr";
// components
import {
ViewAssigneeSelect,
ViewDueDateSelect,
ViewEstimateSelect,
ViewIssueLabel,
ViewPrioritySelect,
ViewStartDateSelect,
ViewStateSelect,
} from "components/issues";
import { Popover2 } from "@blueprintjs/popover2";
// icons
import { Icon } from "components/ui";
import { EllipsisHorizontalIcon, LinkIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
// services
import issuesService from "services/issue.service";
import trackEventServices from "services/track_event.service";
import {
EllipsisHorizontalIcon,
LinkIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
// hooks
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useToast from "hooks/use-toast";
// components
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
// helpers
// services
import issuesService from "services/issues.service";
// constant
import {
CYCLE_DETAILS,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES,
VIEW_ISSUES,
} from "constants/fetch-keys";
// types
import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types";
// helper
import { copyTextToClipboard } from "helpers/string.helper";
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
// types
import { ICurrentUserResponse, IIssue, IState, Properties, TIssuePriorities, UserAuth } from "types";
// constant
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
type Props = {
issue: IIssue;
@@ -56,7 +77,9 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { params } = useSpreadsheetIssuesView();
const { setToastAlert } = useToast();
@@ -64,12 +87,65 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
(formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !projectId) return;
const fetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
: viewId
? VIEW_ISSUES(viewId.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
if (issue.parent)
mutate<ISubIssueResponse>(
SUB_ISSUES(issue.parent.toString()),
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
sub_issues: (prevData.sub_issues ?? []).map((i) => {
if (i.id === issue.id) {
return {
...i,
...formData,
};
}
return i;
}),
};
},
false
);
else
mutate<IIssue[]>(
fetchKey,
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === issue.id) {
return {
...p,
...formData,
};
}
return p;
}),
false
);
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id as string, formData, user)
.patchIssue(
workspaceSlug as string,
projectId as string,
issue.id as string,
formData,
user
)
.then(() => {
if (issue.parent) {
mutate(SUB_ISSUES(issue.parent as string));
} else {
mutate(fetchKey);
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
}
@@ -78,7 +154,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
console.log(error);
});
},
[workspaceSlug, projectId, cycleId, moduleId, user]
[workspaceSlug, projectId, cycleId, moduleId, viewId, params, user]
);
const openPeekOverview = () => {
@@ -91,8 +167,11 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
};
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
@@ -101,86 +180,6 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
});
};
const handleStateChange = (data: string, states: IState[] | undefined) => {
const oldState = states?.find((s) => s.id === issue.state);
const newState = states?.find((s) => s.id === data);
partialUpdateIssue(
{
state: data,
state_detail: newState,
},
issue
);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_STATE",
user
);
if (oldState?.group !== "completed" && newState?.group !== "completed") {
trackEventServices.trackIssueMarkedAsDoneEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
user
);
}
};
const handlePriorityChange = (data: TIssuePriorities) => {
partialUpdateIssue({ priority: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY",
user
);
};
const handleAssigneeChange = (data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
user
);
};
const handleLabelChange = (data: any) => {
partialUpdateIssue({ labels_list: data }, issue);
};
const paddingLeft = `${nestingLevel * 68}px`;
const tooltipPosition = index === 0 ? "bottom" : "top";
@@ -284,49 +283,47 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
</div>
{properties.state && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<StateSelect
value={issue.state_detail}
onChange={handleStateChange}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
hideDropdownArrow
disabled={isNotAllowed}
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
className="max-w-full"
tooltipPosition={tooltipPosition}
customButton
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.priority && (
<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">
<PrioritySelect
value={issue.priority}
onChange={handlePriorityChange}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
hideDropdownArrow
disabled={isNotAllowed}
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
tooltipPosition={tooltipPosition}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.assignee && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<MembersSelect
value={issue.assignees}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
hideDropdownArrow
disabled={isNotAllowed}
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
tooltipPosition={tooltipPosition}
customButton
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.labels && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<LabelSelect
value={issue.labels}
onChange={handleLabelChange}
labelsDetails={issue.label_details}
hideDropdownArrow
maxRender={1}
user={user}
disabled={isNotAllowed}
/>
<ViewIssueLabel labelDetails={issue.label_details} maxRender={1} />
</div>
)}
@@ -1,46 +1,48 @@
import React from "react";
// hooks
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useLocalStorage from "hooks/use-local-storage";
// component
import { CustomMenu, Icon } from "components/ui";
// icon
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
// types
import { IIssueDisplayFilterOptions, TIssueOrderByOptions } from "types";
import { TIssueOrderByOptions } from "types";
type Props = {
columnData: any;
displayFilters: IIssueDisplayFilterOptions;
gridTemplateColumns: string;
handleDisplayFiltersUpdate: (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => void;
};
export const SpreadsheetColumns: React.FC<Props> = (props) => {
const { columnData, displayFilters, gridTemplateColumns, handleDisplayFiltersUpdate } = props;
export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateColumns }) => {
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
"spreadsheetViewSorting",
""
);
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = useLocalStorage(
"spreadsheetViewActiveSortingProperty",
""
);
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
const { displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
handleDisplayFiltersUpdate({ order_by: order });
setDisplayFilters({ order_by: order });
setSelectedMenuItem(`${order}_${itemKey}`);
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
};
return (
<div className={`grid auto-rows-[minmax(36px,1fr)] w-full min-w-max`} style={{ gridTemplateColumns }}>
<div
className={`grid auto-rows-[minmax(36px,1fr)] w-full min-w-max`}
style={{ gridTemplateColumns }}
>
{columnData.map((col: any) => {
if (col.isActive) {
return (
<div
className={`bg-custom-background-90 w-full ${
col.propertyName === "title" ? "sticky left-0 z-20 bg-custom-background-90 pl-24" : ""
col.propertyName === "title"
? "sticky left-0 z-20 bg-custom-background-90 pl-24"
: ""
}`}
>
{col.propertyName === "title" ? (
@@ -106,7 +108,10 @@ export const SpreadsheetColumns: React.FC<Props> = (props) => {
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon iconName="east" className="absolute left-0 rotate-90 text-xs leading-3" />
<Icon
iconName="east"
className="absolute left-0 rotate-90 text-xs leading-3"
/>
<Icon iconName="sort" className="absolute right-0 text-sm" />
</span>
<span>A</span>
@@ -118,7 +123,10 @@ export const SpreadsheetColumns: React.FC<Props> = (props) => {
col.propertyName === "updated_on" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon iconName="east" className="absolute left-0 rotate-90 text-xs leading-3" />
<Icon
iconName="east"
className="absolute left-0 rotate-90 text-xs leading-3"
/>
<Icon iconName="sort" className="absolute right-0 text-sm" />
</span>
<span>New</span>
@@ -128,7 +136,10 @@ export const SpreadsheetColumns: React.FC<Props> = (props) => {
) : (
<>
<span className="relative flex items-center h-6 w-6">
<Icon iconName="east" className="absolute left-0 rotate-90 text-xs leading-3" />
<Icon
iconName="east"
className="absolute left-0 rotate-90 text-xs leading-3"
/>
<Icon iconName="sort" className="absolute right-0 text-sm" />
</span>
<span>First</span>
@@ -140,14 +151,18 @@ export const SpreadsheetColumns: React.FC<Props> = (props) => {
<CheckIcon
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}` ? "opacity-100" : ""
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}`
? "opacity-100"
: ""
}`}
/>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
className={`mt-0.5 ${
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}` ? "bg-custom-background-80" : ""
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
? "bg-custom-background-80"
: ""
}`}
key={col.property}
onClick={() => {
@@ -165,7 +180,10 @@ export const SpreadsheetColumns: React.FC<Props> = (props) => {
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon iconName="east" className="absolute left-0 -rotate-90 text-xs leading-3" />
<Icon
iconName="east"
className="absolute left-0 -rotate-90 text-xs leading-3"
/>
<Icon
iconName="sort"
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
@@ -178,7 +196,10 @@ export const SpreadsheetColumns: React.FC<Props> = (props) => {
) : col.propertyName === "due_date" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon iconName="east" className="absolute left-0 -rotate-90 text-xs leading-3" />
<Icon
iconName="east"
className="absolute left-0 -rotate-90 text-xs leading-3"
/>
<Icon
iconName="sort"
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
@@ -191,7 +212,10 @@ export const SpreadsheetColumns: React.FC<Props> = (props) => {
) : (
<>
<span className="relative flex items-center h-6 w-6">
<Icon iconName="east" className="absolute left-0 -rotate-90 text-xs leading-3" />
<Icon
iconName="east"
className="absolute left-0 -rotate-90 text-xs leading-3"
/>
<Icon
iconName="sort"
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
@@ -206,7 +230,9 @@ export const SpreadsheetColumns: React.FC<Props> = (props) => {
<CheckIcon
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}` ? "opacity-100" : ""
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
? "opacity-100"
: ""
}`}
/>
</div>
@@ -217,7 +243,9 @@ export const SpreadsheetColumns: React.FC<Props> = (props) => {
selectedMenuItem.includes(col.propertyName) && (
<CustomMenu.MenuItem
className={`mt-0.5${
selectedMenuItem === `-created_at_${col.propertyName}` ? "bg-custom-background-80" : ""
selectedMenuItem === `-created_at_${col.propertyName}`
? "bg-custom-background-80"
: ""
}`}
key={col.property}
onClick={() => {
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
// components
import { SingleSpreadsheetIssue } from "components/core";
@@ -9,6 +9,7 @@ import { CustomMenu, Spinner } from "components/ui";
import { IssuePeekOverview } from "components/issues";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
// types
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
// constants
@@ -38,6 +39,8 @@ export const SpreadsheetView: React.FC<Props> = ({
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const columnData = SPREADSHEET_COLUMN.map((column) => ({
@@ -56,89 +59,89 @@ export const SpreadsheetView: React.FC<Props> = ({
.map((column) => column.colSize)
.join(" ");
return null;
// return (
// <>
// <IssuePeekOverview
// handleMutation={() => mutateIssues()}
// projectId={projectId?.toString() ?? ""}
// workspaceSlug={workspaceSlug?.toString() ?? ""}
// readOnly={disableUserActions}
// />
// <div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
// <div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
// <SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
// </div>
// {spreadsheetIssues ? (
// <div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
// {spreadsheetIssues.map((issue: IIssue, index) => (
// <SpreadsheetIssues
// key={`${issue.id}_${index}`}
// index={index}
// issue={issue}
// expandedIssues={expandedIssues}
// setExpandedIssues={setExpandedIssues}
// gridTemplateColumns={gridTemplateColumns}
// properties={properties}
// handleIssueAction={handleIssueAction}
// disableUserActions={disableUserActions}
// user={user}
// userAuth={userAuth}
// />
// ))}
// <div
// className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
// style={{ gridTemplateColumns }}
// >
// {type === "issue" ? (
// <button
// className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
// onClick={() => {
// const e = new KeyboardEvent("keydown", { key: "c" });
// document.dispatchEvent(e);
// }}
// >
// <PlusIcon className="h-4 w-4" />
// Add Issue
// </button>
// ) : (
// !disableUserActions && (
// <CustomMenu
// className="sticky left-0 z-[1]"
// customButton={
// <button
// className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
// type="button"
// >
// <PlusIcon className="h-4 w-4" />
// Add Issue
// </button>
// }
// position="left"
// optionsClassName="left-5 !w-36"
// noBorder
// >
// <CustomMenu.MenuItem
// onClick={() => {
// const e = new KeyboardEvent("keydown", { key: "c" });
// document.dispatchEvent(e);
// }}
// >
// Create new
// </CustomMenu.MenuItem>
// {openIssuesListModal && (
// <CustomMenu.MenuItem onClick={openIssuesListModal}>Add an existing issue</CustomMenu.MenuItem>
// )}
// </CustomMenu>
// )
// )}
// </div>
// </div>
// ) : (
// <Spinner />
// )}
// </div>
// </>
// );
return (
<>
<IssuePeekOverview
handleMutation={() => mutateIssues()}
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
</div>
{spreadsheetIssues ? (
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
{spreadsheetIssues.map((issue: IIssue, index) => (
<SpreadsheetIssues
key={`${issue.id}_${index}`}
index={index}
issue={issue}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns}
properties={properties}
handleIssueAction={handleIssueAction}
disableUserActions={disableUserActions}
user={user}
userAuth={userAuth}
/>
))}
<div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }}
>
{type === "issue" ? (
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
) : (
!disableUserActions && (
<CustomMenu
className="sticky left-0 z-[1]"
customButton={
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)
)}
</div>
</div>
) : (
<Spinner />
)}
</div>
</>
);
};
@@ -127,7 +127,7 @@ export const ActiveCycleDetails: React.FC = () => {
cy="34.375"
r="22"
stroke="rgb(var(--color-text-400))"
strokeLinecap="round"
stroke-linecap="round"
/>
<path
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
-366
View File
@@ -1,366 +0,0 @@
import React, { FC } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { Disclosure, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
// components
import { SingleProgressStats } from "components/core";
// ui
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
import { AssigneesList } from "components/ui/avatar";
import { RadialProgressBar } from "@plane/ui";
// icons
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import {
TargetIcon,
ContrastIcon,
PersonRunningIcon,
ArrowRightIcon,
TriangleExclamationIcon,
AlarmClockIcon,
} from "components/icons";
import { ChevronDownIcon, LinkIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
// helpers
import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// types
import { ICycle } from "types";
const stateGroups = [
{
key: "backlog_issues",
title: "Backlog",
color: "#dee2e6",
},
{
key: "unstarted_issues",
title: "Unstarted",
color: "#26b5ce",
},
{
key: "started_issues",
title: "Started",
color: "#f7ae59",
},
{
key: "cancelled_issues",
title: "Cancelled",
color: "#d687ff",
},
{
key: "completed_issues",
title: "Completed",
color: "#09a953",
},
];
export interface ICyclesBoardCard {
cycle: ICycle;
filter: string;
}
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
const { cycle } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// toast
const { setToastAlert } = useToast();
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Cycle link copied to clipboard.",
});
});
};
const progressIndicatorData = stateGroups.map((group, index) => ({
id: index,
name: group.title,
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
color: group.color,
}));
const groupedIssues: any = {
backlog: cycle.backlog_issues,
unstarted: cycle.unstarted_issues,
started: cycle.started_issues,
completed: cycle.completed_issues,
cancelled: cycle.cancelled_issues,
};
const handleRemoveFromFavorites = () => {};
const handleAddToFavorites = () => {};
const handleEditCycle = () => {};
const handleDeleteCycle = () => {};
return (
<div>
<div className="flex flex-col rounded-[10px] bg-custom-background-100 border border-custom-border-200 text-xs shadow">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="w-full">
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-1">
<span className="h-5 w-5">
<ContrastIcon
className="h-5 w-5"
color={`${
cycleStatus === "current"
? "#09A953"
: cycleStatus === "upcoming"
? "#F7AE59"
: cycleStatus === "completed"
? "#3F76FF"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: ""
}`}
/>
</span>
<Tooltip tooltipContent={cycle.name} className="break-words" position="top-left">
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 15)}</h3>
</Tooltip>
</span>
<span className="flex items-center gap-1 capitalize">
<span
className={`rounded-full px-1.5 py-0.5
${
cycleStatus === "current"
? "bg-green-600/5 text-green-600"
: cycleStatus === "upcoming"
? "bg-orange-300/5 text-orange-300"
: cycleStatus === "completed"
? "bg-blue-500/5 text-blue-500"
: cycleStatus === "draft"
? "bg-neutral-400/5 text-neutral-400"
: ""
}`}
>
{cycleStatus === "current" ? (
<span className="flex gap-1 whitespace-nowrap">
<PersonRunningIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
</span>
) : cycleStatus === "upcoming" ? (
<span className="flex gap-1 whitespace-nowrap">
<AlarmClockIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
</span>
) : cycleStatus === "completed" ? (
<span className="flex gap-1 whitespace-nowrap">
{cycle.total_issues - cycle.completed_issues > 0 && (
<Tooltip
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
}`}
>
<span>
<TriangleExclamationIcon className="h-3.5 w-3.5 fill-current" />
</span>
</Tooltip>
)}{" "}
Completed
</span>
) : (
cycleStatus
)}
</span>
{cycle.is_favorite ? (
<button onClick={handleRemoveFromFavorites}>
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button>
) : (
<button onClick={handleAddToFavorites}>
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
</button>
)}
</span>
</div>
<div className="flex h-4 items-center justify-start gap-5 text-custom-text-200">
{cycleStatus !== "draft" && (
<>
<div className="flex items-start gap-1">
<CalendarDaysIcon className="h-4 w-4" />
<span>{renderShortDateWithYearFormat(startDate)}</span>
</div>
<ArrowRightIcon className="h-4 w-4" />
<div className="flex items-start gap-1">
<TargetIcon className="h-4 w-4" />
<span>{renderShortDateWithYearFormat(endDate)}</span>
</div>
</>
)}
</div>
<div className="flex justify-between items-end">
<div className="flex flex-col gap-2 text-xs text-custom-text-200">
<div className="flex items-center gap-2">
<div className="w-16">Creator:</div>
<div className="flex items-center gap-2.5 text-custom-text-200">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<img
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
</div>
</div>
<div className="flex h-5 items-center gap-2">
<div className="w-16">Members:</div>
{cycle.assignees.length > 0 ? (
<div className="flex items-center gap-1 text-custom-text-200">
<AssigneesList users={cycle.assignees} length={4} />
</div>
) : (
"No members"
)}
</div>
</div>
<div className="flex items-center">
{!isCompleted && (
<button
onClick={handleEditCycle}
className="cursor-pointer rounded p-1 text-custom-text-200 duration-300 hover:bg-custom-background-80"
>
<PencilIcon className="h-4 w-4" />
</button>
)}
<CustomMenu width="auto" verticalEllipsis>
{!isCompleted && (
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
<span className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete cycle</span>
</span>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
handleCopyText();
}}
>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy cycle link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
</div>
</a>
</Link>
<div className="flex h-full flex-col rounded-b-[10px]">
<Disclosure>
{({ open }) => (
<div
className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-custom-border-200 bg-custom-background-80 text-custom-text-200 ${
open ? "" : "flex-row"
}`}
>
<div className="flex w-full items-center gap-2 px-4 py-1">
<span>Progress</span>
<Tooltip
tooltipContent={
<div className="flex w-56 flex-col">
{Object.keys(groupedIssues).map((group, index) => (
<SingleProgressStats
key={index}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full "
style={{
backgroundColor: stateGroups[index].color,
}}
/>
<span className="text-xs capitalize">{group}</span>
</div>
}
completed={groupedIssues[group]}
total={cycle.total_issues}
/>
))}
</div>
}
position="bottom"
>
<div className="flex w-full items-center">
<LinearProgressIndicator data={progressIndicatorData} noTooltip={true} />
</div>
</Tooltip>
<Disclosure.Button>
<span className="p-1">
<ChevronDownIcon className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
</span>
</Disclosure.Button>
</div>
<Transition show={open}>
<Disclosure.Panel>
<div className="overflow-hidden rounded-b-md bg-custom-background-80 py-3 shadow">
<div className="col-span-2 space-y-3 px-4">
<div className="space-y-3 text-xs">
{stateGroups.map((group) => (
<div key={group.key} className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
className="block h-2 w-2 rounded-full"
style={{
backgroundColor: group.color,
}}
/>
<h6 className="text-xs">{group.title}</h6>
</div>
<div>
<span>
{cycle[group.key as keyof ICycle] as number}{" "}
<span className="text-custom-text-200">
-{" "}
{cycle.total_issues > 0
? `${Math.round(
((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100
)}%`
: "0%"}
</span>
</span>
</div>
</div>
))}
</div>
</div>
</div>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
</div>
</div>
);
};
-53
View File
@@ -1,53 +0,0 @@
import { FC } from "react";
// types
import { ICycle } from "types";
// components
import { CyclesBoardCard } from "components/cycles";
export interface ICyclesBoard {
cycles: ICycle[];
filter: string;
}
export const CyclesBoard: FC<ICyclesBoard> = (props) => {
const { cycles, filter } = props;
return (
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
{cycles.length > 0 ? (
<>
{cycles.map((cycle) => (
<CyclesBoardCard key={cycle.id} cycle={cycle} filter={filter} />
))}
</>
) : (
<div className="h-full grid place-items-center text-center">
<div className="space-y-2">
<div className="mx-auto flex justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
<path
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
fill="rgb(var(--color-text-400))"
/>
</svg>
</div>
<h4 className="text-sm text-custom-text-200">{filter === "all" ? "No cycles" : `No ${filter} cycles`}</h4>
<button
type="button"
className="text-custom-primary-100 text-sm outline-none"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
}}
>
Create a new cycle
</button>
</div>
</div>
)}
</div>
);
};
-328
View File
@@ -1,328 +0,0 @@
import { FC, MouseEvent } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
// hooks
import useToast from "hooks/use-toast";
// ui
import { RadialProgressBar } from "@plane/ui";
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
// icons
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import {
TargetIcon,
ContrastIcon,
PersonRunningIcon,
ArrowRightIcon,
TriangleExclamationIcon,
AlarmClockIcon,
} from "components/icons";
import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
// helpers
import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// types
import { ICycle } from "types";
import { useMobxStore } from "lib/mobx/store-provider";
type TCyclesListItem = {
cycle: ICycle;
handleEditCycle?: () => void;
handleDeleteCycle?: () => void;
handleAddToFavorites?: () => void;
handleRemoveFromFavorites?: () => void;
};
const stateGroups = [
{
key: "backlog_issues",
title: "Backlog",
color: "#dee2e6",
},
{
key: "unstarted_issues",
title: "Unstarted",
color: "#26b5ce",
},
{
key: "started_issues",
title: "Started",
color: "#f7ae59",
},
{
key: "cancelled_issues",
title: "Cancelled",
color: "#d687ff",
},
{
key: "completed_issues",
title: "Completed",
color: "#09a953",
},
];
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
const { cycle } = props;
// store
const { cycle: cycleStore } = useMobxStore();
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// toast
const { setToastAlert } = useToast();
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Cycle link copied to clipboard.",
});
});
};
const progressIndicatorData = stateGroups.map((group, index) => ({
id: index,
name: group.title,
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
color: group.color,
}));
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
});
});
};
const handleRemoveFromFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
});
});
};
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
};
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
};
return (
<div>
<div className="flex flex-col text-xs hover:bg-custom-background-80">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="w-full">
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
<div className="flex items-center justify-between gap-1">
<div className="flex items-start gap-2">
<ContrastIcon
className="mt-1 h-5 w-5"
color={`${
cycleStatus === "current"
? "#09A953"
: cycleStatus === "upcoming"
? "#F7AE59"
: cycleStatus === "completed"
? "#3F76FF"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: ""
}`}
/>
<div className="max-w-2xl">
<Tooltip tooltipContent={cycle.name} className="break-words" position="top-left">
<h3 className="break-words w-full text-base font-semibold">{truncateText(cycle.name, 60)}</h3>
</Tooltip>
<p className="mt-2 text-custom-text-200 break-words w-full">{cycle.description}</p>
</div>
</div>
<div className="flex-shrink-0 flex items-center gap-4">
<span
className={`rounded-full px-1.5 py-0.5
${
cycleStatus === "current"
? "bg-green-600/5 text-green-600"
: cycleStatus === "upcoming"
? "bg-orange-300/5 text-orange-300"
: cycleStatus === "completed"
? "bg-blue-500/5 text-blue-500"
: cycleStatus === "draft"
? "bg-neutral-400/5 text-neutral-400"
: ""
}`}
>
{cycleStatus === "current" ? (
<span className="flex gap-1 whitespace-nowrap">
<PersonRunningIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.end_date ?? new Date())} days left
</span>
) : cycleStatus === "upcoming" ? (
<span className="flex gap-1">
<AlarmClockIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.start_date ?? new Date())} days left
</span>
) : cycleStatus === "completed" ? (
<span className="flex items-center gap-1">
{cycle.total_issues - cycle.completed_issues > 0 && (
<Tooltip
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
}`}
>
<span>
<TriangleExclamationIcon className="h-3.5 w-3.5 fill-current" />
</span>
</Tooltip>
)}{" "}
Completed
</span>
) : (
cycleStatus
)}
</span>
{cycleStatus !== "draft" && (
<div className="flex items-center justify-start gap-2 text-custom-text-200">
<div className="flex items-start gap-1 whitespace-nowrap">
<CalendarDaysIcon className="h-4 w-4" />
<span>{renderShortDateWithYearFormat(startDate)}</span>
</div>
<ArrowRightIcon className="h-4 w-4" />
<div className="flex items-start gap-1 whitespace-nowrap">
<TargetIcon className="h-4 w-4" />
<span>{renderShortDateWithYearFormat(endDate)}</span>
</div>
</div>
)}
<div className="flex items-center gap-2.5 text-custom-text-200">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<img
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
</div>
<Tooltip
position="top-right"
tooltipContent={
<div className="flex w-80 items-center gap-2 px-4 py-1">
<span>Progress</span>
<LinearProgressIndicator data={progressIndicatorData} />
</div>
}
>
<span
className={`rounded-md px-1.5 py-1
${
cycleStatus === "current"
? "border border-green-600 bg-green-600/5 text-green-600"
: cycleStatus === "upcoming"
? "border border-orange-300 bg-orange-300/5 text-orange-300"
: cycleStatus === "completed"
? "border border-blue-500 bg-blue-500/5 text-blue-500"
: cycleStatus === "draft"
? "border border-neutral-400 bg-neutral-400/5 text-neutral-400"
: ""
}`}
>
{cycleStatus === "current" ? (
<span className="flex gap-1 whitespace-nowrap">
{cycle.total_issues > 0 ? (
<>
<RadialProgressBar progress={(cycle.completed_issues / cycle.total_issues) * 100} />
<span>{Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} %</span>
</>
) : (
<span className="normal-case">No issues present</span>
)}
</span>
) : cycleStatus === "upcoming" ? (
<span className="flex gap-1">
<RadialProgressBar progress={100} /> Yet to start
</span>
) : cycleStatus === "completed" ? (
<span className="flex gap-1">
<RadialProgressBar progress={100} />
<span>{100} %</span>
</span>
) : (
<span className="flex gap-1">
<RadialProgressBar progress={(cycle.total_issues / cycle.completed_issues) * 100} />
{cycleStatus}
</span>
)}
</span>
</Tooltip>
{cycle.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}>
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button>
) : (
<button type="button" onClick={handleAddToFavorites}>
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
</button>
)}
<div className="flex items-center">
<CustomMenu width="auto" verticalEllipsis>
{!isCompleted && (
<CustomMenu.MenuItem onClick={handleEditCycle}>
<span className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit Cycle</span>
</span>
</CustomMenu.MenuItem>
)}
{!isCompleted && (
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
<span className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete cycle</span>
</span>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy cycle link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
</div>
</div>
</a>
</Link>
</div>
</div>
);
};
-70
View File
@@ -1,70 +0,0 @@
import { FC } from "react";
// ui
import { Loader } from "components/ui";
// types
import { ICycle } from "types";
import { CyclesListItem } from "./cycles-list-item";
export interface ICyclesList {
cycles: ICycle[];
filter: string;
}
export const CyclesList: FC<ICyclesList> = (props) => {
const { cycles, filter } = props;
return (
<div>
{cycles ? (
<>
{cycles.length > 0 ? (
<div className="divide-y divide-custom-border-200">
{cycles.map((cycle) => (
<div className="hover:bg-custom-background-80" key={cycle.id}>
<div className="flex flex-col border-custom-border-200">
<CyclesListItem cycle={cycle} />
</div>
</div>
))}
</div>
) : (
<div className="h-full grid place-items-center text-center">
<div className="space-y-2">
<div className="mx-auto flex justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
<path
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
fill="rgb(var(--color-text-400))"
/>
</svg>
</div>
<h4 className="text-sm text-custom-text-200">
{filter === "all" ? "No cycles" : `No ${filter} cycles`}
</h4>
<button
type="button"
className="text-custom-primary-100 text-sm outline-none"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
}}
>
Create a new cycle
</button>
</div>
</div>
)}
</>
) : (
<Loader className="space-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
)}
</div>
);
};
@@ -20,7 +20,8 @@ export const AllCyclesList: React.FC<Props> = ({ viewType }) => {
const { data: allCyclesList, mutate } = useSWR(
workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null,
workspaceSlug && projectId
? () => cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "all")
? () =>
cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "all")
: null
);
@@ -1,254 +0,0 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { KeyedMutator, mutate } from "swr";
// services
import cyclesService from "services/cycles.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
import useLocalStorage from "hooks/use-local-storage";
// components
import {
CreateUpdateCycleModal,
CyclesListGanttChartView,
DeleteCycleModal,
SingleCycleCard,
SingleCycleList,
} from "components/cycles";
// ui
import { Loader } from "components/ui";
// helpers
import { getDateRangeStatus } from "helpers/date-time.helper";
// types
import { ICycle } from "types";
// fetch-keys
import {
COMPLETED_CYCLES_LIST,
CURRENT_CYCLE_LIST,
CYCLES_LIST,
DRAFT_CYCLES_LIST,
UPCOMING_CYCLES_LIST,
} from "constants/fetch-keys";
import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle";
type Props = {
cycles: ICycle[] | undefined;
mutateCycles?: KeyedMutator<ICycle[]>;
viewType: string | null;
};
export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType }) => {
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState<ICycle | null>(null);
const [deleteCycleModal, setDeleteCycleModal] = useState(false);
const [selectedCycleToDelete, setSelectedCycleToDelete] = useState<ICycle | null>(null);
const { storedValue: cycleTab } = useLocalStorage("cycle_tab", "all");
console.log("cycleTab", cycleTab);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { user } = useUserAuth();
const { setToastAlert } = useToast();
const handleEditCycle = (cycle: ICycle) => {
setSelectedCycleToUpdate(cycle);
setCreateUpdateCycleModal(true);
};
const handleDeleteCycle = (cycle: ICycle) => {
setSelectedCycleToDelete(cycle);
setDeleteCycleModal(true);
};
const handleAddToFavorites = (cycle: ICycle) => {
if (!workspaceSlug || !projectId) return;
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const fetchKey =
cycleStatus === "current"
? CURRENT_CYCLE_LIST(projectId as string)
: cycleStatus === "upcoming"
? UPCOMING_CYCLES_LIST(projectId as string)
: cycleStatus === "completed"
? COMPLETED_CYCLES_LIST(projectId as string)
: DRAFT_CYCLES_LIST(projectId as string);
mutate<ICycle[]>(
fetchKey,
(prevData) =>
(prevData ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? true : c.is_favorite,
})),
false
);
mutate(
CYCLES_LIST(projectId as string),
(prevData: any) =>
(prevData ?? []).map((c: any) => ({
...c,
is_favorite: c.id === cycle.id ? true : c.is_favorite,
})),
false
);
cyclesService
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
cycle: cycle.id,
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
});
});
};
const handleRemoveFromFavorites = (cycle: ICycle) => {
if (!workspaceSlug || !projectId) return;
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const fetchKey =
cycleStatus === "current"
? CURRENT_CYCLE_LIST(projectId as string)
: cycleStatus === "upcoming"
? UPCOMING_CYCLES_LIST(projectId as string)
: cycleStatus === "completed"
? COMPLETED_CYCLES_LIST(projectId as string)
: DRAFT_CYCLES_LIST(projectId as string);
mutate<ICycle[]>(
fetchKey,
(prevData) =>
(prevData ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? false : c.is_favorite,
})),
false
);
mutate(
CYCLES_LIST(projectId as string),
(prevData: any) =>
(prevData ?? []).map((c: any) => ({
...c,
is_favorite: c.id === cycle.id ? false : c.is_favorite,
})),
false
);
cyclesService.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the cycle from favorites. Please try again.",
});
});
};
return (
<>
<CreateUpdateCycleModal
isOpen={createUpdateCycleModal}
handleClose={() => setCreateUpdateCycleModal(false)}
data={selectedCycleToUpdate}
user={user}
/>
<DeleteCycleModal
isOpen={deleteCycleModal}
setIsOpen={setDeleteCycleModal}
data={selectedCycleToDelete}
user={user}
/>
{cycles ? (
cycles.length > 0 ? (
viewType === "list" ? (
<div className="divide-y divide-custom-border-200">
{cycles.map((cycle) => (
<div className="hover:bg-custom-background-80">
<div className="flex flex-col border-custom-border-200">
<SingleCycleList
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
handleAddToFavorites={() => handleAddToFavorites(cycle)}
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
/>
</div>
</div>
))}
</div>
) : viewType === "board" ? (
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
{cycles.map((cycle) => (
<SingleCycleCard
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
handleAddToFavorites={() => handleAddToFavorites(cycle)}
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
/>
))}
</div>
) : (
<CyclesListGanttChartView cycles={cycles ?? []} mutateCycles={mutateCycles} />
)
) : (
<div className="h-full grid place-items-center text-center">
<div className="space-y-2">
<div className="mx-auto flex justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
<path
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
fill="rgb(var(--color-text-400))"
/>
</svg>
</div>
<h4 className="text-sm text-custom-text-200">
{cycleTab === "all" ? "No cycles" : `No ${cycleTab} cycles`}
</h4>
<button
type="button"
className="text-custom-primary-100 text-sm outline-none"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
}}
>
Create a new cycle
</button>
</div>
</div>
)
) : viewType === "list" ? (
<Loader className="space-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
) : viewType === "board" ? (
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<Loader.Item height="200px" />
<Loader.Item height="200px" />
<Loader.Item height="200px" />
</Loader>
) : (
<Loader>
<Loader.Item height="300px" />
</Loader>
)}
</>
);
};
+259 -49
View File
@@ -1,61 +1,271 @@
import { FC } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// store
import { useMobxStore } from "lib/mobx/store-provider";
import React, { useState } from "react";
import { useRouter } from "next/router";
import { KeyedMutator, mutate } from "swr";
// services
import cyclesService from "services/cycles.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
import useLocalStorage from "hooks/use-local-storage";
// components
import { CyclesBoard, CyclesList } from "components/cycles";
import {
CreateUpdateCycleModal,
CyclesListGanttChartView,
DeleteCycleModal,
SingleCycleCard,
SingleCycleList,
} from "components/cycles";
// ui
import { Loader } from "components/ui";
// helpers
import { getDateRangeStatus } from "helpers/date-time.helper";
// types
import { ICycle } from "types";
// fetch-keys
import {
COMPLETED_CYCLES_LIST,
CURRENT_CYCLE_LIST,
CYCLES_LIST,
DRAFT_CYCLES_LIST,
UPCOMING_CYCLES_LIST,
} from "constants/fetch-keys";
export interface ICyclesView {
filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete";
view: "list" | "board" | "gantt";
workspaceSlug: string;
projectId: string;
}
type Props = {
cycles: ICycle[] | undefined;
mutateCycles: KeyedMutator<ICycle[]>;
viewType: string | null;
};
export const CyclesView: FC<ICyclesView> = observer((props) => {
const { filter, view, workspaceSlug, projectId } = props;
// store
const { cycle: cycleStore } = useMobxStore();
// api call to fetch cycles list
const { isLoading } = useSWR(
workspaceSlug && projectId ? `CYCLES_LIST_${projectId}_${filter}` : null,
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null
);
export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType }) => {
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState<ICycle | null>(null);
const cyclesList = cycleStore.cycles?.[projectId];
console.log("cyclesList", cyclesList);
const [deleteCycleModal, setDeleteCycleModal] = useState(false);
const [selectedCycleToDelete, setSelectedCycleToDelete] = useState<ICycle | null>(null);
const { storedValue: cycleTab } = useLocalStorage("cycleTab", "All");
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { user } = useUserAuth();
const { setToastAlert } = useToast();
const handleEditCycle = (cycle: ICycle) => {
setSelectedCycleToUpdate(cycle);
setCreateUpdateCycleModal(true);
};
const handleDeleteCycle = (cycle: ICycle) => {
setSelectedCycleToDelete(cycle);
setDeleteCycleModal(true);
};
const handleAddToFavorites = (cycle: ICycle) => {
if (!workspaceSlug || !projectId) return;
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const fetchKey =
cycleStatus === "current"
? CURRENT_CYCLE_LIST(projectId as string)
: cycleStatus === "upcoming"
? UPCOMING_CYCLES_LIST(projectId as string)
: cycleStatus === "completed"
? COMPLETED_CYCLES_LIST(projectId as string)
: DRAFT_CYCLES_LIST(projectId as string);
mutate<ICycle[]>(
fetchKey,
(prevData) =>
(prevData ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? true : c.is_favorite,
})),
false
);
mutate(
CYCLES_LIST(projectId as string),
(prevData: any) =>
(prevData ?? []).map((c: any) => ({
...c,
is_favorite: c.id === cycle.id ? true : c.is_favorite,
})),
false
);
cyclesService
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
cycle: cycle.id,
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
});
});
};
const handleRemoveFromFavorites = (cycle: ICycle) => {
if (!workspaceSlug || !projectId) return;
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const fetchKey =
cycleStatus === "current"
? CURRENT_CYCLE_LIST(projectId as string)
: cycleStatus === "upcoming"
? UPCOMING_CYCLES_LIST(projectId as string)
: cycleStatus === "completed"
? COMPLETED_CYCLES_LIST(projectId as string)
: DRAFT_CYCLES_LIST(projectId as string);
mutate<ICycle[]>(
fetchKey,
(prevData) =>
(prevData ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? false : c.is_favorite,
})),
false
);
mutate(
CYCLES_LIST(projectId as string),
(prevData: any) =>
(prevData ?? []).map((c: any) => ({
...c,
is_favorite: c.id === cycle.id ? false : c.is_favorite,
})),
false
);
cyclesService
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the cycle from favorites. Please try again.",
});
});
};
return (
<>
{view === "list" && (
<>
{!isLoading ? (
<CyclesList cycles={cyclesList} filter={filter} />
<CreateUpdateCycleModal
isOpen={createUpdateCycleModal}
handleClose={() => setCreateUpdateCycleModal(false)}
data={selectedCycleToUpdate}
user={user}
/>
<DeleteCycleModal
isOpen={deleteCycleModal}
setIsOpen={setDeleteCycleModal}
data={selectedCycleToDelete}
user={user}
/>
{cycles ? (
cycles.length > 0 ? (
viewType === "list" ? (
<div className="divide-y divide-custom-border-200">
{cycles.map((cycle) => (
<div className="hover:bg-custom-background-80">
<div className="flex flex-col border-custom-border-200">
<SingleCycleList
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
handleAddToFavorites={() => handleAddToFavorites(cycle)}
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
/>
</div>
</div>
))}
</div>
) : viewType === "board" ? (
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
{cycles.map((cycle) => (
<SingleCycleCard
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
handleAddToFavorites={() => handleAddToFavorites(cycle)}
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
/>
))}
</div>
) : (
<Loader className="space-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
)}
</>
<CyclesListGanttChartView cycles={cycles ?? []} mutateCycles={mutateCycles} />
)
) : (
<div className="h-full grid place-items-center text-center">
<div className="space-y-2">
<div className="mx-auto flex justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="66"
height="66"
viewBox="0 0 66 66"
fill="none"
>
<circle
cx="34.375"
cy="34.375"
r="22"
stroke="rgb(var(--color-text-400))"
stroke-linecap="round"
/>
<path
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
fill="rgb(var(--color-text-400))"
/>
</svg>
</div>
<h4 className="text-sm text-custom-text-200">
{cycleTab === "All"
? "No cycles"
: `No ${cycleTab === "Drafts" ? "draft" : cycleTab?.toLowerCase()} cycles`}
</h4>
<button
type="button"
className="text-custom-primary-100 text-sm outline-none"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
}}
>
Create a new cycle
</button>
</div>
</div>
)
) : viewType === "list" ? (
<Loader className="space-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
) : viewType === "board" ? (
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<Loader.Item height="200px" />
<Loader.Item height="200px" />
<Loader.Item height="200px" />
</Loader>
) : (
<Loader>
<Loader.Item height="300px" />
</Loader>
)}
{view === "board" && (
<>
{!isLoading ? (
<CyclesBoard cycles={cyclesList} filter={filter} />
) : (
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<Loader.Item height="200px" />
<Loader.Item height="200px" />
<Loader.Item height="200px" />
</Loader>
)}
</>
)}
{view === "gantt" && <CyclesList cycles={cyclesList} filter={filter} />}
</>
);
});
};
+54 -43
View File
@@ -1,33 +1,50 @@
import { useEffect } from "react";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// ui
import { Input, TextArea } from "@plane/ui";
import { DateSelect, PrimaryButton, SecondaryButton } from "components/ui";
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// types
import { ICycle } from "types";
type Props = {
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
handleClose: () => void;
status: boolean;
data?: ICycle | null;
};
export const CycleForm: React.FC<Props> = (props) => {
const { handleFormSubmit, handleClose, data } = props;
// form data
const defaultValues: Partial<ICycle> = {
name: "",
description: "",
start_date: null,
end_date: null,
};
export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
control,
reset,
watch,
} = useForm<ICycle>({
defaultValues: {
name: data?.name || "",
description: data?.description || "",
start_date: data?.start_date || null,
end_date: data?.end_date || null,
},
defaultValues,
});
const handleCreateUpdateCycle = async (formData: Partial<ICycle>) => {
await handleFormSubmit(formData);
};
useEffect(() => {
reset({
...defaultValues,
...data,
});
}, [data, reset]);
const startDate = watch("start_date");
const endDate = watch("end_date");
@@ -38,50 +55,39 @@ export const CycleForm: React.FC<Props> = (props) => {
maxDate?.setDate(maxDate.getDate() - 1);
return (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<form onSubmit={handleSubmit(handleCreateUpdateCycle)}>
<div className="space-y-5">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{status ? "Update" : "Create"} Cycle</h3>
<h3 className="text-lg font-medium leading-6 text-custom-text-100">
{status ? "Update" : "Create"} Cycle
</h3>
<div className="space-y-3">
<div>
<Controller
<Input
autoComplete="off"
id="name"
name="name"
control={control}
rules={{
type="name"
className="resize-none text-xl"
placeholder="Title"
error={errors.name}
register={register}
validations={{
required: "Name is required",
maxLength: {
value: 255,
message: "Name should be less than 255 characters",
},
}}
render={({ field: { value, onChange } }) => (
<Input
id="cycle_name"
name="name"
type="text"
placeholder="Cycle Name"
className="resize-none text-xl w-full p-2"
value={value}
onChange={onChange}
hasError={Boolean(errors?.name)}
/>
)}
/>
</div>
<div>
<Controller
<TextArea
id="description"
name="description"
control={control}
render={({ field: { value, onChange } }) => (
<TextArea
id="cycle_description"
name="description"
placeholder="Description"
className="h-32 resize-none text-sm"
hasError={Boolean(errors?.description)}
value={value}
onChange={onChange}
/>
)}
placeholder="Description"
className="h-32 resize-none text-sm"
error={errors.description}
register={register}
/>
</div>
@@ -106,7 +112,12 @@ export const CycleForm: React.FC<Props> = (props) => {
control={control}
name="end_date"
render={({ field: { value, onChange } }) => (
<DateSelect label="End date" value={value} onChange={(val) => onChange(val)} minDate={minDate} />
<DateSelect
label="End date"
value={value}
onChange={(val) => onChange(val)}
minDate={minDate}
/>
)}
/>
</div>
@@ -116,7 +127,7 @@ export const CycleForm: React.FC<Props> = (props) => {
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-custom-border-200 px-5 pt-5">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{data
{status
? isSubmitting
? "Updating Cycle..."
: "Update Cycle"
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
import useIssuesView from "hooks/use-issues-view";
import useUser from "hooks/use-user";
import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view";
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
@@ -46,7 +47,9 @@ export const CycleIssuesGanttChartView: React.FC<Props> = ({ disableUserActions
title="Issues"
loaderTitle="Issues"
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
blockUpdateHandler={(block, payload) => {}}
blockUpdateHandler={(block, payload) =>
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
}
SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock}
enableBlockLeftResize={isAllowed}
@@ -17,7 +17,7 @@ import { ICycle } from "types";
type Props = {
cycles: ICycle[];
mutateCycles?: KeyedMutator<ICycle[]>;
mutateCycles: KeyedMutator<ICycle[]>;
};
export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) => {
@@ -29,32 +29,33 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
if (!workspaceSlug || !user) return;
mutateCycles &&
mutateCycles((prevData: any) => {
if (!prevData) return prevData;
const newList = prevData.map((p: any) => ({
...p,
...(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,
}
: {}),
}));
mutateCycles((prevData: any) => {
if (!prevData) return prevData;
if (payload.sort_order) {
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
}
const newList = prevData.map((p: any) => ({
...p,
...(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,
}
: {}),
}));
return newList;
}, false);
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;
if (newPayload.sort_order && payload.sort_order)
newPayload.sort_order = payload.sort_order.newSortOrder;
cyclesService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload, user);
};
@@ -62,7 +63,9 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
const blockFormat = (blocks: ICycle[]) =>
blocks && blocks.length > 0
? blocks
.filter((b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date))
.filter(
(b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)
)
.map((block) => ({
data: block,
id: block.id,
+1 -6
View File
@@ -1,4 +1,4 @@
export * from "./cycles-view";
export * from "./cycles-list";
export * from "./active-cycle-details";
export * from "./active-cycle-stats";
export * from "./gantt-chart";
@@ -12,8 +12,3 @@ export * from "./single-cycle-card";
export * from "./single-cycle-list";
export * from "./transfer-issues-modal";
export * from "./transfer-issues";
export * from "./cycles-list";
export * from "./cycles-list-item";
export * from "./cycles-board";
export * from "./cycles-board-card";
export * from "./cycles-gantt";
+28 -8
View File
@@ -1,6 +1,10 @@
import { Fragment } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import cycleService from "services/cycles.service";
@@ -30,7 +34,12 @@ type CycleModalProps = {
user: ICurrentUserResponse | undefined;
};
export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({ isOpen, handleClose, data, user }) => {
export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
isOpen,
handleClose,
data,
user,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -107,7 +116,10 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({ isOpen, hand
mutate(DRAFT_CYCLES_LIST(projectId.toString()));
}
mutate(CYCLES_LIST(projectId.toString()));
if (getDateRangeStatus(data?.start_date, data?.end_date) != getDateRangeStatus(res.start_date, res.end_date)) {
if (
getDateRangeStatus(data?.start_date, data?.end_date) !=
getDateRangeStatus(res.start_date, res.end_date)
) {
switch (getDateRangeStatus(res.start_date, res.end_date)) {
case "completed":
mutate(COMPLETED_CYCLES_LIST(projectId.toString()));
@@ -129,7 +141,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({ isOpen, hand
message: "Cycle updated successfully.",
});
})
.catch(() => {
.catch((err) => {
setToastAlert({
type: "error",
title: "Error!",
@@ -141,9 +153,11 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({ isOpen, hand
const dateChecker = async (payload: CycleDateCheckData) => {
let status = false;
await cycleService.cycleDateCheck(workspaceSlug as string, projectId as string, payload).then((res) => {
status = res.status;
});
await cycleService
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
.then((res) => {
status = res.status;
});
return status;
};
@@ -180,7 +194,8 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({ isOpen, hand
setToastAlert({
type: "error",
title: "Error!",
message: "You already have a cycle on the given dates, if you want to create a draft cycle, remove the dates.",
message:
"You already have a cycle on the given dates, if you want to create a draft cycle, remove the dates.",
});
};
@@ -210,7 +225,12 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({ isOpen, hand
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<CycleForm handleFormSubmit={handleFormSubmit} handleClose={handleClose} data={data} />
<CycleForm
handleFormSubmit={handleFormSubmit}
handleClose={handleClose}
status={data ? true : false}
data={data}
/>
</Dialog.Panel>
</Transition.Child>
</div>
+29 -7
View File
@@ -25,12 +25,17 @@ const tabOptions = [
},
];
const EmojiIconPicker: React.FC<Props> = (props) => {
const { label, value, onChange, onIconColorChange, disabled = false } = props;
// states
const EmojiIconPicker: React.FC<Props> = ({
label,
value,
onChange,
onIconColorChange,
disabled = false,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [openColorPicker, setOpenColorPicker] = useState(false);
const [activeColor, setActiveColor] = useState<string>("rgb(var(--color-text-200))");
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
const emojiPickerRef = useRef<HTMLDivElement>(null);
@@ -47,7 +52,11 @@ const EmojiIconPicker: React.FC<Props> = (props) => {
return (
<Popover className="relative z-[1]">
<Popover.Button onClick={() => setIsOpen((prev) => !prev)} className="outline-none" disabled={disabled}>
<Popover.Button
onClick={() => setIsOpen((prev) => !prev)}
className="outline-none"
disabled={disabled}
>
{label}
</Popover.Button>
<Transition
@@ -130,7 +139,15 @@ const EmojiIconPicker: React.FC<Props> = (props) => {
<Tab.Panel className="flex h-full w-full flex-col justify-center">
<div className="relative">
<div className="flex items-center justify-between px-1 pb-2">
{["#FF6B00", "#8CC1FF", "#FCBE1D", "#18904F", "#ADF672", "#05C3FF", "#000000"].map((curCol) => (
{[
"#FF6B00",
"#8CC1FF",
"#FCBE1D",
"#18904F",
"#ADF672",
"#05C3FF",
"#000000",
].map((curCol) => (
<span
key={curCol}
className="h-4 w-4 cursor-pointer rounded-full"
@@ -151,7 +168,9 @@ const EmojiIconPicker: React.FC<Props> = (props) => {
</div>
<div>
<TwitterPicker
className={`!absolute top-4 left-4 z-10 m-2 ${openColorPicker ? "block" : "hidden"}`}
className={`!absolute top-4 left-4 z-10 m-2 ${
openColorPicker ? "block" : "hidden"
}`}
color={activeColor}
onChange={(color) => {
setActiveColor(color.hex);
@@ -174,7 +193,10 @@ const EmojiIconPicker: React.FC<Props> = (props) => {
setIsOpen(false);
}}
>
<span style={{ color: activeColor }} className="material-symbols-rounded text-lg">
<span
style={{ color: activeColor }}
className="material-symbols-rounded text-lg"
>
{icon.name}
</span>
</button>
@@ -9,7 +9,7 @@ import { useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import estimatesService from "services/project_estimates.service";
import estimatesService from "services/estimates.service";
// hooks
import useToast from "hooks/use-toast";
// ui
@@ -119,7 +119,13 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
);
await estimatesService
.patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload, user)
.patchEstimate(
workspaceSlug as string,
projectId as string,
data?.id as string,
payload,
user
)
.then(() => {
mutate(ESTIMATES_LIST(projectId.toString()));
mutate(ESTIMATE_DETAILS(data.id));
@@ -251,7 +257,9 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-3">
<div className="text-lg font-medium leading-6">{data ? "Update" : "Create"} Estimate</div>
<div className="text-lg font-medium leading-6">
{data ? "Update" : "Create"} Estimate
</div>
<div>
<Input
id="name"
+37 -11
View File
@@ -1,9 +1,10 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import { CSVIntegrationService } from "services/csv.service";
import CSVIntegrationService from "services/integration/csv.services";
// hooks
import useToast from "hooks/use-toast";
// ui
@@ -22,9 +23,13 @@ type Props = {
mutateServices: () => void;
};
const cvsService = new CSVIntegrationService();
export const Exporter: React.FC<Props> = ({ isOpen, handleClose, user, provider, mutateServices }) => {
export const Exporter: React.FC<Props> = ({
isOpen,
handleClose,
user,
provider,
mutateServices,
}) => {
const [exportLoading, setExportLoading] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -55,8 +60,7 @@ export const Exporter: React.FC<Props> = ({ isOpen, handleClose, user, provider,
project: value,
multiple: multiple,
};
await cvsService
.exportCSVService(workspaceSlug as string, payload, user)
await CSVIntegrationService.exportCSVService(workspaceSlug as string, payload, user)
.then(() => {
mutateServices();
router.push(`/${workspaceSlug}/settings/exports`);
@@ -65,7 +69,13 @@ export const Exporter: React.FC<Props> = ({ isOpen, handleClose, user, provider,
type: "success",
title: "Export Successful",
message: `You will be able to download the exported ${
provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : ""
provider === "csv"
? "CSV"
: provider === "xlsx"
? "Excel"
: provider === "json"
? "JSON"
: ""
} from the previous export.`,
});
})
@@ -112,7 +122,13 @@ export const Exporter: React.FC<Props> = ({ isOpen, handleClose, user, provider,
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">
Export to{" "}
{provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : ""}
{provider === "csv"
? "CSV"
: provider === "xlsx"
? "Excel"
: provider === "json"
? "JSON"
: ""}
</h3>
</span>
</div>
@@ -139,12 +155,22 @@ export const Exporter: React.FC<Props> = ({ isOpen, handleClose, user, provider,
onClick={() => setMultiple(!multiple)}
className="flex items-center gap-2 max-w-min cursor-pointer"
>
<input type="checkbox" checked={multiple} onChange={() => setMultiple(!multiple)} />
<div className="text-sm whitespace-nowrap">Export the data into separate files</div>
<input
type="checkbox"
checked={multiple}
onChange={() => setMultiple(!multiple)}
/>
<div className="text-sm whitespace-nowrap">
Export the data into separate files
</div>
</div>
<div className="flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton onClick={ExportCSVToMail} disabled={exportLoading} loading={exportLoading}>
<PrimaryButton
onClick={ExportCSVToMail}
disabled={exportLoading}
loading={exportLoading}
>
{exportLoading ? "Exporting..." : "Export"}
</PrimaryButton>
</div>
+31 -12
View File
@@ -9,7 +9,7 @@ import useSWR, { mutate } from "swr";
// hooks
import useUserAuth from "hooks/use-user-auth";
// services
import IntegrationService from "services/integration.service";
import IntegrationService from "services/integration";
// components
import { Exporter, SingleExport } from "components/exporter";
// ui
@@ -32,7 +32,9 @@ const IntegrationGuide = () => {
const { user } = useUserAuth();
const { data: exporterServices } = useSWR(
workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null,
workspaceSlug && cursor
? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`)
: null,
workspaceSlug && cursor
? () => IntegrationService.getExportsServicesList(workspaceSlug as string, cursor, per_page)
: null
@@ -55,11 +57,20 @@ const IntegrationGuide = () => {
<div className="flex items-start justify-between gap-4 w-full">
<div className="flex item-center gap-2.5">
<div className="relative h-10 w-10 flex-shrink-0">
<Image src={service.logo} layout="fill" objectFit="cover" alt={`${service.title} Logo`} />
<Image
src={service.logo}
layout="fill"
objectFit="cover"
alt={`${service.title} Logo`}
/>
</div>
<div>
<h3 className="flex items-center gap-4 text-sm font-medium">{service.title}</h3>
<p className="text-sm text-custom-text-200 tracking-tight">{service.description}</p>
<h3 className="flex items-center gap-4 text-sm font-medium">
{service.title}
</h3>
<p className="text-sm text-custom-text-200 tracking-tight">
{service.description}
</p>
</div>
</div>
<div className="flex-shrink-0">
@@ -85,9 +96,9 @@ const IntegrationGuide = () => {
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
onClick={() => {
setRefreshing(true);
mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`)).then(() =>
setRefreshing(false)
);
mutate(
EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`)
).then(() => setRefreshing(false));
}}
>
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
@@ -97,7 +108,9 @@ const IntegrationGuide = () => {
<div className="flex gap-2 items-center text-xs">
<button
disabled={!exporterServices?.prev_page_results}
onClick={() => exporterServices?.prev_page_results && setCursor(exporterServices?.prev_cursor)}
onClick={() =>
exporterServices?.prev_page_results && setCursor(exporterServices?.prev_cursor)
}
className={`flex items-center border border-custom-primary-100 text-custom-primary-100 px-1 rounded ${
exporterServices?.prev_page_results
? "cursor-pointer hover:bg-custom-primary-100 hover:text-white"
@@ -109,7 +122,9 @@ const IntegrationGuide = () => {
</button>
<button
disabled={!exporterServices?.next_page_results}
onClick={() => exporterServices?.next_page_results && setCursor(exporterServices?.next_cursor)}
onClick={() =>
exporterServices?.next_page_results && setCursor(exporterServices?.next_cursor)
}
className={`flex items-center border border-custom-primary-100 text-custom-primary-100 px-1 rounded ${
exporterServices?.next_page_results
? "cursor-pointer hover:bg-custom-primary-100 hover:text-white"
@@ -132,7 +147,9 @@ const IntegrationGuide = () => {
</div>
</div>
) : (
<p className="text-sm text-custom-text-200 px-4 py-6">No previous export available.</p>
<p className="text-sm text-custom-text-200 px-4 py-6">
No previous export available.
</p>
)
) : (
<Loader className="mt-6 grid grid-cols-1 gap-3">
@@ -152,7 +169,9 @@ const IntegrationGuide = () => {
data={null}
user={user}
provider={provider}
mutateServices={() => mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`))}
mutateServices={() =>
mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`))
}
/>
)}
</div>
@@ -6,7 +6,10 @@ import { allViewsWithData, currentViewDataWithView } from "../data";
export const ChartContext = createContext<ChartContextReducer | undefined>(undefined);
const chartReducer = (state: ChartContextData, action: ChartContextActionPayload): ChartContextData => {
const chartReducer = (
state: ChartContextData,
action: ChartContextActionPayload
): ChartContextData => {
switch (action.type) {
case "CURRENT_VIEW":
return { ...state, currentView: action.payload };
@@ -47,7 +50,9 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
};
return (
<ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}>
<ChartContext.Provider
value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}
>
{children}
</ChartContext.Provider>
);
@@ -0,0 +1,41 @@
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: any) => {
if (!prevData) return prevData;
const newList = prevData.map((p: any) => ({
...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);
};
-2
View File
@@ -1,2 +0,0 @@
export * from "./module-issues";
export * from "./project-issues";
-94
View File
@@ -1,94 +0,0 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
// types
import { IIssueDisplayFilterOptions, IIssueFilterOptions, TIssueLayouts } from "types";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
export const ModuleIssuesHeader: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
const { issueFilter: issueFilterStore, moduleFilter: moduleFilterStore } = useMobxStore();
const activeLayout = issueFilterStore.userDisplayFilters.layout;
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
layout,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId || !moduleId) return;
const newValues = moduleFilterStore.userModuleFilters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (moduleFilterStore.userModuleFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
moduleFilterStore.updateUserModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), {
[key]: newValues,
});
},
[moduleId, moduleFilterStore, projectId, workspaceSlug]
);
const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
return (
<div className="flex items-center gap-2">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters">
<FilterSelection
filters={moduleFilterStore.userModuleFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
projectId={projectId?.toString() ?? ""}
/>
</FiltersDropdown>
<FiltersDropdown title="View">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
/>
</FiltersDropdown>
</div>
);
});
-96
View File
@@ -1,96 +0,0 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
// types
import { IIssueDisplayFilterOptions, IIssueFilterOptions, TIssueLayouts } from "types";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
export const ProjectIssuesHeader: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { issueFilter: issueFilterStore } = useMobxStore();
const activeLayout = issueFilterStore.userDisplayFilters.layout;
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
layout,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilterStore.userFilters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilterStore.userFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
[key]: newValues,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
return (
<div className="flex items-center gap-2">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters">
<FilterSelection
filters={issueFilterStore.userFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
projectId={projectId?.toString() ?? ""}
/>
</FiltersDropdown>
<FiltersDropdown title="View">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
/>
</FiltersDropdown>
</div>
);
});
+1 -1
View File
@@ -20,7 +20,7 @@ export const ModuleCancelledIcon: React.FC<Props> = ({
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_4052_100277)">
<g clip-path="url(#clip0_4052_100277)">
<path
d="M8 8.84L10.58 11.42C10.7 11.54 10.84 11.6 11 11.6C11.16 11.6 11.3 11.54 11.42 11.42C11.54 11.3 11.6 11.16 11.6 11C11.6 10.84 11.54 10.7 11.42 10.58L8.84 8L11.42 5.42C11.54 5.3 11.6 5.16 11.6 5C11.6 4.84 11.54 4.7 11.42 4.58C11.3 4.46 11.16 4.4 11 4.4C10.84 4.4 10.7 4.46 10.58 4.58L8 7.16L5.42 4.58C5.3 4.46 5.16 4.4 5 4.4C4.84 4.4 4.7 4.46 4.58 4.58C4.46 4.7 4.4 4.84 4.4 5C4.4 5.16 4.46 5.3 4.58 5.42L7.16 8L4.58 10.58C4.46 10.7 4.4 10.84 4.4 11C4.4 11.16 4.46 11.3 4.58 11.42C4.7 11.54 4.84 11.6 5 11.6C5.16 11.6 5.3 11.54 5.42 11.42L8 8.84ZM8 16C6.90667 16 5.87333 15.79 4.9 15.37C3.92667 14.95 3.07667 14.3767 2.35 13.65C1.62333 12.9233 1.05 12.0733 0.63 11.1C0.21 10.1267 0 9.09333 0 8C0 6.89333 0.21 5.85333 0.63 4.88C1.05 3.90667 1.62333 3.06 2.35 2.34C3.07667 1.62 3.92667 1.05 4.9 0.63C5.87333 0.21 6.90667 0 8 0C9.10667 0 10.1467 0.21 11.12 0.63C12.0933 1.05 12.94 1.62 13.66 2.34C14.38 3.06 14.95 3.90667 15.37 4.88C15.79 5.85333 16 6.89333 16 8C16 9.09333 15.79 10.1267 15.37 11.1C14.95 12.0733 14.38 12.9233 13.66 13.65C12.94 14.3767 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM8 14.8C9.89333 14.8 11.5 14.1367 12.82 12.81C14.14 11.4833 14.8 9.88 14.8 8C14.8 6.10667 14.14 4.5 12.82 3.18C11.5 1.86 9.89333 1.2 8 1.2C6.12 1.2 4.51667 1.86 3.19 3.18C1.86333 4.5 1.2 6.10667 1.2 8C1.2 9.88 1.86333 11.4833 3.19 12.81C4.51667 14.1367 6.12 14.8 8 14.8Z"
fill="#ef4444"
+1 -1
View File
@@ -16,7 +16,7 @@ export const ModulePausedIcon: React.FC<Props> = ({ width = "20", height = "20",
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_4052_100275)">
<g clip-path="url(#clip0_4052_100275)">
<path
d="M6.4435 10.34C6.6145 10.34 6.75667 10.2825 6.87 10.1675C6.98333 10.0525 7.04 9.91 7.04 9.74V6.24C7.04 6.07 6.98217 5.9275 6.8665 5.8125C6.75082 5.6975 6.60749 5.64 6.4365 5.64C6.2655 5.64 6.12333 5.6975 6.01 5.8125C5.89667 5.9275 5.84 6.07 5.84 6.24V9.74C5.84 9.91 5.89783 10.0525 6.0135 10.1675C6.12918 10.2825 6.27251 10.34 6.4435 10.34ZM9.5635 10.34C9.7345 10.34 9.87667 10.2825 9.99 10.1675C10.1033 10.0525 10.16 9.91 10.16 9.74V6.24C10.16 6.07 10.1022 5.9275 9.9865 5.8125C9.87082 5.6975 9.72749 5.64 9.5565 5.64C9.3855 5.64 9.24333 5.6975 9.13 5.8125C9.01667 5.9275 8.96 6.07 8.96 6.24V9.74C8.96 9.91 9.01783 10.0525 9.1335 10.1675C9.24918 10.2825 9.39251 10.34 9.5635 10.34ZM8 16C6.89333 16 5.85333 15.79 4.88 15.37C3.90667 14.95 3.06 14.38 2.34 13.66C1.62 12.94 1.05 12.0933 0.63 11.12C0.21 10.1467 0 9.10667 0 8C0 7.54667 0.0366667 7.09993 0.11 6.6598C0.183333 6.21965 0.293333 5.78639 0.44 5.36C0.493333 5.21333 0.593333 5.11667 0.74 5.07C0.886667 5.02333 1.02667 5.04199 1.16 5.12596C1.30285 5.20993 1.40523 5.33327 1.46714 5.49596C1.52905 5.65865 1.54 5.82 1.5 5.98C1.42 6.31333 1.35 6.64765 1.29 6.98294C1.23 7.31823 1.2 7.65725 1.2 8C1.2 9.89833 1.85875 11.5063 3.17624 12.8238C4.49375 14.1413 6.10167 14.8 8 14.8C9.89833 14.8 11.5063 14.1413 12.8238 12.8238C14.1413 11.5063 14.8 9.89833 14.8 8C14.8 6.10167 14.1413 4.49375 12.8238 3.17624C11.5063 1.85875 9.89833 1.2 8 1.2C7.63235 1.2 7.26852 1.22667 6.90852 1.28C6.54852 1.33333 6.19235 1.41333 5.84 1.52C5.68 1.57333 5.52 1.56667 5.36 1.5C5.2 1.43333 5.08667 1.32667 5.02 1.18C4.95333 1.03333 4.96 0.886667 5.04 0.74C5.12 0.593333 5.23333 0.493333 5.38 0.44C5.79333 0.306667 6.21333 0.2 6.64 0.12C7.06667 0.04 7.49333 0 7.92 0C9.02667 0 10.07 0.21 11.05 0.63C12.03 1.05 12.8863 1.62 13.6189 2.34C14.3516 3.06 14.9316 3.90667 15.3589 4.88C15.7863 5.85333 16 6.89333 16 8C16 9.10667 15.79 10.1467 15.37 11.12C14.95 12.0933 14.38 12.94 13.66 13.66C12.94 14.38 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM2.65764 3.62C2.37921 3.62 2.14333 3.52255 1.95 3.32764C1.75667 3.13275 1.66 2.89608 1.66 2.61764C1.66 2.33921 1.75745 2.10333 1.95236 1.91C2.14725 1.71667 2.38392 1.62 2.66236 1.62C2.94079 1.62 3.17667 1.71745 3.37 1.91236C3.56333 2.10725 3.66 2.34392 3.66 2.62236C3.66 2.90079 3.56255 3.13667 3.36764 3.33C3.17275 3.52333 2.93608 3.62 2.65764 3.62Z"
fill="#525252"
+1 -1
View File
@@ -19,6 +19,6 @@ export const StateGroupBacklogIcon: React.FC<Props> = ({
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="6" cy="6" r="5.6" stroke={color} strokeWidth="0.8" strokeDasharray="4 4" />
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" stroke-dasharray="4 4" />
</svg>
);

Some files were not shown because too many files have changed in this diff Show More