forked from github/plane
Compare commits
26 Commits
tiptap-pac
...
fix/issue_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f25bebd92 | ||
|
|
85b838e0df | ||
|
|
42d38f7531 | ||
|
|
61672f47ac | ||
|
|
23e62c83eb | ||
|
|
e58b76c8a6 | ||
|
|
4ce01ca4f8 | ||
|
|
a34b0b059d | ||
|
|
164e0b9301 | ||
|
|
5a91031243 | ||
|
|
47bec7704b | ||
|
|
b9c935092a | ||
|
|
3a2a329000 | ||
|
|
8e9a4dca78 | ||
|
|
cdb888c23e | ||
|
|
2186db8bba | ||
|
|
9bff10de6d | ||
|
|
6867154963 | ||
|
|
7bb73b74ba | ||
|
|
991258084e | ||
|
|
1a37668f0b | ||
|
|
4447a4b519 | ||
|
|
7842c4b2ea | ||
|
|
8de93d0081 | ||
|
|
5b228bd1eb | ||
|
|
ad8a011bb9 |
@@ -23,6 +23,8 @@ NEXT_PUBLIC_SLACK_CLIENT_ID=""
|
||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
|
||||
# public boards deploy url
|
||||
NEXT_PUBLIC_DEPLOY_URL=""
|
||||
# plane deploy using nginx
|
||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
# Backend
|
||||
# Debug value for api server use it as 0 for production use
|
||||
|
||||
2
.github/workflows/Update_Docker_Images.yml
vendored
2
.github/workflows/Update_Docker_Images.yml
vendored
@@ -2,7 +2,7 @@ name: Update Docker Images for Plane on Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
types: [released, prereleased]
|
||||
|
||||
jobs:
|
||||
build_push_backend:
|
||||
|
||||
@@ -31,8 +31,6 @@ from .issue import (
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssuePropertySerializer,
|
||||
BlockerIssueSerializer,
|
||||
BlockedIssueSerializer,
|
||||
IssueAssigneeSerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
@@ -45,6 +43,8 @@ from .issue import (
|
||||
IssueReactionSerializer,
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssueRelationSerializer,
|
||||
RelatedIssueSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
|
||||
|
||||
@@ -17,12 +17,10 @@ from plane.db.models import (
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
IssueProperty,
|
||||
IssueBlocker,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
IssueLabel,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
CycleIssue,
|
||||
Cycle,
|
||||
Module,
|
||||
@@ -32,6 +30,7 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
IssueVote,
|
||||
IssueRelation,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,25 +80,12 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
required=False,
|
||||
)
|
||||
|
||||
# List of issues that are blocking this issue
|
||||
blockers_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
labels_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
# List of issues that are blocked by this issue
|
||||
blocks_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = "__all__"
|
||||
@@ -122,10 +108,8 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
blockers = validated_data.pop("blockers_list", None)
|
||||
assignees = validated_data.pop("assignees_list", None)
|
||||
labels = validated_data.pop("labels_list", None)
|
||||
blocks = validated_data.pop("blocks_list", None)
|
||||
|
||||
project_id = self.context["project_id"]
|
||||
workspace_id = self.context["workspace_id"]
|
||||
@@ -137,22 +121,6 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
created_by_id = issue.created_by_id
|
||||
updated_by_id = issue.updated_by_id
|
||||
|
||||
if blockers is not None and len(blockers):
|
||||
IssueBlocker.objects.bulk_create(
|
||||
[
|
||||
IssueBlocker(
|
||||
block=issue,
|
||||
blocked_by=blocker,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for blocker in blockers
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if assignees is not None and len(assignees):
|
||||
IssueAssignee.objects.bulk_create(
|
||||
[
|
||||
@@ -196,29 +164,11 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if blocks is not None and len(blocks):
|
||||
IssueBlocker.objects.bulk_create(
|
||||
[
|
||||
IssueBlocker(
|
||||
block=block,
|
||||
blocked_by=issue,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for block in blocks
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
return issue
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
blockers = validated_data.pop("blockers_list", None)
|
||||
assignees = validated_data.pop("assignees_list", None)
|
||||
labels = validated_data.pop("labels_list", None)
|
||||
blocks = validated_data.pop("blocks_list", None)
|
||||
|
||||
# Related models
|
||||
project_id = instance.project_id
|
||||
@@ -226,23 +176,6 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
created_by_id = instance.created_by_id
|
||||
updated_by_id = instance.updated_by_id
|
||||
|
||||
if blockers is not None:
|
||||
IssueBlocker.objects.filter(block=instance).delete()
|
||||
IssueBlocker.objects.bulk_create(
|
||||
[
|
||||
IssueBlocker(
|
||||
block=instance,
|
||||
blocked_by=blocker,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for blocker in blockers
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if assignees is not None:
|
||||
IssueAssignee.objects.filter(issue=instance).delete()
|
||||
IssueAssignee.objects.bulk_create(
|
||||
@@ -277,23 +210,6 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if blocks is not None:
|
||||
IssueBlocker.objects.filter(blocked_by=instance).delete()
|
||||
IssueBlocker.objects.bulk_create(
|
||||
[
|
||||
IssueBlocker(
|
||||
block=block,
|
||||
blocked_by=instance,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for block in blocks
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
# Time updation occues even when other related models are updated
|
||||
instance.updated_at = timezone.now()
|
||||
return super().update(instance, validated_data)
|
||||
@@ -375,32 +291,39 @@ class IssueLabelSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class BlockedIssueSerializer(BaseSerializer):
|
||||
blocked_issue_detail = IssueProjectLiteSerializer(source="block", read_only=True)
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
related_issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||
|
||||
class Meta:
|
||||
model = IssueBlocker
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"blocked_issue_detail",
|
||||
"blocked_by",
|
||||
"block",
|
||||
"related_issue_detail",
|
||||
"relation_type",
|
||||
"related_issue",
|
||||
"issue",
|
||||
"id"
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class BlockerIssueSerializer(BaseSerializer):
|
||||
blocker_issue_detail = IssueProjectLiteSerializer(
|
||||
source="blocked_by", read_only=True
|
||||
)
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
||||
|
||||
class Meta:
|
||||
model = IssueBlocker
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"blocker_issue_detail",
|
||||
"blocked_by",
|
||||
"block",
|
||||
"issue_detail",
|
||||
"relation_type",
|
||||
"related_issue",
|
||||
"issue",
|
||||
"id"
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class IssueAssigneeSerializer(BaseSerializer):
|
||||
@@ -617,10 +540,8 @@ class IssueSerializer(BaseSerializer):
|
||||
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
# List of issues blocked by this issue
|
||||
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
|
||||
# List of issues that block this issue
|
||||
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
|
||||
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
||||
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||
|
||||
@@ -90,7 +90,9 @@ from plane.api.views import (
|
||||
IssueSubscriberViewSet,
|
||||
IssueCommentPublicViewSet,
|
||||
IssueReactionViewSet,
|
||||
IssueRelationViewSet,
|
||||
CommentReactionViewSet,
|
||||
IssueDraftViewSet,
|
||||
## End Issues
|
||||
# States
|
||||
StateViewSet,
|
||||
@@ -1010,6 +1012,47 @@ urlpatterns = [
|
||||
name="project-issue-archive",
|
||||
),
|
||||
## End Issue Archives
|
||||
## Issue Relation
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||
IssueRelationViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="issue-relation",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
|
||||
IssueRelationViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-relation",
|
||||
),
|
||||
## End Issue Relation
|
||||
## Issue Drafts
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
|
||||
IssueDraftViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
name="project-issue-draft",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
|
||||
IssueDraftViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-draft",
|
||||
),
|
||||
## End Issue Drafts
|
||||
## File Assets
|
||||
path(
|
||||
"workspaces/<str:slug>/file-assets/",
|
||||
|
||||
@@ -86,8 +86,10 @@ from .issue import (
|
||||
IssueReactionPublicViewSet,
|
||||
CommentReactionPublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
IssueRelationViewSet,
|
||||
IssueRetrievePublicEndpoint,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
IssueDraftViewSet,
|
||||
)
|
||||
|
||||
from .auth_extended import (
|
||||
@@ -167,6 +169,4 @@ from .analytic import (
|
||||
|
||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
||||
|
||||
from .exporter import (
|
||||
ExportIssuesEndpoint,
|
||||
)
|
||||
from .exporter import ExportIssuesEndpoint
|
||||
@@ -517,6 +517,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
try:
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||
@@ -555,9 +556,15 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
|
||||
issues_data = IssueStateSerializer(issues, many=True).data
|
||||
|
||||
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_data, group_by),
|
||||
group_results(issues_data, group_by, sub_group_by),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.db import IntegrityError
|
||||
from django.conf import settings
|
||||
from django.db import IntegrityError
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -51,6 +52,7 @@ from plane.api.serializers import (
|
||||
IssueReactionSerializer,
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssueRelationSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
from plane.api.permissions import (
|
||||
@@ -76,6 +78,7 @@ from plane.db.models import (
|
||||
CommentReaction,
|
||||
ProjectDeployBoard,
|
||||
IssueVote,
|
||||
IssueRelation,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
@@ -178,7 +181,7 @@ class IssueViewSet(BaseViewSet):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
@@ -266,9 +269,16 @@ class IssueViewSet(BaseViewSet):
|
||||
|
||||
## 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), status=status.HTTP_200_OK
|
||||
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
@@ -331,7 +341,7 @@ class UserWorkSpaceIssues(BaseAPIView):
|
||||
try:
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
@@ -443,9 +453,16 @@ class UserWorkSpaceIssues(BaseAPIView):
|
||||
|
||||
## 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), status=status.HTTP_200_OK
|
||||
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
@@ -1068,7 +1085,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
@@ -2040,6 +2057,98 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
|
||||
class IssueRelationViewSet(BaseViewSet):
|
||||
serializer_class = IssueRelationSerializer
|
||||
model = IssueRelation
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
current_instance = (
|
||||
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||
)
|
||||
if current_instance is not None:
|
||||
issue_activity.delay(
|
||||
type="issue_relation.activity.deleted",
|
||||
requested_data=json.dumps({"related_list": None}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
IssueRelationSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
try:
|
||||
related_list = request.data.get("related_list", [])
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
issueRelation = IssueRelation.objects.bulk_create(
|
||||
[
|
||||
IssueRelation(
|
||||
issue_id=related_issue["issue"],
|
||||
related_issue_id=related_issue["related_issue"],
|
||||
relation_type=related_issue["relation_type"],
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
for related_issue in related_list
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
issue_activity.delay(
|
||||
type="issue_relation.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
)
|
||||
|
||||
return Response(
|
||||
IssueRelationSerializer(issueRelation, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The issue is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
else:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("issue")
|
||||
.distinct()
|
||||
)
|
||||
class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
@@ -2078,7 +2187,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
@@ -2240,3 +2349,157 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class IssueDraftViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
serializer_class = IssueFlatSerializer
|
||||
model = Issue
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Issue.objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(is_draft=True)
|
||||
.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, project_id):
|
||||
try:
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order
|
||||
if order_by_param == "priority"
|
||||
else priority_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
*[
|
||||
When(priority=p, then=Value(i))
|
||||
for i, p in enumerate(priority_order)
|
||||
],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
||||
# State Ordering
|
||||
elif order_by_param in [
|
||||
"state__name",
|
||||
"state__group",
|
||||
"-state__name",
|
||||
"-state__group",
|
||||
]:
|
||||
state_order = (
|
||||
state_order
|
||||
if order_by_param in ["state__name", "state__group"]
|
||||
else state_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[
|
||||
When(state__group=state_group, then=Value(i))
|
||||
for i, state_group in enumerate(state_order)
|
||||
],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("state_order")
|
||||
# assignee and label ordering
|
||||
elif order_by_param in [
|
||||
"labels__name",
|
||||
"-labels__name",
|
||||
"assignees__first_name",
|
||||
"-assignees__first_name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
max_values=Max(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
||||
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
if group_by:
|
||||
return Response(
|
||||
group_results(issues, 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,
|
||||
)
|
||||
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
try:
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True
|
||||
)
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||
except Issue.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
@@ -308,6 +308,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
try:
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||
@@ -346,9 +347,15 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
|
||||
issues_data = IssueStateSerializer(issues, many=True).data
|
||||
|
||||
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_data, group_by),
|
||||
group_results(issues_data, group_by, sub_group_by),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
query = request.query_params.get("search", False)
|
||||
workspace_search = request.query_params.get("workspace_search", "false")
|
||||
parent = request.query_params.get("parent", "false")
|
||||
blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false")
|
||||
issue_relation = request.query_params.get("issue_relation", "false")
|
||||
cycle = request.query_params.get("cycle", "false")
|
||||
module = request.query_params.get("module", "false")
|
||||
sub_issue = request.query_params.get("sub_issue", "false")
|
||||
@@ -247,12 +247,12 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
"parent_id", flat=True
|
||||
)
|
||||
)
|
||||
if blocker_blocked_by == "true" and issue_id:
|
||||
if issue_relation == "true" and issue_id:
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id),
|
||||
~Q(blocked_issues__block=issue),
|
||||
~Q(blocker_issues__blocked_by=issue),
|
||||
~Q(issue_related__issue=issue),
|
||||
~Q(issue_relation__related_issue=issue),
|
||||
)
|
||||
if sub_issue == "true" and issue_id:
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
|
||||
@@ -1072,7 +1072,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
.order_by("state_group")
|
||||
)
|
||||
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
|
||||
priority_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
|
||||
@@ -393,130 +393,6 @@ def track_assignees(
|
||||
)
|
||||
|
||||
|
||||
# Track changes in blocking issues
|
||||
def track_blocks(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if len(requested_data.get("blocks_list")) > len(
|
||||
current_instance.get("blocked_issues")
|
||||
):
|
||||
for block in requested_data.get("blocks_list"):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
blocked
|
||||
for blocked in current_instance.get("blocked_issues")
|
||||
if blocked.get("block") == block
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
issue = Issue.objects.get(pk=block)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"added blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Blocked Issue Removal
|
||||
if len(requested_data.get("blocks_list")) < len(
|
||||
current_instance.get("blocked_issues")
|
||||
):
|
||||
for blocked in current_instance.get("blocked_issues"):
|
||||
if blocked.get("block") not in requested_data.get("blocks_list"):
|
||||
issue = Issue.objects.get(pk=blocked.get("block"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"removed blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in blocked_by issues
|
||||
def track_blockings(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if len(requested_data.get("blockers_list")) > len(
|
||||
current_instance.get("blocker_issues")
|
||||
):
|
||||
for block in requested_data.get("blockers_list"):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
blocked
|
||||
for blocked in current_instance.get("blocker_issues")
|
||||
if blocked.get("blocked_by") == block
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
issue = Issue.objects.get(pk=block)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"added blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Blocked Issue Removal
|
||||
if len(requested_data.get("blockers_list")) < len(
|
||||
current_instance.get("blocker_issues")
|
||||
):
|
||||
for blocked in current_instance.get("blocker_issues"):
|
||||
if blocked.get("blocked_by") not in requested_data.get("blockers_list"):
|
||||
issue = Issue.objects.get(pk=blocked.get("blocked_by"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"removed blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
@@ -637,8 +513,6 @@ def update_issue_activity(
|
||||
"start_date": track_start_date,
|
||||
"labels_list": track_labels,
|
||||
"assignees_list": track_assignees,
|
||||
"blocks_list": track_blocks,
|
||||
"blockers_list": track_blockings,
|
||||
"estimate_point": track_estimate_points,
|
||||
"archived_at": track_archive_at,
|
||||
"closed_to": track_closed_to,
|
||||
@@ -1170,6 +1044,57 @@ def delete_issue_vote_activity(
|
||||
)
|
||||
|
||||
|
||||
def create_issue_relation_activity(
|
||||
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 = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance is None and requested_data.get("related_list") is not None:
|
||||
for issue_relation in requested_data.get("related_list"):
|
||||
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_relation.get("issue"),
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field=f'{issue_relation.get("relation_type")}',
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f'added {issue_relation.get("relation_type")} relation',
|
||||
old_identifier=issue_relation.get("issue"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_relation_activity(
|
||||
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 = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance is not None and requested_data.get("related_list") is None:
|
||||
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=current_instance.get("issue"),
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field=f'{current_instance.get("relation_type")}',
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f'deleted the {current_instance.get("relation_type")} relation',
|
||||
old_identifier=current_instance.get("issue"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Receive message from room group
|
||||
@shared_task
|
||||
def issue_activity(
|
||||
@@ -1233,6 +1158,8 @@ def issue_activity(
|
||||
"link.activity.deleted": delete_link_activity,
|
||||
"attachment.activity.created": create_attachment_activity,
|
||||
"attachment.activity.deleted": delete_attachment_activity,
|
||||
"issue_relation.activity.created": create_issue_relation_activity,
|
||||
"issue_relation.activity.deleted": delete_issue_relation_activity,
|
||||
"issue_reaction.activity.created": create_issue_reaction_activity,
|
||||
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
|
||||
"comment_reaction.activity.created": create_comment_reaction_activity,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-12 07:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from plane.db.models import IssueRelation
|
||||
from sentry_sdk import capture_exception
|
||||
import uuid
|
||||
|
||||
|
||||
def create_issue_relation(apps, schema_editor):
|
||||
try:
|
||||
IssueBlockerModel = apps.get_model("db", "IssueBlocker")
|
||||
updated_issue_relation = []
|
||||
for blocked_issue in IssueBlockerModel.objects.all():
|
||||
updated_issue_relation.append(
|
||||
IssueRelation(
|
||||
issue_id=blocked_issue.block_id,
|
||||
related_issue_id=blocked_issue.blocked_by_id,
|
||||
relation_type="blocked_by",
|
||||
project_id=blocked_issue.project_id,
|
||||
workspace_id=blocked_issue.workspace_id,
|
||||
created_by_id=blocked_issue.created_by_id,
|
||||
updated_by_id=blocked_issue.updated_by_id,
|
||||
)
|
||||
)
|
||||
IssueRelation.objects.bulk_create(updated_issue_relation, batch_size=100)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
|
||||
|
||||
def update_issue_priority_choice(apps, schema_editor):
|
||||
IssueModel = apps.get_model("db", "Issue")
|
||||
updated_issues = []
|
||||
for obj in IssueModel.objects.all():
|
||||
if obj.priority is None:
|
||||
obj.priority = "none"
|
||||
updated_issues.append(obj)
|
||||
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0042_alter_analyticview_created_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IssueRelation',
|
||||
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)),
|
||||
('relation_type', models.CharField(choices=[('duplicate', 'Duplicate'), ('relates_to', 'Relates To'), ('blocked_by', 'Blocked By')], default='blocked_by', max_length=20, verbose_name='Issue Relation Type')),
|
||||
('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')),
|
||||
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_relation', to='db.issue')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||
('related_issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_related', to='db.issue')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Relation',
|
||||
'verbose_name_plural': 'Issue Relations',
|
||||
'db_table': 'issue_relations',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('issue', 'related_issue')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='is_draft',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issue',
|
||||
name='priority',
|
||||
field=models.CharField(choices=[('urgent', 'Urgent'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low'), ('none', 'None')], default='none', max_length=30, verbose_name='Issue Priority'),
|
||||
),
|
||||
migrations.RunPython(create_issue_relation),
|
||||
migrations.RunPython(update_issue_priority_choice),
|
||||
]
|
||||
138
apiserver/plane/db/migrations/0044_auto_20230913_0709.py
Normal file
138
apiserver/plane/db/migrations/0044_auto_20230913_0709.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-13 07:09
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def workspace_member_props(old_props):
|
||||
new_props = {
|
||||
"filters": {
|
||||
"priority": old_props.get("filters", {}).get("priority", None),
|
||||
"state": old_props.get("filters", {}).get("state", None),
|
||||
"state_group": old_props.get("filters", {}).get("state_group", None),
|
||||
"assignees": old_props.get("filters", {}).get("assignees", None),
|
||||
"created_by": old_props.get("filters", {}).get("created_by", None),
|
||||
"labels": old_props.get("filters", {}).get("labels", None),
|
||||
"start_date": old_props.get("filters", {}).get("start_date", None),
|
||||
"target_date": old_props.get("filters", {}).get("target_date", None),
|
||||
"subscriber": old_props.get("filters", {}).get("subscriber", None),
|
||||
},
|
||||
"display_filters": {
|
||||
"group_by": old_props.get("groupByProperty", None),
|
||||
"order_by": old_props.get("orderBy", "-created_at"),
|
||||
"type": old_props.get("filters", {}).get("type", None),
|
||||
"sub_issue": old_props.get("showSubIssues", True),
|
||||
"show_empty_groups": old_props.get("showEmptyGroups", True),
|
||||
"layout": old_props.get("issueView", "list"),
|
||||
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||
},
|
||||
"display_properties": {
|
||||
"assignee": old_props.get("properties", {}).get("assignee",None),
|
||||
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
|
||||
"created_on": old_props.get("properties", {}).get("created_on", None),
|
||||
"due_date": old_props.get("properties", {}).get("due_date", None),
|
||||
"estimate": old_props.get("properties", {}).get("estimate", None),
|
||||
"key": old_props.get("properties", {}).get("key", None),
|
||||
"labels": old_props.get("properties", {}).get("labels", None),
|
||||
"link": old_props.get("properties", {}).get("link", None),
|
||||
"priority": old_props.get("properties", {}).get("priority", None),
|
||||
"start_date": old_props.get("properties", {}).get("start_date", None),
|
||||
"state": old_props.get("properties", {}).get("state", None),
|
||||
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
|
||||
"updated_on": old_props.get("properties", {}).get("updated_on", None),
|
||||
},
|
||||
}
|
||||
return new_props
|
||||
|
||||
|
||||
def project_member_props(old_props):
|
||||
new_props = {
|
||||
"filters": {
|
||||
"priority": old_props.get("filters", {}).get("priority", None),
|
||||
"state": old_props.get("filters", {}).get("state", None),
|
||||
"state_group": old_props.get("filters", {}).get("state_group", None),
|
||||
"assignees": old_props.get("filters", {}).get("assignees", None),
|
||||
"created_by": old_props.get("filters", {}).get("created_by", None),
|
||||
"labels": old_props.get("filters", {}).get("labels", None),
|
||||
"start_date": old_props.get("filters", {}).get("start_date", None),
|
||||
"target_date": old_props.get("filters", {}).get("target_date", None),
|
||||
"subscriber": old_props.get("filters", {}).get("subscriber", None),
|
||||
},
|
||||
"display_filters": {
|
||||
"group_by": old_props.get("groupByProperty", None),
|
||||
"order_by": old_props.get("orderBy", "-created_at"),
|
||||
"type": old_props.get("filters", {}).get("type", None),
|
||||
"sub_issue": old_props.get("showSubIssues", True),
|
||||
"show_empty_groups": old_props.get("showEmptyGroups", True),
|
||||
"layout": old_props.get("issueView", "list"),
|
||||
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||
},
|
||||
}
|
||||
return new_props
|
||||
|
||||
|
||||
def cycle_module_props(old_props):
|
||||
new_props = {
|
||||
"filters": {
|
||||
"priority": old_props.get("filters", {}).get("priority", None),
|
||||
"state": old_props.get("filters", {}).get("state", None),
|
||||
"state_group": old_props.get("filters", {}).get("state_group", None),
|
||||
"assignees": old_props.get("filters", {}).get("assignees", None),
|
||||
"created_by": old_props.get("filters", {}).get("created_by", None),
|
||||
"labels": old_props.get("filters", {}).get("labels", None),
|
||||
"start_date": old_props.get("filters", {}).get("start_date", None),
|
||||
"target_date": old_props.get("filters", {}).get("target_date", None),
|
||||
"subscriber": old_props.get("filters", {}).get("subscriber", None),
|
||||
},
|
||||
}
|
||||
return new_props
|
||||
|
||||
|
||||
def update_workspace_member_view_props(apps, schema_editor):
|
||||
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
|
||||
updated_workspace_member = []
|
||||
for obj in WorkspaceMemberModel.objects.all():
|
||||
obj.view_props = workspace_member_props(obj.view_props)
|
||||
obj.default_props = workspace_member_props(obj.default_props)
|
||||
updated_workspace_member.append(obj)
|
||||
WorkspaceMemberModel.objects.bulk_update(updated_workspace_member, ["view_props", "default_props"], batch_size=100)
|
||||
|
||||
def update_project_member_view_props(apps, schema_editor):
|
||||
ProjectMemberModel = apps.get_model("db", "ProjectMember")
|
||||
updated_project_member = []
|
||||
for obj in ProjectMemberModel.objects.all():
|
||||
obj.view_props = project_member_props(obj.view_props)
|
||||
obj.default_props = project_member_props(obj.default_props)
|
||||
updated_project_member.append(obj)
|
||||
ProjectMemberModel.objects.bulk_update(updated_project_member, ["view_props", "default_props"], batch_size=100)
|
||||
|
||||
def update_cycle_props(apps, schema_editor):
|
||||
CycleModel = apps.get_model("db", "Cycle")
|
||||
updated_cycle = []
|
||||
for obj in CycleModel.objects.all():
|
||||
if "filter" in obj.view_props:
|
||||
obj.view_props = cycle_module_props(obj.view_props)
|
||||
updated_cycle.append(obj)
|
||||
CycleModel.objects.bulk_update(updated_cycle, ["view_props"], batch_size=100)
|
||||
|
||||
def update_module_props(apps, schema_editor):
|
||||
ModuleModel = apps.get_model("db", "Module")
|
||||
updated_module = []
|
||||
for obj in ModuleModel.objects.all():
|
||||
if "filter" in obj.view_props:
|
||||
obj.view_props = cycle_module_props(obj.view_props)
|
||||
updated_module.append(obj)
|
||||
ModuleModel.objects.bulk_update(updated_module, ["view_props"], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0043_alter_analyticview_created_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_workspace_member_view_props),
|
||||
migrations.RunPython(update_project_member_view_props),
|
||||
migrations.RunPython(update_cycle_props),
|
||||
migrations.RunPython(update_module_props),
|
||||
]
|
||||
@@ -32,6 +32,7 @@ from .issue import (
|
||||
IssueAssignee,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
IssueRelation,
|
||||
IssueLink,
|
||||
IssueSequence,
|
||||
IssueAttachment,
|
||||
|
||||
@@ -29,6 +29,7 @@ class IssueManager(models.Manager):
|
||||
| models.Q(issue_inbox__isnull=True)
|
||||
)
|
||||
.exclude(archived_at__isnull=False)
|
||||
.exclude(is_draft=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -38,6 +39,7 @@ class Issue(ProjectBaseModel):
|
||||
("high", "High"),
|
||||
("medium", "Medium"),
|
||||
("low", "Low"),
|
||||
("none", "None")
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
@@ -64,8 +66,7 @@ class Issue(ProjectBaseModel):
|
||||
max_length=30,
|
||||
choices=PRIORITY_CHOICES,
|
||||
verbose_name="Issue Priority",
|
||||
null=True,
|
||||
blank=True,
|
||||
default="none",
|
||||
)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
target_date = models.DateField(null=True, blank=True)
|
||||
@@ -83,6 +84,7 @@ class Issue(ProjectBaseModel):
|
||||
sort_order = models.FloatField(default=65535)
|
||||
completed_at = models.DateTimeField(null=True)
|
||||
archived_at = models.DateField(null=True)
|
||||
is_draft = models.BooleanField(default=False)
|
||||
|
||||
objects = models.Manager()
|
||||
issue_objects = IssueManager()
|
||||
@@ -178,6 +180,37 @@ class IssueBlocker(ProjectBaseModel):
|
||||
return f"{self.block.name} {self.blocked_by.name}"
|
||||
|
||||
|
||||
class IssueRelation(ProjectBaseModel):
|
||||
RELATION_CHOICES = (
|
||||
("duplicate", "Duplicate"),
|
||||
("relates_to", "Relates To"),
|
||||
("blocked_by", "Blocked By"),
|
||||
)
|
||||
|
||||
issue = models.ForeignKey(
|
||||
Issue, related_name="issue_relation", on_delete=models.CASCADE
|
||||
)
|
||||
related_issue = models.ForeignKey(
|
||||
Issue, related_name="issue_related", on_delete=models.CASCADE
|
||||
)
|
||||
relation_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=RELATION_CHOICES,
|
||||
verbose_name="Issue Relation Type",
|
||||
default="blocked_by",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "related_issue"]
|
||||
verbose_name = "Issue Relation"
|
||||
verbose_name_plural = "Issue Relations"
|
||||
db_table = "issue_relations"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.issue.name} {self.related_issue.name}"
|
||||
|
||||
|
||||
class IssueAssignee(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
|
||||
|
||||
@@ -15,7 +15,7 @@ def resolve_keys(group_keys, value):
|
||||
return value
|
||||
|
||||
|
||||
def group_results(results_data, group_by):
|
||||
def group_results(results_data, group_by, sub_group_by=False):
|
||||
"""group results data into certain group_by
|
||||
|
||||
Args:
|
||||
@@ -25,38 +25,140 @@ def group_results(results_data, group_by):
|
||||
Returns:
|
||||
obj: grouped results
|
||||
"""
|
||||
response_dict = dict()
|
||||
if sub_group_by:
|
||||
main_responsive_dict = dict()
|
||||
|
||||
if group_by == "priority":
|
||||
response_dict = {
|
||||
"urgent": [],
|
||||
"high": [],
|
||||
"medium": [],
|
||||
"low": [],
|
||||
"None": [],
|
||||
}
|
||||
if sub_group_by == "priority":
|
||||
main_responsive_dict = {
|
||||
"urgent": {},
|
||||
"high": {},
|
||||
"medium": {},
|
||||
"low": {},
|
||||
"none": {},
|
||||
}
|
||||
|
||||
for value in results_data:
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
if isinstance(group_attribute, list):
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in response_dict:
|
||||
response_dict[str(attrib)].append(value)
|
||||
else:
|
||||
response_dict[str(attrib)] = []
|
||||
response_dict[str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in response_dict:
|
||||
response_dict[str(None)].append(value)
|
||||
for value in results_data:
|
||||
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list):
|
||||
if len(main_group_attribute):
|
||||
for attrib in main_group_attribute:
|
||||
if str(attrib) not in main_responsive_dict:
|
||||
main_responsive_dict[str(attrib)] = {}
|
||||
if str(group_attribute) in main_responsive_dict[str(attrib)]:
|
||||
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(attrib)][str(group_attribute)] = []
|
||||
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||
else:
|
||||
response_dict[str(None)] = []
|
||||
response_dict[str(None)].append(value)
|
||||
else:
|
||||
if str(group_attribute) in response_dict:
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
else:
|
||||
response_dict[str(group_attribute)] = []
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
if str(None) not in main_responsive_dict:
|
||||
main_responsive_dict[str(None)] = {}
|
||||
|
||||
return response_dict
|
||||
if str(group_attribute) in main_responsive_dict[str(None)]:
|
||||
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(None)][str(group_attribute)] = []
|
||||
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||
|
||||
elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list):
|
||||
if str(main_group_attribute) not in main_responsive_dict:
|
||||
main_responsive_dict[str(main_group_attribute)] = {}
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in main_responsive_dict[str(main_group_attribute)]:
|
||||
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_group_attribute)][str(attrib)] = []
|
||||
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in main_responsive_dict[str(main_group_attribute)]:
|
||||
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_group_attribute)][str(None)] = []
|
||||
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
|
||||
|
||||
elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list):
|
||||
if len(main_group_attribute):
|
||||
for main_attrib in main_group_attribute:
|
||||
if str(main_attrib) not in main_responsive_dict:
|
||||
main_responsive_dict[str(main_attrib)] = {}
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in main_responsive_dict[str(main_attrib)]:
|
||||
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_attrib)][str(attrib)] = []
|
||||
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in main_responsive_dict[str(main_attrib)]:
|
||||
main_responsive_dict[str(main_attrib)][str(None)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_attrib)][str(None)] = []
|
||||
main_responsive_dict[str(main_attrib)][str(None)].append(value)
|
||||
else:
|
||||
if str(None) not in main_responsive_dict:
|
||||
main_responsive_dict[str(None)] = {}
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in main_responsive_dict[str(None)]:
|
||||
main_responsive_dict[str(None)][str(attrib)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(None)][str(attrib)] = []
|
||||
main_responsive_dict[str(None)][str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in main_responsive_dict[str(None)]:
|
||||
main_responsive_dict[str(None)][str(None)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(None)][str(None)] = []
|
||||
main_responsive_dict[str(None)][str(None)].append(value)
|
||||
else:
|
||||
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
|
||||
if str(main_group_attribute) not in main_responsive_dict:
|
||||
main_responsive_dict[str(main_group_attribute)] = {}
|
||||
|
||||
if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]:
|
||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
|
||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
||||
|
||||
return main_responsive_dict
|
||||
|
||||
else:
|
||||
response_dict = dict()
|
||||
|
||||
if group_by == "priority":
|
||||
response_dict = {
|
||||
"urgent": [],
|
||||
"high": [],
|
||||
"medium": [],
|
||||
"low": [],
|
||||
"none": [],
|
||||
}
|
||||
|
||||
for value in results_data:
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
if isinstance(group_attribute, list):
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in response_dict:
|
||||
response_dict[str(attrib)].append(value)
|
||||
else:
|
||||
response_dict[str(attrib)] = []
|
||||
response_dict[str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in response_dict:
|
||||
response_dict[str(None)].append(value)
|
||||
else:
|
||||
response_dict[str(None)] = []
|
||||
response_dict[str(None)].append(value)
|
||||
else:
|
||||
if str(group_attribute) in response_dict:
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
else:
|
||||
response_dict[str(group_attribute)] = []
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
|
||||
return response_dict
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
|
||||
def filter_state(params, filter, method):
|
||||
if method == "GET":
|
||||
states = params.get("state").split(",")
|
||||
@@ -23,7 +24,6 @@ def filter_state_group(params, filter, method):
|
||||
return filter
|
||||
|
||||
|
||||
|
||||
def filter_estimate_point(params, filter, method):
|
||||
if method == "GET":
|
||||
estimate_points = params.get("estimate_point").split(",")
|
||||
@@ -39,25 +39,10 @@ def filter_priority(params, filter, method):
|
||||
if method == "GET":
|
||||
priorities = params.get("priority").split(",")
|
||||
if len(priorities) and "" not in priorities:
|
||||
if len(priorities) == 1 and "null" in priorities:
|
||||
filter["priority__isnull"] = True
|
||||
elif len(priorities) > 1 and "null" in priorities:
|
||||
filter["priority__isnull"] = True
|
||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
||||
else:
|
||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
||||
|
||||
filter["priority__in"] = priorities
|
||||
else:
|
||||
if params.get("priority", None) and len(params.get("priority")):
|
||||
priorities = params.get("priority")
|
||||
if len(priorities) == 1 and "null" in priorities:
|
||||
filter["priority__isnull"] = True
|
||||
elif len(priorities) > 1 and "null" in priorities:
|
||||
filter["priority__isnull"] = True
|
||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
||||
else:
|
||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
||||
|
||||
filter["priority__in"] = params.get("priority")
|
||||
return filter
|
||||
|
||||
|
||||
@@ -229,7 +214,6 @@ def filter_issue_state_type(params, filter, method):
|
||||
return filter
|
||||
|
||||
|
||||
|
||||
def filter_project(params, filter, method):
|
||||
if method == "GET":
|
||||
projects = params.get("project").split(",")
|
||||
@@ -329,7 +313,7 @@ def issue_filters(query_params, method):
|
||||
"module": filter_module,
|
||||
"inbox_status": filter_inbox_status,
|
||||
"sub_issue": filter_sub_issue_toggle,
|
||||
"subscriber": filter_subscribed_issues,
|
||||
"subscriber": filter_subscribed_issues,
|
||||
"start_target_date": filter_start_target_date_issues,
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
|
||||
plane-worker:
|
||||
container_name: planebgworker
|
||||
image: makeplane/plane-worker:latest
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/worker
|
||||
env_file:
|
||||
@@ -99,7 +99,7 @@ services:
|
||||
|
||||
plane-beat-worker:
|
||||
container_name: planebeatworker
|
||||
image: makeplane/plane-worker:latest
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/beat
|
||||
env_file:
|
||||
|
||||
@@ -90,8 +90,6 @@ services:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: ./bin/takeoff
|
||||
ports:
|
||||
- 8000:8000
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"start": "turbo run start",
|
||||
@@ -17,8 +16,12 @@
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint-config-custom": "*",
|
||||
"postcss": "^8.4.29",
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"turbo": "latest"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
|
||||
@@ -16,5 +16,7 @@ module.exports = {
|
||||
"no-duplicate-imports": "error",
|
||||
"arrow-body-style": ["error", "as-needed"],
|
||||
"react/self-closing-comp": ["error", { component: true, html: true }],
|
||||
"@next/next/no-img-element": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn"],
|
||||
},
|
||||
};
|
||||
|
||||
10
packages/tailwind-config-custom/package.json
Normal file
10
packages/tailwind-config-custom/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.0.1",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
}
|
||||
}
|
||||
7
packages/tailwind-config-custom/postcss.config.js
Normal file
7
packages/tailwind-config-custom/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
212
packages/tailwind-config-custom/tailwind.config.js
Normal file
212
packages/tailwind-config-custom/tailwind.config.js
Normal file
@@ -0,0 +1,212 @@
|
||||
const convertToRGB = (variableName) => `rgba(var(${variableName}))`;
|
||||
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: [
|
||||
"./components/**/*.tsx",
|
||||
"./constants/**/*.{js,ts,jsx,tsx}",
|
||||
"./layouts/**/*.tsx",
|
||||
"./pages/**/*.tsx",
|
||||
"./ui/**/*.tsx",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
"custom-shadow-2xs": "var(--color-shadow-2xs)",
|
||||
"custom-shadow-xs": "var(--color-shadow-xs)",
|
||||
"custom-shadow-sm": "var(--color-shadow-sm)",
|
||||
"custom-shadow-rg": "var(--color-shadow-rg)",
|
||||
"custom-shadow-md": "var(--color-shadow-md)",
|
||||
"custom-shadow-lg": "var(--color-shadow-lg)",
|
||||
"custom-shadow-xl": "var(--color-shadow-xl)",
|
||||
"custom-shadow-2xl": "var(--color-shadow-2xl)",
|
||||
"custom-shadow-3xl": "var(--color-shadow-3xl)",
|
||||
"custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)",
|
||||
"custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)",
|
||||
"custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)",
|
||||
"custom-sidebar-shadow-rg": "var(--color-sidebar-shadow-rg)",
|
||||
"custom-sidebar-shadow-md": "var(--color-sidebar-shadow-md)",
|
||||
"custom-sidebar-shadow-lg": "var(--color-sidebar-shadow-lg)",
|
||||
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
|
||||
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
|
||||
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
|
||||
},
|
||||
colors: {
|
||||
custom: {
|
||||
primary: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-primary-10"),
|
||||
20: convertToRGB("--color-primary-20"),
|
||||
30: convertToRGB("--color-primary-30"),
|
||||
40: convertToRGB("--color-primary-40"),
|
||||
50: convertToRGB("--color-primary-50"),
|
||||
60: convertToRGB("--color-primary-60"),
|
||||
70: convertToRGB("--color-primary-70"),
|
||||
80: convertToRGB("--color-primary-80"),
|
||||
90: convertToRGB("--color-primary-90"),
|
||||
100: convertToRGB("--color-primary-100"),
|
||||
200: convertToRGB("--color-primary-200"),
|
||||
300: convertToRGB("--color-primary-300"),
|
||||
400: convertToRGB("--color-primary-400"),
|
||||
500: convertToRGB("--color-primary-500"),
|
||||
600: convertToRGB("--color-primary-600"),
|
||||
700: convertToRGB("--color-primary-700"),
|
||||
800: convertToRGB("--color-primary-800"),
|
||||
900: convertToRGB("--color-primary-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-primary-100"),
|
||||
},
|
||||
background: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-background-10"),
|
||||
20: convertToRGB("--color-background-20"),
|
||||
30: convertToRGB("--color-background-30"),
|
||||
40: convertToRGB("--color-background-40"),
|
||||
50: convertToRGB("--color-background-50"),
|
||||
60: convertToRGB("--color-background-60"),
|
||||
70: convertToRGB("--color-background-70"),
|
||||
80: convertToRGB("--color-background-80"),
|
||||
90: convertToRGB("--color-background-90"),
|
||||
100: convertToRGB("--color-background-100"),
|
||||
200: convertToRGB("--color-background-200"),
|
||||
300: convertToRGB("--color-background-300"),
|
||||
400: convertToRGB("--color-background-400"),
|
||||
500: convertToRGB("--color-background-500"),
|
||||
600: convertToRGB("--color-background-600"),
|
||||
700: convertToRGB("--color-background-700"),
|
||||
800: convertToRGB("--color-background-800"),
|
||||
900: convertToRGB("--color-background-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-background-100"),
|
||||
},
|
||||
text: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-text-10"),
|
||||
20: convertToRGB("--color-text-20"),
|
||||
30: convertToRGB("--color-text-30"),
|
||||
40: convertToRGB("--color-text-40"),
|
||||
50: convertToRGB("--color-text-50"),
|
||||
60: convertToRGB("--color-text-60"),
|
||||
70: convertToRGB("--color-text-70"),
|
||||
80: convertToRGB("--color-text-80"),
|
||||
90: convertToRGB("--color-text-90"),
|
||||
100: convertToRGB("--color-text-100"),
|
||||
200: convertToRGB("--color-text-200"),
|
||||
300: convertToRGB("--color-text-300"),
|
||||
400: convertToRGB("--color-text-400"),
|
||||
500: convertToRGB("--color-text-500"),
|
||||
600: convertToRGB("--color-text-600"),
|
||||
700: convertToRGB("--color-text-700"),
|
||||
800: convertToRGB("--color-text-800"),
|
||||
900: convertToRGB("--color-text-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-text-100"),
|
||||
},
|
||||
border: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
100: convertToRGB("--color-border-100"),
|
||||
200: convertToRGB("--color-border-200"),
|
||||
300: convertToRGB("--color-border-300"),
|
||||
400: convertToRGB("--color-border-400"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-border-200"),
|
||||
},
|
||||
sidebar: {
|
||||
background: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-sidebar-background-10"),
|
||||
20: convertToRGB("--color-sidebar-background-20"),
|
||||
30: convertToRGB("--color-sidebar-background-30"),
|
||||
40: convertToRGB("--color-sidebar-background-40"),
|
||||
50: convertToRGB("--color-sidebar-background-50"),
|
||||
60: convertToRGB("--color-sidebar-background-60"),
|
||||
70: convertToRGB("--color-sidebar-background-70"),
|
||||
80: convertToRGB("--color-sidebar-background-80"),
|
||||
90: convertToRGB("--color-sidebar-background-90"),
|
||||
100: convertToRGB("--color-sidebar-background-100"),
|
||||
200: convertToRGB("--color-sidebar-background-200"),
|
||||
300: convertToRGB("--color-sidebar-background-300"),
|
||||
400: convertToRGB("--color-sidebar-background-400"),
|
||||
500: convertToRGB("--color-sidebar-background-500"),
|
||||
600: convertToRGB("--color-sidebar-background-600"),
|
||||
700: convertToRGB("--color-sidebar-background-700"),
|
||||
800: convertToRGB("--color-sidebar-background-800"),
|
||||
900: convertToRGB("--color-sidebar-background-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-background-100"),
|
||||
},
|
||||
text: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-sidebar-text-10"),
|
||||
20: convertToRGB("--color-sidebar-text-20"),
|
||||
30: convertToRGB("--color-sidebar-text-30"),
|
||||
40: convertToRGB("--color-sidebar-text-40"),
|
||||
50: convertToRGB("--color-sidebar-text-50"),
|
||||
60: convertToRGB("--color-sidebar-text-60"),
|
||||
70: convertToRGB("--color-sidebar-text-70"),
|
||||
80: convertToRGB("--color-sidebar-text-80"),
|
||||
90: convertToRGB("--color-sidebar-text-90"),
|
||||
100: convertToRGB("--color-sidebar-text-100"),
|
||||
200: convertToRGB("--color-sidebar-text-200"),
|
||||
300: convertToRGB("--color-sidebar-text-300"),
|
||||
400: convertToRGB("--color-sidebar-text-400"),
|
||||
500: convertToRGB("--color-sidebar-text-500"),
|
||||
600: convertToRGB("--color-sidebar-text-600"),
|
||||
700: convertToRGB("--color-sidebar-text-700"),
|
||||
800: convertToRGB("--color-sidebar-text-800"),
|
||||
900: convertToRGB("--color-sidebar-text-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-text-100"),
|
||||
},
|
||||
border: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
100: convertToRGB("--color-sidebar-border-100"),
|
||||
200: convertToRGB("--color-sidebar-border-200"),
|
||||
300: convertToRGB("--color-sidebar-border-300"),
|
||||
400: convertToRGB("--color-sidebar-border-400"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-border-200"),
|
||||
},
|
||||
},
|
||||
backdrop: "#131313",
|
||||
},
|
||||
},
|
||||
keyframes: {
|
||||
leftToaster: {
|
||||
"0%": { left: "-20rem" },
|
||||
"100%": { left: "0" },
|
||||
},
|
||||
rightToaster: {
|
||||
"0%": { right: "-20rem" },
|
||||
"100%": { right: "0" },
|
||||
},
|
||||
},
|
||||
typography: ({ theme }) => ({
|
||||
brand: {
|
||||
css: {
|
||||
"--tw-prose-body": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-p": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-headings": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-lead": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-links": convertToRGB("--color-primary-100"),
|
||||
"--tw-prose-bold": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-counters": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-bullets": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-hr": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-quotes": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-quote-borders": convertToRGB("--color-border"),
|
||||
"--tw-prose-code": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-pre-code": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-pre-bg": convertToRGB("--color-background-100"),
|
||||
"--tw-prose-th-borders": convertToRGB("--color-border"),
|
||||
"--tw-prose-td-borders": convertToRGB("--color-border"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
fontFamily: {
|
||||
custom: ["Inter", "sans-serif"],
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
||||
};
|
||||
@@ -17,6 +17,7 @@
|
||||
"next": "12.3.2",
|
||||
"react": "^18.2.0",
|
||||
"tsconfig": "*",
|
||||
"tailwind-config-custom": "*",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/ui/postcss.config.js
Normal file
1
packages/ui/postcss.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require("tailwind-config-custom/postcss.config");
|
||||
1
packages/ui/tailwind.config.js
Normal file
1
packages/ui/tailwind.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require("tailwind-config-custom/tailwind.config");
|
||||
@@ -1,9 +1,5 @@
|
||||
{
|
||||
"extends": "../tsconfig/nextjs.json",
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM"]
|
||||
}
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ fi
|
||||
# Only perform action if $FROM and $TO are different.
|
||||
echo "Replacing all statically built instances of $FROM with this string $TO ."
|
||||
|
||||
grep -R -la "${FROM}" apps/$DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
|
||||
grep -R -la "${FROM}" $DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
rules: {
|
||||
"@next/next/no-img-element": "off",
|
||||
},
|
||||
};
|
||||
|
||||
2
space/additional.d.ts
vendored
Normal file
2
space/additional.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// additional.d.ts
|
||||
/// <reference types="next-images" />
|
||||
@@ -1,9 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
// react hook form
|
||||
import { useForm } from "react-hook-form";
|
||||
// components
|
||||
import { EmailResetPasswordForm } from "./email-reset-password-form";
|
||||
|
||||
@@ -13,7 +13,7 @@ import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
||||
// images
|
||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
|
||||
|
||||
export const SignInView = observer(() => {
|
||||
const { user: userStore } = useMobxStore();
|
||||
@@ -112,7 +112,7 @@ export const SignInView = observer(() => {
|
||||
<div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28">
|
||||
<div className="grid place-items-center bg-custom-background-100">
|
||||
<div className="h-[30px] w-[30px]">
|
||||
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
|
||||
<img src={`${imagePrefix}/plane-logos/blue-without-text.png`} alt="Plane Logo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useState } from "react";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// react-hook-form
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// lib
|
||||
@@ -30,10 +30,13 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
// states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const editorRef = React.useRef<any>(null);
|
||||
|
||||
const showEditorRef = React.useRef<any>(null);
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
} = useForm<any>({
|
||||
defaultValues: { comment_html: comment.comment_html },
|
||||
});
|
||||
@@ -47,6 +50,9 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
if (!workspaceSlug || !issueDetailStore.peekId) return;
|
||||
issueDetailStore.updateIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id, formData);
|
||||
setIsEditing(false);
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -96,6 +102,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TipTapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={value}
|
||||
debouncedUpdatesEnabled={false}
|
||||
customClassName="min-h-[50px] p-3 shadow-sm"
|
||||
@@ -125,7 +132,8 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
</form>
|
||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<TipTapEditor
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html}
|
||||
editable={false}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
|
||||
@@ -44,7 +44,6 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
||||
{mode === "full" && (
|
||||
<div className="flex justify-between gap-2 pb-3">
|
||||
<h6 className="flex items-center gap-2 font-medium">
|
||||
{/* {getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)} */}
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||
</h6>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -56,12 +56,6 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
editor.commands.setContent(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
|
||||
@@ -12,7 +12,10 @@ const nextConfig = {
|
||||
};
|
||||
|
||||
if (parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) {
|
||||
const nextConfigWithNginx = withImages({ basePath: "/spaces", ...nextConfig });
|
||||
const nextConfigWithNginx = withImages({
|
||||
basePath: "/spaces",
|
||||
...nextConfig,
|
||||
});
|
||||
module.exports = nextConfigWithNginx;
|
||||
} else {
|
||||
module.exports = nextConfig;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"@heroicons/react": "^2.0.12",
|
||||
"@mui/icons-material": "^5.14.1",
|
||||
"@mui/material": "^5.14.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tiptap-pro/extension-unique-id": "^2.1.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
||||
"@tiptap/extension-color": "^2.0.4",
|
||||
@@ -62,19 +61,17 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/node": "18.14.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-config-custom": "*",
|
||||
"eslint-config-next": "13.2.1",
|
||||
"postcss": "^8.4.21",
|
||||
"tsconfig": "*",
|
||||
"tailwindcss": "^3.2.7"
|
||||
"tailwind-config-custom": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import useSWR from "swr";
|
||||
import type { GetServerSideProps } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
/// layouts
|
||||
import ProjectLayout from "layouts/project-layout";
|
||||
// components
|
||||
@@ -39,12 +40,4 @@ const WorkspaceProjectPage = (props: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
// export const getServerSideProps: GetServerSideProps<any> = async ({ query: { workspace_slug, project_slug } }) => {
|
||||
// const res = await fetch(
|
||||
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/settings/`
|
||||
// );
|
||||
// const project_settings = await res.json();
|
||||
// return { props: { project_settings } };
|
||||
// };
|
||||
|
||||
export default WorkspaceProjectPage;
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import React, { useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
// assets
|
||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import authenticationService from "services/authentication.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { OnBoardingForm } from "components/accounts/onboarding-form";
|
||||
|
||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
|
||||
|
||||
const OnBoardingPage = () => {
|
||||
const { user: userStore } = useMobxStore();
|
||||
|
||||
const user = userStore?.currentUser;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const user = userStore?.currentUser;
|
||||
|
||||
@@ -34,7 +27,7 @@ const OnBoardingPage = () => {
|
||||
<div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0 z-10" />
|
||||
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 py-5 left-2 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12 z-10">
|
||||
<div className="h-[30px] w-[30px]">
|
||||
<Image src={BluePlaneLogoWithoutText} alt="Plane logo" />
|
||||
<img src={`${imagePrefix}/plane-logos/blue-without-text.png`} alt="Plane logo" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute sm:fixed text-custom-text-100 text-sm font-medium right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
module.exports = require("tailwind-config-custom/postcss.config");
|
||||
|
||||
@@ -1,203 +1 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
const convertToRGB = (variableName) => `rgba(var(${variableName}))`;
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
"./app/**/*.{js,ts,jsx,tsx}",
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
"./layouts/**/*.tsx",
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
"./constants/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
"custom-shadow-2xs": "var(--color-shadow-2xs)",
|
||||
"custom-shadow-xs": "var(--color-shadow-xs)",
|
||||
"custom-shadow-sm": "var(--color-shadow-sm)",
|
||||
"custom-shadow-rg": "var(--color-shadow-rg)",
|
||||
"custom-shadow-md": "var(--color-shadow-md)",
|
||||
"custom-shadow-lg": "var(--color-shadow-lg)",
|
||||
"custom-shadow-xl": "var(--color-shadow-xl)",
|
||||
"custom-shadow-2xl": "var(--color-shadow-2xl)",
|
||||
"custom-shadow-3xl": "var(--color-shadow-3xl)",
|
||||
"custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)",
|
||||
"custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)",
|
||||
"custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)",
|
||||
"custom-sidebar-shadow-rg": "var(--color-sidebar-shadow-rg)",
|
||||
"custom-sidebar-shadow-md": "var(--color-sidebar-shadow-md)",
|
||||
"custom-sidebar-shadow-lg": "var(--color-sidebar-shadow-lg)",
|
||||
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
|
||||
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
|
||||
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
|
||||
},
|
||||
colors: {
|
||||
custom: {
|
||||
primary: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-primary-10"),
|
||||
20: convertToRGB("--color-primary-20"),
|
||||
30: convertToRGB("--color-primary-30"),
|
||||
40: convertToRGB("--color-primary-40"),
|
||||
50: convertToRGB("--color-primary-50"),
|
||||
60: convertToRGB("--color-primary-60"),
|
||||
70: convertToRGB("--color-primary-70"),
|
||||
80: convertToRGB("--color-primary-80"),
|
||||
90: convertToRGB("--color-primary-90"),
|
||||
100: convertToRGB("--color-primary-100"),
|
||||
200: convertToRGB("--color-primary-200"),
|
||||
300: convertToRGB("--color-primary-300"),
|
||||
400: convertToRGB("--color-primary-400"),
|
||||
500: convertToRGB("--color-primary-500"),
|
||||
600: convertToRGB("--color-primary-600"),
|
||||
700: convertToRGB("--color-primary-700"),
|
||||
800: convertToRGB("--color-primary-800"),
|
||||
900: convertToRGB("--color-primary-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-primary-100"),
|
||||
},
|
||||
background: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-background-10"),
|
||||
20: convertToRGB("--color-background-20"),
|
||||
30: convertToRGB("--color-background-30"),
|
||||
40: convertToRGB("--color-background-40"),
|
||||
50: convertToRGB("--color-background-50"),
|
||||
60: convertToRGB("--color-background-60"),
|
||||
70: convertToRGB("--color-background-70"),
|
||||
80: convertToRGB("--color-background-80"),
|
||||
90: convertToRGB("--color-background-90"),
|
||||
100: convertToRGB("--color-background-100"),
|
||||
200: convertToRGB("--color-background-200"),
|
||||
300: convertToRGB("--color-background-300"),
|
||||
400: convertToRGB("--color-background-400"),
|
||||
500: convertToRGB("--color-background-500"),
|
||||
600: convertToRGB("--color-background-600"),
|
||||
700: convertToRGB("--color-background-700"),
|
||||
800: convertToRGB("--color-background-800"),
|
||||
900: convertToRGB("--color-background-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-background-100"),
|
||||
},
|
||||
text: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-text-10"),
|
||||
20: convertToRGB("--color-text-20"),
|
||||
30: convertToRGB("--color-text-30"),
|
||||
40: convertToRGB("--color-text-40"),
|
||||
50: convertToRGB("--color-text-50"),
|
||||
60: convertToRGB("--color-text-60"),
|
||||
70: convertToRGB("--color-text-70"),
|
||||
80: convertToRGB("--color-text-80"),
|
||||
90: convertToRGB("--color-text-90"),
|
||||
100: convertToRGB("--color-text-100"),
|
||||
200: convertToRGB("--color-text-200"),
|
||||
300: convertToRGB("--color-text-300"),
|
||||
400: convertToRGB("--color-text-400"),
|
||||
500: convertToRGB("--color-text-500"),
|
||||
600: convertToRGB("--color-text-600"),
|
||||
700: convertToRGB("--color-text-700"),
|
||||
800: convertToRGB("--color-text-800"),
|
||||
900: convertToRGB("--color-text-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-text-100"),
|
||||
},
|
||||
border: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
100: convertToRGB("--color-border-100"),
|
||||
200: convertToRGB("--color-border-200"),
|
||||
300: convertToRGB("--color-border-300"),
|
||||
400: convertToRGB("--color-border-400"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-border-200"),
|
||||
},
|
||||
sidebar: {
|
||||
background: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-sidebar-background-10"),
|
||||
20: convertToRGB("--color-sidebar-background-20"),
|
||||
30: convertToRGB("--color-sidebar-background-30"),
|
||||
40: convertToRGB("--color-sidebar-background-40"),
|
||||
50: convertToRGB("--color-sidebar-background-50"),
|
||||
60: convertToRGB("--color-sidebar-background-60"),
|
||||
70: convertToRGB("--color-sidebar-background-70"),
|
||||
80: convertToRGB("--color-sidebar-background-80"),
|
||||
90: convertToRGB("--color-sidebar-background-90"),
|
||||
100: convertToRGB("--color-sidebar-background-100"),
|
||||
200: convertToRGB("--color-sidebar-background-200"),
|
||||
300: convertToRGB("--color-sidebar-background-300"),
|
||||
400: convertToRGB("--color-sidebar-background-400"),
|
||||
500: convertToRGB("--color-sidebar-background-500"),
|
||||
600: convertToRGB("--color-sidebar-background-600"),
|
||||
700: convertToRGB("--color-sidebar-background-700"),
|
||||
800: convertToRGB("--color-sidebar-background-800"),
|
||||
900: convertToRGB("--color-sidebar-background-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-background-100"),
|
||||
},
|
||||
text: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-sidebar-text-10"),
|
||||
20: convertToRGB("--color-sidebar-text-20"),
|
||||
30: convertToRGB("--color-sidebar-text-30"),
|
||||
40: convertToRGB("--color-sidebar-text-40"),
|
||||
50: convertToRGB("--color-sidebar-text-50"),
|
||||
60: convertToRGB("--color-sidebar-text-60"),
|
||||
70: convertToRGB("--color-sidebar-text-70"),
|
||||
80: convertToRGB("--color-sidebar-text-80"),
|
||||
90: convertToRGB("--color-sidebar-text-90"),
|
||||
100: convertToRGB("--color-sidebar-text-100"),
|
||||
200: convertToRGB("--color-sidebar-text-200"),
|
||||
300: convertToRGB("--color-sidebar-text-300"),
|
||||
400: convertToRGB("--color-sidebar-text-400"),
|
||||
500: convertToRGB("--color-sidebar-text-500"),
|
||||
600: convertToRGB("--color-sidebar-text-600"),
|
||||
700: convertToRGB("--color-sidebar-text-700"),
|
||||
800: convertToRGB("--color-sidebar-text-800"),
|
||||
900: convertToRGB("--color-sidebar-text-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-text-100"),
|
||||
},
|
||||
border: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
100: convertToRGB("--color-sidebar-border-100"),
|
||||
200: convertToRGB("--color-sidebar-border-200"),
|
||||
300: convertToRGB("--color-sidebar-border-300"),
|
||||
400: convertToRGB("--color-sidebar-border-400"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-border-200"),
|
||||
},
|
||||
},
|
||||
backdrop: "#131313",
|
||||
},
|
||||
},
|
||||
typography: ({ theme }) => ({
|
||||
brand: {
|
||||
css: {
|
||||
"--tw-prose-body": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-p": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-headings": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-lead": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-links": convertToRGB("--color-primary-100"),
|
||||
"--tw-prose-bold": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-counters": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-bullets": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-hr": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-quotes": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-quote-borders": convertToRGB("--color-border"),
|
||||
"--tw-prose-code": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-pre-code": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-pre-bg": convertToRGB("--color-background-100"),
|
||||
"--tw-prose-th-borders": convertToRGB("--color-border"),
|
||||
"--tw-prose-td-borders": convertToRGB("--color-border"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
fontFamily: {
|
||||
custom: ["Inter", "sans-serif"],
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
};
|
||||
module.exports = require("tailwind-config-custom/tailwind.config");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": "tsconfig/nextjs.json",
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
rules: {
|
||||
"@next/next/no-img-element": "off",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// nivo
|
||||
import { BarDatum } from "@nivo/bar";
|
||||
// icons
|
||||
import { getPriorityIcon } from "components/icons";
|
||||
import { PriorityIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// helpers
|
||||
import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics";
|
||||
|
||||
@@ -53,7 +53,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{params.segment === "priority" ? (
|
||||
getPriorityIcon(key)
|
||||
<PriorityIcon priority={key as TIssuePriorities} />
|
||||
) : (
|
||||
<span
|
||||
className="h-3 w-3 flex-shrink-0 rounded"
|
||||
@@ -91,7 +91,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
||||
}`}
|
||||
>
|
||||
{params.x_axis === "priority" ? (
|
||||
getPriorityIcon(`${item.name}`)
|
||||
<PriorityIcon priority={item.name as TIssuePriorities} />
|
||||
) : (
|
||||
<span
|
||||
className="h-3 w-3 rounded"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// icons
|
||||
import { PlayIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IDefaultAnalyticsResponse } from "types";
|
||||
import { IDefaultAnalyticsResponse, TStateGroups } from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
@@ -27,7 +27,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<span
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group],
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
|
||||
}}
|
||||
/>
|
||||
<h6 className="capitalize">{group.state_group}</h6>
|
||||
@@ -42,7 +42,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
className="absolute top-0 left-0 h-1 rounded duration-300"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group],
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
|
||||
import { SelectMonthModal } from "components/automation";
|
||||
// icons
|
||||
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
// constants
|
||||
@@ -46,7 +46,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
||||
query: state.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
@@ -140,14 +140,19 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedOption ? (
|
||||
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
|
||||
<StateGroupIcon
|
||||
stateGroup={selectedOption.group}
|
||||
color={selectedOption.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
) : currentDefaultState ? (
|
||||
getStateGroupIcon(
|
||||
currentDefaultState.group,
|
||||
"16",
|
||||
"16",
|
||||
currentDefaultState.color
|
||||
)
|
||||
<StateGroupIcon
|
||||
stateGroup={currentDefaultState.group}
|
||||
color={currentDefaultState.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
) : (
|
||||
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Dispatch, SetStateAction, useCallback } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// cmdk
|
||||
@@ -7,12 +9,12 @@ import { Command } from "cmdk";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
import { PRIORITIES } from "constants/project";
|
||||
// icons
|
||||
import { CheckIcon, getPriorityIcon } from "components/icons";
|
||||
import { CheckIcon, PriorityIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -54,7 +56,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
|
||||
[workspaceSlug, issueId, projectId, user]
|
||||
);
|
||||
|
||||
const handleIssueState = (priority: string | null) => {
|
||||
const handleIssueState = (priority: TIssuePriorities) => {
|
||||
submitChanges({ priority });
|
||||
setIsPaletteOpen(false);
|
||||
};
|
||||
@@ -68,7 +70,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
{getPriorityIcon(priority)}
|
||||
<PriorityIcon priority={priority} />
|
||||
<span className="capitalize">{priority ?? "None"}</span>
|
||||
</div>
|
||||
<div>{priority === issue.priority && <CheckIcon className="h-3 w-3" />}</div>
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Dispatch, SetStateAction, useCallback } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// cmdk
|
||||
import { Command } from "cmdk";
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import stateService from "services/state.service";
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
// icons
|
||||
import { CheckIcon, StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
// fetch keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
|
||||
// icons
|
||||
import { CheckIcon, getStateGroupIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -82,7 +84,12 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||
<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>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||
// ui
|
||||
import { Avatar } from "components/ui";
|
||||
// helpers
|
||||
@@ -71,12 +71,10 @@ export const FiltersList: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{getStateGroupIcon(
|
||||
state?.group ?? "backlog",
|
||||
"12",
|
||||
"12",
|
||||
state?.color
|
||||
)}
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
color={state?.color}
|
||||
/>
|
||||
</span>
|
||||
<span>{state?.name ?? ""}</span>
|
||||
<span
|
||||
@@ -105,7 +103,9 @@ export const FiltersList: React.FC<Props> = ({
|
||||
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
|
||||
}}
|
||||
>
|
||||
<span>{getStateGroupIcon(group, "16", "16")}</span>
|
||||
<span>
|
||||
<StateGroupIcon stateGroup={group} color={undefined} />
|
||||
</span>
|
||||
<span>{group}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
@@ -136,7 +136,9 @@ export const FiltersList: React.FC<Props> = ({
|
||||
: "bg-custom-background-90 text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<span>{getPriorityIcon(priority)}</span>
|
||||
<span>
|
||||
<PriorityIcon priority={priority} />
|
||||
</span>
|
||||
<span>{priority === "null" ? "None" : priority}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
|
||||
@@ -58,16 +58,8 @@ export const IssuesFilterView: React.FC = () => {
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const {
|
||||
issueView,
|
||||
setIssueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
showSubIssues,
|
||||
setShowSubIssues,
|
||||
setShowEmptyGroups,
|
||||
displayFilters,
|
||||
setDisplayFilters,
|
||||
filters,
|
||||
setFilters,
|
||||
resetFilterToDefault,
|
||||
@@ -96,11 +88,11 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
issueView === option.type
|
||||
displayFilters.layout === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setIssueView(option.type)}
|
||||
onClick={() => setDisplayFilters({ layout: option.type })}
|
||||
>
|
||||
<option.Icon
|
||||
sx={{
|
||||
@@ -174,28 +166,30 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||
<div className="relative divide-y-2 divide-custom-border-200">
|
||||
<div className="space-y-4 pb-3 text-xs">
|
||||
{issueView !== "calendar" &&
|
||||
issueView !== "spreadsheet" &&
|
||||
issueView !== "gantt_chart" && (
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
displayFilters.layout !== "gantt_chart" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
GROUP_BY_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.group_by
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (displayFilters.layout === "kanban" && option.key === null)
|
||||
return null;
|
||||
if (option.key === "project") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
onClick={() => setDisplayFilters({ group_by: option.key })}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
@@ -205,41 +199,45 @@ export const IssuesFilterView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{issueView !== "calendar" && issueView !== "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 === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) =>
|
||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</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={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
||||
?.name ?? "Select"
|
||||
FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.type
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
@@ -248,7 +246,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
setDisplayFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
@@ -260,33 +258,40 @@ export const IssuesFilterView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={showSubIssues}
|
||||
onChange={() => setShowSubIssues(!showSubIssues)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{issueView !== "calendar" &&
|
||||
issueView !== "spreadsheet" &&
|
||||
issueView !== "gantt_chart" && (
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={showEmptyGroups}
|
||||
onChange={() => setShowEmptyGroups(!showEmptyGroups)}
|
||||
value={displayFilters.sub_issue ?? true}
|
||||
onChange={() =>
|
||||
setDisplayFilters({ sub_issue: !displayFilters.sub_issue })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{issueView !== "calendar" &&
|
||||
issueView !== "spreadsheet" &&
|
||||
issueView !== "gantt_chart" && (
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
displayFilters.layout !== "gantt_chart" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={displayFilters.show_empty_groups ?? true}
|
||||
onChange={() =>
|
||||
setDisplayFilters({
|
||||
show_empty_groups: !displayFilters.show_empty_groups,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
displayFilters.layout !== "gantt_chart" && (
|
||||
<div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
@@ -302,7 +307,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{issueView !== "gantt_chart" && (
|
||||
{displayFilters.layout !== "gantt_chart" && (
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||
@@ -310,7 +315,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
if (
|
||||
issueView === "spreadsheet" &&
|
||||
displayFilters.layout === "spreadsheet" &&
|
||||
(key === "attachment_count" ||
|
||||
key === "link" ||
|
||||
key === "sub_issue_count")
|
||||
@@ -318,7 +323,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
return null;
|
||||
|
||||
if (
|
||||
issueView !== "spreadsheet" &&
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
(key === "created_on" || key === "updated_on")
|
||||
)
|
||||
return null;
|
||||
|
||||
@@ -58,7 +58,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
||||
);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
const { issueView, params } = useIssuesView();
|
||||
const { displayFilters, params } = useIssuesView();
|
||||
const { params: calendarParams } = useCalendarIssuesView();
|
||||
const { order_by, group_by, ...viewGanttParams } = params;
|
||||
|
||||
@@ -126,8 +126,8 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
||||
message: "Issues deleted successfully!",
|
||||
});
|
||||
|
||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
||||
else if (issueView === "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));
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TAssigneesDistribution,
|
||||
TCompletionChartDistribution,
|
||||
TLabelsDistribution,
|
||||
TStateGroups,
|
||||
} from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
@@ -215,7 +216,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full "
|
||||
style={{
|
||||
backgroundColor: STATE_GROUP_COLORS[group],
|
||||
backgroundColor: STATE_GROUP_COLORS[group as TStateGroups],
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs capitalize">{group}</span>
|
||||
|
||||
@@ -80,7 +80,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
const { user } = useUser();
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
|
||||
const { groupedIssues, isEmpty, issueView } = viewProps;
|
||||
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
@@ -117,11 +117,11 @@ export const AllViews: React.FC<Props> = ({
|
||||
</StrictModeDroppable>
|
||||
{groupedIssues ? (
|
||||
!isEmpty ||
|
||||
issueView === "kanban" ||
|
||||
issueView === "calendar" ||
|
||||
issueView === "gantt_chart" ? (
|
||||
displayFilters?.layout === "kanban" ||
|
||||
displayFilters?.layout === "calendar" ||
|
||||
displayFilters?.layout === "gantt_chart" ? (
|
||||
<>
|
||||
{issueView === "list" ? (
|
||||
{displayFilters?.layout === "list" ? (
|
||||
<AllLists
|
||||
states={states}
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
@@ -134,7 +134,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
userAuth={memberRole}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
) : issueView === "kanban" ? (
|
||||
) : displayFilters?.layout === "kanban" ? (
|
||||
<AllBoards
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
disableUserActions={disableUserActions}
|
||||
@@ -149,7 +149,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
userAuth={memberRole}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
) : issueView === "calendar" ? (
|
||||
) : displayFilters?.layout === "calendar" ? (
|
||||
<CalendarView
|
||||
handleIssueAction={handleIssueAction}
|
||||
addIssueToDate={addIssueToDate}
|
||||
@@ -157,7 +157,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
user={user}
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : issueView === "spreadsheet" ? (
|
||||
) : displayFilters?.layout === "spreadsheet" ? (
|
||||
<SpreadsheetView
|
||||
handleIssueAction={handleIssueAction}
|
||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
@@ -166,7 +166,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : (
|
||||
issueView === "gantt_chart" && <GanttChartView />
|
||||
displayFilters?.layout === "gantt_chart" && <GanttChartView />
|
||||
)}
|
||||
</>
|
||||
) : router.pathname.includes("archived-issues") ? (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// components
|
||||
import { SingleBoard } from "components/core/views/board-view/single-board";
|
||||
// icons
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
@@ -36,7 +36,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -44,9 +44,12 @@ export const AllBoards: React.FC<Props> = ({
|
||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
|
||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
displayFilters?.group_by === "state"
|
||||
? states?.find((s) => s.id === singleGroup)
|
||||
: null;
|
||||
|
||||
if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
|
||||
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<SingleBoard
|
||||
@@ -67,13 +70,15 @@ export const AllBoards: React.FC<Props> = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!showEmptyGroups && (
|
||||
{!displayFilters?.show_empty_groups && (
|
||||
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
|
||||
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
||||
<div className="space-y-3">
|
||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||
const currentState =
|
||||
selectedGroup === "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 (
|
||||
@@ -82,10 +87,16 @@ export const AllBoards: React.FC<Props> = ({
|
||||
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{currentState &&
|
||||
getStateGroupIcon(currentState.group, "16", "16", currentState.color)}
|
||||
{currentState && (
|
||||
<StateGroupIcon
|
||||
stateGroup={currentState.group}
|
||||
color={currentState.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
)}
|
||||
<h4 className="text-sm capitalize">
|
||||
{selectedGroup === "state"
|
||||
{displayFilters?.group_by === "state"
|
||||
? addSpaceIfCamelCase(currentState?.name ?? "")
|
||||
: addSpaceIfCamelCase(singleGroup)}
|
||||
</h4>
|
||||
|
||||
@@ -13,14 +13,16 @@ import useProjects from "hooks/use-projects";
|
||||
import { Avatar, Icon } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import { IIssueViewProps, IState } from "types";
|
||||
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
type Props = {
|
||||
currentState?: IState | null;
|
||||
@@ -46,22 +48,28 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
console.log("dF", displayFilters);
|
||||
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId && selectedGroup === "labels"
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && selectedGroup === "labels"
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
|
||||
workspaceSlug &&
|
||||
projectId &&
|
||||
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
|
||||
? PROJECT_MEMBERS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
|
||||
workspaceSlug &&
|
||||
projectId &&
|
||||
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
|
||||
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
@@ -71,7 +79,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
const getGroupTitle = () => {
|
||||
let title = addSpaceIfCamelCase(groupTitle);
|
||||
|
||||
switch (selectedGroup) {
|
||||
switch (displayFilters?.group_by) {
|
||||
case "state":
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
@@ -95,16 +103,29 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
const getGroupIcon = () => {
|
||||
let icon;
|
||||
|
||||
switch (selectedGroup) {
|
||||
switch (displayFilters?.group_by) {
|
||||
case "state":
|
||||
icon =
|
||||
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||
icon = currentState && (
|
||||
<StateGroupIcon
|
||||
stateGroup={currentState.group}
|
||||
color={currentState.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "state_detail.group":
|
||||
icon = getStateGroupIcon(groupTitle as any, "16", "16");
|
||||
icon = (
|
||||
<StateGroupIcon
|
||||
stateGroup={groupTitle as TStateGroups}
|
||||
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "priority":
|
||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||
icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
|
||||
break;
|
||||
case "project":
|
||||
const project = projects?.find((p) => p.id === groupTitle);
|
||||
@@ -152,7 +173,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
<span className="flex items-center">{getGroupIcon()}</span>
|
||||
<h2
|
||||
className={`text-lg font-semibold truncate ${
|
||||
selectedGroup === "created_by" ? "" : "capitalize"
|
||||
displayFilters?.group_by === "created_by" ? "" : "capitalize"
|
||||
}`}
|
||||
style={{
|
||||
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
|
||||
@@ -183,7 +204,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
|
||||
)}
|
||||
</button>
|
||||
{!disableAddIssue && !disableUserActions && selectedGroup !== "created_by" && (
|
||||
{!disableAddIssue && !disableUserActions && displayFilters?.group_by !== "created_by" && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
||||
|
||||
@@ -50,7 +50,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
// collapse/expand
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
const { groupedIssues, groupByProperty: selectedGroup, orderBy, properties } = viewProps;
|
||||
const { displayFilters, groupedIssues, properties } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
@@ -80,14 +80,14 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`relative h-full ${
|
||||
orderBy !== "sort_order" && snapshot.isDraggingOver
|
||||
displayFilters?.order_by !== "sort_order" && snapshot.isDraggingOver
|
||||
? "bg-custom-background-100/20"
|
||||
: ""
|
||||
} ${!isCollapsed ? "hidden" : "flex flex-col"}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{orderBy !== "sort_order" && (
|
||||
{displayFilters?.order_by !== "sort_order" && (
|
||||
<>
|
||||
<div
|
||||
className={`absolute ${
|
||||
@@ -101,7 +101,11 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
>
|
||||
This board is ordered by{" "}
|
||||
{replaceUnderscoreIfSnakeCase(
|
||||
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
|
||||
displayFilters?.order_by
|
||||
? displayFilters?.order_by[0] === "-"
|
||||
? displayFilters?.order_by.slice(1)
|
||||
: displayFilters?.order_by
|
||||
: "created_at"
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
@@ -145,13 +149,13 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
))}
|
||||
<span
|
||||
style={{
|
||||
display: orderBy === "sort_order" ? "inline" : "none",
|
||||
display: displayFilters?.order_by === "sort_order" ? "inline" : "none",
|
||||
}}
|
||||
>
|
||||
{provided.placeholder}
|
||||
</span>
|
||||
</div>
|
||||
{selectedGroup !== "created_by" && (
|
||||
{displayFilters?.group_by !== "created_by" && (
|
||||
<div>
|
||||
{type === "issue"
|
||||
? !disableAddIssueOption && (
|
||||
|
||||
@@ -93,7 +93,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
|
||||
const { displayFilters, properties, mutateIssues } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
@@ -131,9 +131,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
handleIssuesMutation(
|
||||
formData,
|
||||
groupTitle ?? "",
|
||||
selectedGroup,
|
||||
displayFilters?.group_by ?? null,
|
||||
index,
|
||||
orderBy,
|
||||
displayFilters?.order_by ?? "-created_at",
|
||||
prevData
|
||||
),
|
||||
false
|
||||
@@ -149,24 +149,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
});
|
||||
},
|
||||
[
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
groupTitle,
|
||||
index,
|
||||
selectedGroup,
|
||||
mutateIssues,
|
||||
orderBy,
|
||||
user,
|
||||
]
|
||||
[displayFilters, workspaceSlug, cycleId, moduleId, groupTitle, index, mutateIssues, user]
|
||||
);
|
||||
|
||||
const getStyle = (
|
||||
style: DraggingStyle | NotDraggingStyle | undefined,
|
||||
snapshot: DraggableStateSnapshot
|
||||
) => {
|
||||
if (orderBy === "sort_order") return style;
|
||||
if (displayFilters?.order_by === "sort_order") return style;
|
||||
if (!snapshot.isDragging) return {};
|
||||
if (!snapshot.isDropAnimating) return style;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export const CalendarView: React.FC<Props> = ({
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { calendarIssues, params, setCalendarDateRange } = useCalendarIssuesView();
|
||||
const { calendarIssues, params, displayFilters, setDisplayFilters } = useCalendarIssuesView();
|
||||
|
||||
const totalDate = eachDayOfInterval({
|
||||
start: calendarDates.startDate,
|
||||
@@ -152,18 +152,21 @@ export const CalendarView: React.FC<Props> = ({
|
||||
endDate,
|
||||
});
|
||||
|
||||
setCalendarDateRange(
|
||||
`${renderDateFormat(startDate)};after,${renderDateFormat(endDate)};before`
|
||||
);
|
||||
setDisplayFilters({
|
||||
calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat(
|
||||
endDate
|
||||
)};before`,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCalendarDateRange(
|
||||
`${renderDateFormat(startOfWeek(currentDate))};after,${renderDateFormat(
|
||||
lastDayOfWeek(currentDate)
|
||||
)};before`
|
||||
);
|
||||
}, [currentDate]);
|
||||
if (!displayFilters || displayFilters.calendar_date_range === "")
|
||||
setDisplayFilters({
|
||||
calendar_date_range: `${renderDateFormat(
|
||||
startOfWeek(currentDate)
|
||||
)};after,${renderDateFormat(lastDayOfWeek(currentDate))};before`,
|
||||
});
|
||||
}, [currentDate, displayFilters, setDisplayFilters]);
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue, IIssueFilterOptions, IState } from "types";
|
||||
import { IIssue, IIssueFilterOptions, IState, TIssuePriorities } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
@@ -77,18 +77,8 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
groupedByIssues,
|
||||
mutateIssues,
|
||||
issueView,
|
||||
groupByProperty: selectedGroup,
|
||||
orderBy,
|
||||
filters,
|
||||
isEmpty,
|
||||
setFilters,
|
||||
params,
|
||||
showEmptyGroups,
|
||||
} = useIssuesView();
|
||||
const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params } =
|
||||
useIssuesView();
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
@@ -129,7 +119,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
if (destination.droppableId === "trashBox") {
|
||||
handleDeleteIssue(draggedItem);
|
||||
} else {
|
||||
if (orderBy === "sort_order") {
|
||||
if (displayFilters.order_by === "sort_order") {
|
||||
let newSortOrder = draggedItem.sort_order;
|
||||
|
||||
const destinationGroupArray = groupedByIssues[destination.droppableId];
|
||||
@@ -177,15 +167,19 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const destinationGroup = destination.droppableId; // destination group id
|
||||
|
||||
if (orderBy === "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 (selectedGroup === "priority") draggedItem.priority = destinationGroup;
|
||||
else if (selectedGroup === "state") {
|
||||
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;
|
||||
}
|
||||
@@ -212,8 +206,14 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
[sourceGroup]: orderArrayBy(sourceGroupArray, orderBy),
|
||||
[destinationGroup]: orderArrayBy(destinationGroupArray, orderBy),
|
||||
[sourceGroup]: orderArrayBy(
|
||||
sourceGroupArray,
|
||||
displayFilters.order_by ?? "-created_at"
|
||||
),
|
||||
[destinationGroup]: orderArrayBy(
|
||||
destinationGroupArray,
|
||||
displayFilters.order_by ?? "-created_at"
|
||||
),
|
||||
};
|
||||
},
|
||||
false
|
||||
@@ -266,13 +266,13 @@ export const IssuesView: React.FC<Props> = ({
|
||||
}
|
||||
},
|
||||
[
|
||||
displayFilters.group_by,
|
||||
displayFilters.order_by,
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
groupedByIssues,
|
||||
projectId,
|
||||
selectedGroup,
|
||||
orderBy,
|
||||
handleDeleteIssue,
|
||||
params,
|
||||
states,
|
||||
@@ -286,19 +286,19 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
let preloadedValue: string | string[] = groupTitle;
|
||||
|
||||
if (selectedGroup === "labels") {
|
||||
if (displayFilters.group_by === "labels") {
|
||||
if (groupTitle === "None") preloadedValue = [];
|
||||
else preloadedValue = [groupTitle];
|
||||
}
|
||||
|
||||
if (selectedGroup)
|
||||
if (displayFilters.group_by)
|
||||
setPreloadedData({
|
||||
[selectedGroup]: preloadedValue,
|
||||
[displayFilters.group_by]: preloadedValue,
|
||||
actionType: "createIssue",
|
||||
});
|
||||
else setPreloadedData({ actionType: "createIssue" });
|
||||
},
|
||||
[setCreateIssueModal, setPreloadedData, selectedGroup]
|
||||
[displayFilters.group_by, setCreateIssueModal, setPreloadedData]
|
||||
);
|
||||
|
||||
const addIssueToDate = useCallback(
|
||||
@@ -351,7 +351,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
|
||||
(prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
if (selectedGroup) {
|
||||
if (displayFilters.group_by) {
|
||||
const filteredData: any = {};
|
||||
for (const key in prevData) {
|
||||
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
|
||||
@@ -383,7 +383,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
console.log(e);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, projectId, cycleId, params, selectedGroup, setToastAlert]
|
||||
[displayFilters.group_by, workspaceSlug, projectId, cycleId, params, setToastAlert]
|
||||
);
|
||||
|
||||
const removeIssueFromModule = useCallback(
|
||||
@@ -394,7 +394,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
MODULE_ISSUES_WITH_PARAMS(moduleId as string, params),
|
||||
(prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
if (selectedGroup) {
|
||||
if (displayFilters.group_by) {
|
||||
const filteredData: any = {};
|
||||
for (const key in prevData) {
|
||||
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
|
||||
@@ -426,7 +426,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
console.log(e);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
|
||||
[displayFilters.group_by, workspaceSlug, projectId, moduleId, params, setToastAlert]
|
||||
);
|
||||
|
||||
const nullFilters = Object.keys(filters).filter(
|
||||
@@ -480,7 +480,6 @@ export const IssuesView: React.FC<Props> = ({
|
||||
state: null,
|
||||
start_date: null,
|
||||
target_date: null,
|
||||
type: null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -512,10 +511,10 @@ export const IssuesView: React.FC<Props> = ({
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
disableUserActions={disableUserActions}
|
||||
dragDisabled={
|
||||
selectedGroup === "created_by" ||
|
||||
selectedGroup === "labels" ||
|
||||
selectedGroup === "state_detail.group" ||
|
||||
selectedGroup === "assignees"
|
||||
displayFilters.group_by === "created_by" ||
|
||||
displayFilters.group_by === "labels" ||
|
||||
displayFilters.group_by === "state_detail.group" ||
|
||||
displayFilters.group_by === "assignees"
|
||||
}
|
||||
emptyState={{
|
||||
title: cycleId
|
||||
@@ -553,15 +552,12 @@ export const IssuesView: React.FC<Props> = ({
|
||||
trashBox={trashBox}
|
||||
setTrashBox={setTrashBox}
|
||||
viewProps={{
|
||||
groupByProperty: selectedGroup,
|
||||
groupedIssues: groupedByIssues,
|
||||
displayFilters,
|
||||
isEmpty,
|
||||
issueView,
|
||||
mutateIssues,
|
||||
orderBy,
|
||||
params,
|
||||
properties,
|
||||
showEmptyGroups,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -29,7 +29,7 @@ export const AllLists: React.FC<Props> = ({
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -37,9 +37,12 @@ export const AllLists: React.FC<Props> = ({
|
||||
<div className="h-full overflow-y-auto">
|
||||
{Object.keys(groupedIssues).map((singleGroup) => {
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
displayFilters?.group_by === "state"
|
||||
? states?.find((s) => s.id === singleGroup)
|
||||
: null;
|
||||
|
||||
if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
|
||||
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<SingleList
|
||||
|
||||
@@ -91,7 +91,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
|
||||
const { displayFilters, properties, mutateIssues } = viewProps;
|
||||
|
||||
const partialUpdateIssue = useCallback(
|
||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||
@@ -124,9 +124,9 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
handleIssuesMutation(
|
||||
formData,
|
||||
groupTitle ?? "",
|
||||
selectedGroup,
|
||||
displayFilters?.group_by ?? null,
|
||||
index,
|
||||
orderBy,
|
||||
displayFilters?.order_by ?? "-created_at",
|
||||
prevData
|
||||
),
|
||||
false
|
||||
@@ -148,15 +148,14 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
});
|
||||
},
|
||||
[
|
||||
displayFilters,
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
userId,
|
||||
groupTitle,
|
||||
index,
|
||||
selectedGroup,
|
||||
mutateIssues,
|
||||
orderBy,
|
||||
user,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import { SingleListIssue } from "components/core";
|
||||
import { Avatar, CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
@@ -26,10 +26,14 @@ import {
|
||||
IIssueLabels,
|
||||
IIssueViewProps,
|
||||
IState,
|
||||
TIssuePriorities,
|
||||
TStateGroups,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
type Props = {
|
||||
currentState?: IState | null;
|
||||
@@ -65,7 +69,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
|
||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||
|
||||
const { groupByProperty: selectedGroup, groupedIssues } = viewProps;
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
@@ -86,7 +90,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
const getGroupTitle = () => {
|
||||
let title = addSpaceIfCamelCase(groupTitle);
|
||||
|
||||
switch (selectedGroup) {
|
||||
switch (displayFilters?.group_by) {
|
||||
case "state":
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
@@ -109,16 +113,29 @@ export const SingleList: React.FC<Props> = ({
|
||||
const getGroupIcon = () => {
|
||||
let icon;
|
||||
|
||||
switch (selectedGroup) {
|
||||
switch (displayFilters?.group_by) {
|
||||
case "state":
|
||||
icon =
|
||||
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||
icon = currentState && (
|
||||
<StateGroupIcon
|
||||
stateGroup={currentState.group}
|
||||
color={currentState.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "state_detail.group":
|
||||
icon = getStateGroupIcon(groupTitle as any, "16", "16");
|
||||
icon = (
|
||||
<StateGroupIcon
|
||||
stateGroup={groupTitle as TStateGroups}
|
||||
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "priority":
|
||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||
icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
|
||||
break;
|
||||
case "project":
|
||||
const project = projects?.find((p) => p.id === groupTitle);
|
||||
@@ -160,13 +177,13 @@ 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">
|
||||
{selectedGroup !== null && (
|
||||
{displayFilters?.group_by !== null && (
|
||||
<div className="flex items-center">{getGroupIcon()}</div>
|
||||
)}
|
||||
{selectedGroup !== null ? (
|
||||
{displayFilters?.group_by !== null ? (
|
||||
<h2
|
||||
className={`text-sm font-semibold leading-6 text-custom-text-100 ${
|
||||
selectedGroup === "created_by" ? "" : "capitalize"
|
||||
displayFilters?.group_by === "created_by" ? "" : "capitalize"
|
||||
}`}
|
||||
>
|
||||
{getGroupTitle()}
|
||||
|
||||
@@ -22,10 +22,10 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
|
||||
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
|
||||
|
||||
const { orderBy, setOrderBy } = useSpreadsheetIssuesView();
|
||||
const { displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
|
||||
|
||||
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
|
||||
setOrderBy(order);
|
||||
setDisplayFilters({ order_by: order });
|
||||
setSelectedMenuItem(`${order}_${itemKey}`);
|
||||
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
|
||||
};
|
||||
@@ -239,7 +239,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
</CustomMenu.MenuItem>
|
||||
{selectedMenuItem &&
|
||||
selectedMenuItem !== "" &&
|
||||
orderBy !== "-created_at" &&
|
||||
displayFilters?.order_by !== "-created_at" &&
|
||||
selectedMenuItem.includes(col.propertyName) && (
|
||||
<CustomMenu.MenuItem
|
||||
className={`mt-0.5${
|
||||
|
||||
@@ -19,7 +19,7 @@ import { ActiveCycleProgressStats } from "components/cycles";
|
||||
|
||||
// icons
|
||||
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
|
||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||
import { PriorityIcon } from "components/icons/priority-icon";
|
||||
import {
|
||||
TargetIcon,
|
||||
ContrastIcon,
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
TriangleExclamationIcon,
|
||||
AlarmClockIcon,
|
||||
LayerDiagonalIcon,
|
||||
CompletedStateIcon,
|
||||
StateGroupIcon,
|
||||
} from "components/icons";
|
||||
import { StarIcon } from "@heroicons/react/24/outline";
|
||||
// components
|
||||
@@ -385,8 +385,8 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
<LayerDiagonalIcon className="h-4 w-4 flex-shrink-0" />
|
||||
{cycle.total_issues} issues
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<CompletedStateIcon width={16} height={16} color="#438AF3" />
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup="completed" height="14px" width="14px" />
|
||||
{cycle.completed_issues} issues
|
||||
</div>
|
||||
</div>
|
||||
@@ -477,7 +477,7 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
: "border-orange-500/20 bg-orange-500/20 text-orange-500"
|
||||
}`}
|
||||
>
|
||||
{getPriorityIcon(issue.priority, "text-sm")}
|
||||
<PriorityIcon priority={issue.priority} className="text-sm" />
|
||||
</div>
|
||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={2} />
|
||||
<div className={`flex items-center gap-2 text-custom-text-200`}>
|
||||
|
||||
@@ -16,7 +16,7 @@ export const CycleIssuesGanttChartView = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
|
||||
const { orderBy } = useIssuesView();
|
||||
const { displayFilters } = useIssuesView();
|
||||
|
||||
const { user } = useUser();
|
||||
const { projectDetails } = useProjectDetails();
|
||||
@@ -44,7 +44,7 @@ export const CycleIssuesGanttChartView = () => {
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={orderBy === "sort_order" && isAllowed}
|
||||
enableReorder={displayFilters.order_by === "sort_order" && isAllowed}
|
||||
bottomSpacing
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const BacklogStateIcon: React.FC<Props> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "rgb(var(--color-text-200))",
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="10" cy="10" r="9" stroke={color} strokeLinecap="round" strokeDasharray="4 4" />
|
||||
</svg>
|
||||
);
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const CancelledStateIcon: React.FC<Props> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#f2655a",
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 84.36 84.36"
|
||||
>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
|
||||
/>
|
||||
<circle className="cls-2" fill={color} cx="42.18" cy="42.18" r="31.04" />
|
||||
<path
|
||||
className="cls-3"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke="#ffffff"
|
||||
strokeLinecap="square"
|
||||
strokeMiterlimit={10}
|
||||
d="M32.64,32.44q9.54,9.75,19.09,19.48"
|
||||
/>
|
||||
<path
|
||||
className="cls-3"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke="#ffffff"
|
||||
strokeLinecap="square"
|
||||
strokeMiterlimit={10}
|
||||
d="M32.64,51.92,51.73,32.44"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const CompletedStateIcon: React.FC<Props> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#438af3",
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 84.36 84.36"
|
||||
>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
|
||||
/>
|
||||
<circle className="cls-2" fill={color} cx="42.18" cy="42.18" r="31.04" />
|
||||
<path
|
||||
className="cls-3"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
stroke="#ffffff"
|
||||
strokeLinecap="square"
|
||||
strokeMiterlimit={10}
|
||||
d="M30.45,43.75l6.61,6.61L53.92,34"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./module";
|
||||
export * from "./state";
|
||||
export * from "./alarm-clock-icon";
|
||||
export * from "./attachment-icon";
|
||||
export * from "./backlog-state-icon";
|
||||
export * from "./blocked-icon";
|
||||
export * from "./blocker-icon";
|
||||
export * from "./bolt-icon";
|
||||
@@ -8,12 +9,10 @@ export * from "./calendar-before-icon";
|
||||
export * from "./calendar-after-icon";
|
||||
export * from "./calendar-month-icon";
|
||||
export * from "./cancel-icon";
|
||||
export * from "./cancelled-state-icon";
|
||||
export * from "./clipboard-icon";
|
||||
export * from "./color-pallette-icon";
|
||||
export * from "./comment-icon";
|
||||
export * from "./completed-cycle-icon";
|
||||
export * from "./completed-state-icon";
|
||||
export * from "./current-cycle-icon";
|
||||
export * from "./cycle-icon";
|
||||
export * from "./discord-icon";
|
||||
@@ -23,11 +22,9 @@ export * from "./ellipsis-horizontal-icon";
|
||||
export * from "./external-link-icon";
|
||||
export * from "./github-icon";
|
||||
export * from "./heartbeat-icon";
|
||||
export * from "./started-state-icon";
|
||||
export * from "./layer-diagonal-icon";
|
||||
export * from "./lock-icon";
|
||||
export * from "./menu-icon";
|
||||
export * from "./module";
|
||||
export * from "./pencil-scribble-icon";
|
||||
export * from "./plus-icon";
|
||||
export * from "./person-running-icon";
|
||||
@@ -36,11 +33,8 @@ export * from "./question-mark-circle-icon";
|
||||
export * from "./setting-icon";
|
||||
export * from "./signal-cellular-icon";
|
||||
export * from "./stacked-layers-icon";
|
||||
export * from "./started-state-icon";
|
||||
export * from "./state-group-icon";
|
||||
export * from "./tag-icon";
|
||||
export * from "./tune-icon";
|
||||
export * from "./unstarted-state-icon";
|
||||
export * from "./upcoming-cycle-icon";
|
||||
export * from "./user-group-icon";
|
||||
export * from "./user-icon-circle";
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
export const getPriorityIcon = (priority: string | null, className?: string) => {
|
||||
// types
|
||||
import { TIssuePriorities } from "types";
|
||||
|
||||
type Props = {
|
||||
priority: TIssuePriorities | null;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const PriorityIcon: React.FC<Props> = ({ priority, className = "" }) => {
|
||||
if (!className || className === "") className = "text-xs flex items-center";
|
||||
|
||||
priority = priority?.toLowerCase() ?? null;
|
||||
|
||||
switch (priority) {
|
||||
case "urgent":
|
||||
return <span className={`material-symbols-rounded ${className}`}>error</span>;
|
||||
case "high":
|
||||
return <span className={`material-symbols-rounded ${className}`}>signal_cellular_alt</span>;
|
||||
case "medium":
|
||||
return (
|
||||
<span className={`material-symbols-rounded ${className}`}>signal_cellular_alt_2_bar</span>
|
||||
);
|
||||
case "low":
|
||||
return (
|
||||
<span className={`material-symbols-rounded ${className}`}>signal_cellular_alt_1_bar</span>
|
||||
);
|
||||
default:
|
||||
return <span className={`material-symbols-rounded ${className}`}>block</span>;
|
||||
}
|
||||
return (
|
||||
<span className={`material-symbols-rounded ${className}`}>
|
||||
{priority === "urgent"
|
||||
? "error"
|
||||
: priority === "high"
|
||||
? "signal_cellular_alt"
|
||||
: priority === "medium"
|
||||
? "signal_cellular_alt_2_bar"
|
||||
: priority === "low"
|
||||
? "signal_cellular_alt_1_bar"
|
||||
: "block"}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const StartedStateIcon: React.FC<Props> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#fbb040",
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 83.36 83.36"
|
||||
>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M20,7.19a39.74,39.74,0,0,1,43.43.54"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M76.17,20a39.76,39.76,0,0,1-.53,43.43"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M63.42,76.17A39.78,39.78,0,0,1,20,75.64"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M7.19,63.42A39.75,39.75,0,0,1,7.73,20"
|
||||
/>
|
||||
<path
|
||||
className="cls-2"
|
||||
fill={color}
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M42.32,41.21q9.57-14.45,19.13-28.9a35.8,35.8,0,0,0-39.09,0Z"
|
||||
/>
|
||||
<path
|
||||
className="cls-2"
|
||||
fill={color}
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M42.32,41.7,61.45,70.6a35.75,35.75,0,0,1-39.09,0Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
BacklogStateIcon,
|
||||
CancelledStateIcon,
|
||||
CompletedStateIcon,
|
||||
StartedStateIcon,
|
||||
UnstartedStateIcon,
|
||||
} from "components/icons";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
export const getStateGroupIcon = (
|
||||
stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled",
|
||||
width = "20",
|
||||
height = "20",
|
||||
color?: string
|
||||
) => {
|
||||
switch (stateGroup) {
|
||||
case "backlog":
|
||||
return (
|
||||
<BacklogStateIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["backlog"]}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
);
|
||||
case "unstarted":
|
||||
return (
|
||||
<UnstartedStateIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["unstarted"]}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
);
|
||||
case "started":
|
||||
return (
|
||||
<StartedStateIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["started"]}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
);
|
||||
case "completed":
|
||||
return (
|
||||
<CompletedStateIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["completed"]}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
);
|
||||
case "cancelled":
|
||||
return (
|
||||
<CancelledStateIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["cancelled"]}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
24
web/components/icons/state/backlog.tsx
Normal file
24
web/components/icons/state/backlog.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
type Props = {
|
||||
width?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const StateGroupBacklogIcon: React.FC<Props> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#a3a3a3",
|
||||
}) => (
|
||||
<svg
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" stroke-dasharray="4 4" />
|
||||
</svg>
|
||||
);
|
||||
34
web/components/icons/state/cancelled.tsx
Normal file
34
web/components/icons/state/cancelled.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
type Props = {
|
||||
width?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const StateGroupCancelledIcon: React.FC<Props> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#ef4444",
|
||||
}) => (
|
||||
<svg
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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={color}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4052_100277">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
27
web/components/icons/state/completed.tsx
Normal file
27
web/components/icons/state/completed.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
type Props = {
|
||||
width?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const StateGroupCompletedIcon: React.FC<Props> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#16a34a",
|
||||
}) => (
|
||||
<svg
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.80486 9.80731L4.84856 7.85103C4.73197 7.73443 4.58542 7.67478 4.4089 7.67208C4.23238 7.66937 4.08312 7.72902 3.96113 7.85103C3.83913 7.97302 3.77814 8.12093 3.77814 8.29474C3.77814 8.46855 3.83913 8.61645 3.96113 8.73844L6.27206 11.0494C6.42428 11.2016 6.60188 11.2777 6.80486 11.2777C7.00782 11.2777 7.18541 11.2016 7.33764 11.0494L12.0227 6.36435C12.1393 6.24776 12.1989 6.10121 12.2016 5.92469C12.2043 5.74817 12.1447 5.59891 12.0227 5.47692C11.9007 5.35493 11.7528 5.29393 11.579 5.29393C11.4051 5.29393 11.2572 5.35493 11.1353 5.47692L6.80486 9.80731ZM8.00141 16C6.89494 16 5.85491 15.79 4.88132 15.3701C3.90772 14.9502 3.06082 14.3803 2.34064 13.6604C1.62044 12.9405 1.05028 12.094 0.63017 11.1208C0.210057 10.1477 0 9.10788 0 8.00141C0 6.89494 0.209966 5.85491 0.629896 4.88132C1.04983 3.90772 1.61972 3.06082 2.33958 2.34064C3.05946 1.62044 3.90598 1.05028 4.87915 0.630171C5.8523 0.210058 6.89212 0 7.99859 0C9.10506 0 10.1451 0.209966 11.1187 0.629897C12.0923 1.04983 12.9392 1.61972 13.6594 2.33959C14.3796 3.05946 14.9497 3.90598 15.3698 4.87915C15.7899 5.8523 16 6.89212 16 7.99859C16 9.10506 15.79 10.1451 15.3701 11.1187C14.9502 12.0923 14.3803 12.9392 13.6604 13.6594C12.9405 14.3796 12.094 14.9497 11.1208 15.3698C10.1477 15.7899 9.10788 16 8.00141 16ZM8 14.7369C9.88071 14.7369 11.4737 14.0842 12.779 12.779C14.0842 11.4737 14.7369 9.88071 14.7369 8C14.7369 6.11929 14.0842 4.52631 12.779 3.22104C11.4737 1.91577 9.88071 1.26314 8 1.26314C6.11929 1.26314 4.52631 1.91577 3.22104 3.22104C1.91577 4.52631 1.26314 6.11929 1.26314 8C1.26314 9.88071 1.91577 11.4737 3.22104 12.779C4.52631 14.0842 6.11929 14.7369 8 14.7369Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
6
web/components/icons/state/index.ts
Normal file
6
web/components/icons/state/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./backlog";
|
||||
export * from "./cancelled";
|
||||
export * from "./completed";
|
||||
export * from "./started";
|
||||
export * from "./state-group-icon";
|
||||
export * from "./unstarted";
|
||||
25
web/components/icons/state/started.tsx
Normal file
25
web/components/icons/state/started.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
type Props = {
|
||||
width?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const StateGroupStartedIcon: React.FC<Props> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#f59e0b",
|
||||
}) => (
|
||||
<svg
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
>
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" />
|
||||
<circle cx="6" cy="6" r="3.35" stroke={color} stroke-width="0.8" stroke-dasharray="2.4 2.4" />
|
||||
</svg>
|
||||
);
|
||||
74
web/components/icons/state/state-group-icon.tsx
Normal file
74
web/components/icons/state/state-group-icon.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// icons
|
||||
import {
|
||||
StateGroupBacklogIcon,
|
||||
StateGroupCancelledIcon,
|
||||
StateGroupCompletedIcon,
|
||||
StateGroupStartedIcon,
|
||||
StateGroupUnstartedIcon,
|
||||
} from "components/icons";
|
||||
// types
|
||||
import { TStateGroups } from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
color?: string;
|
||||
height?: string;
|
||||
stateGroup: TStateGroups;
|
||||
width?: string;
|
||||
};
|
||||
|
||||
export const StateGroupIcon: React.FC<Props> = ({
|
||||
className = "",
|
||||
color,
|
||||
height = "12px",
|
||||
width = "12px",
|
||||
stateGroup,
|
||||
}) => {
|
||||
if (stateGroup === "backlog")
|
||||
return (
|
||||
<StateGroupBacklogIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["backlog"]}
|
||||
className={`flex-shrink-0 ${className}`}
|
||||
/>
|
||||
);
|
||||
else if (stateGroup === "cancelled")
|
||||
return (
|
||||
<StateGroupCancelledIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["cancelled"]}
|
||||
className={`flex-shrink-0 ${className}`}
|
||||
/>
|
||||
);
|
||||
else if (stateGroup === "completed")
|
||||
return (
|
||||
<StateGroupCompletedIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["completed"]}
|
||||
className={`flex-shrink-0 ${className}`}
|
||||
/>
|
||||
);
|
||||
else if (stateGroup === "started")
|
||||
return (
|
||||
<StateGroupStartedIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["started"]}
|
||||
className={`flex-shrink-0 ${className}`}
|
||||
/>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<StateGroupUnstartedIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["unstarted"]}
|
||||
className={`flex-shrink-0 ${className}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
24
web/components/icons/state/unstarted.tsx
Normal file
24
web/components/icons/state/unstarted.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
type Props = {
|
||||
width?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const StateGroupUnstartedIcon: React.FC<Props> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#3a3a3a",
|
||||
}) => (
|
||||
<svg
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="8" cy="8" r="7.4" stroke={color} stroke-width="1.2" />
|
||||
</svg>
|
||||
);
|
||||
@@ -1,59 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const UnstartedStateIcon: React.FC<Props> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "rgb(var(--color-text-200))",
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 84.36 84.36"
|
||||
>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
|
||||
/>
|
||||
<path
|
||||
className="cls-1"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -3,7 +3,7 @@ import useInboxView from "hooks/use-inbox-view";
|
||||
// ui
|
||||
import { MultiLevelDropdown } from "components/ui";
|
||||
// icons
|
||||
import { getPriorityIcon } from "components/icons";
|
||||
import { PriorityIcon } from "components/icons";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
import { INBOX_STATUS } from "constants/inbox";
|
||||
@@ -42,7 +42,7 @@ export const FiltersDropdown: React.FC = () => {
|
||||
id: priority === null ? "null" : priority,
|
||||
label: (
|
||||
<div className="flex items-center gap-2 capitalize">
|
||||
{getPriorityIcon(priority)} {priority ?? "None"}
|
||||
<PriorityIcon priority={priority} /> {priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
import useInboxView from "hooks/use-inbox-view";
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon } from "components/icons";
|
||||
import { PriorityIcon } from "components/icons";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { INBOX_STATUS } from "constants/inbox";
|
||||
|
||||
@@ -48,7 +50,9 @@ export const InboxFiltersList = () => {
|
||||
: "bg-custom-background-90 text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<span>{getPriorityIcon(priority)}</span>
|
||||
<span>
|
||||
<PriorityIcon priority={priority as TIssuePriorities} />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer"
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
// ui
|
||||
import { Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { getPriorityIcon } from "components/icons";
|
||||
import { PriorityIcon } from "components/icons";
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
CheckCircleIcon,
|
||||
@@ -65,10 +65,7 @@ export const InboxIssueCard: React.FC<Props> = (props) => {
|
||||
: "border-custom-border-200"
|
||||
}`}
|
||||
>
|
||||
{getPriorityIcon(
|
||||
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
||||
"text-sm"
|
||||
)}
|
||||
<PriorityIcon priority={issue.priority ?? null} className="text-sm" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
|
||||
@@ -35,7 +35,7 @@ import type { IInboxIssue, IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
const defaultValues = {
|
||||
const defaultValues: Partial<IInboxIssue> = {
|
||||
name: "",
|
||||
description_html: "",
|
||||
estimate_point: null,
|
||||
|
||||
@@ -50,7 +50,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId, issueId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const { issueView, params } = useIssuesView();
|
||||
const { displayFilters, params } = useIssuesView();
|
||||
const { params: calendarParams } = useCalendarIssuesView();
|
||||
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||
|
||||
@@ -73,7 +73,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
|
||||
await issueServices
|
||||
.deleteIssue(workspaceSlug as string, data.project, data.id, user)
|
||||
.then(() => {
|
||||
if (issueView === "calendar") {
|
||||
if (displayFilters.layout === "calendar") {
|
||||
const calendarFetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
|
||||
: moduleId
|
||||
@@ -87,7 +87,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
|
||||
(prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
|
||||
false
|
||||
);
|
||||
} else if (issueView === "spreadsheet") {
|
||||
} else if (displayFilters.layout === "spreadsheet") {
|
||||
const spreadsheetFetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
|
||||
: moduleId
|
||||
|
||||
@@ -52,7 +52,7 @@ const defaultValues: Partial<IIssue> = {
|
||||
estimate_point: null,
|
||||
state: "",
|
||||
parent: null,
|
||||
priority: null,
|
||||
priority: "none",
|
||||
assignees: [],
|
||||
assignees_list: [],
|
||||
labels: [],
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
// ui
|
||||
import { Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { findTotalDaysInRange, renderShortDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
@@ -52,7 +52,7 @@ export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
|
||||
className="relative w-full flex items-center gap-2 h-full cursor-pointer"
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)}
|
||||
>
|
||||
{getStateGroupIcon(data?.state_detail?.group, "14", "14", data?.state_detail?.color)}
|
||||
<StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
|
||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||
{data?.project_detail?.identifier} {data?.sequence_id}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export const IssueGanttChartView = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { orderBy } = useIssuesView();
|
||||
const { displayFilters } = useIssuesView();
|
||||
|
||||
const { user } = useUser();
|
||||
const { projectDetails } = useProjectDetails();
|
||||
@@ -43,7 +43,7 @@ export const IssueGanttChartView = () => {
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={orderBy === "sort_order" && isAllowed}
|
||||
enableReorder={displayFilters.order_by === "sort_order" && isAllowed}
|
||||
bottomSpacing
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId, inboxId } = router.query;
|
||||
|
||||
const { issueView, params } = useIssuesView();
|
||||
const { displayFilters, params } = useIssuesView();
|
||||
const { params: calendarParams } = useCalendarIssuesView();
|
||||
const { order_by, group_by, ...viewGanttParams } = params;
|
||||
const { params: inboxParams } = useInboxView();
|
||||
@@ -247,13 +247,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
if (payload.module && payload.module !== "")
|
||||
await addIssueToModule(res.id, payload.module);
|
||||
|
||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
||||
if (issueView === "gantt_chart")
|
||||
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||
if (displayFilters.layout === "gantt_chart")
|
||||
mutate(ganttFetchKey, {
|
||||
start_target_date: true,
|
||||
order_by: "sort_order",
|
||||
});
|
||||
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||
if (groupedIssues) mutateMyIssues();
|
||||
|
||||
setToastAlert({
|
||||
@@ -285,8 +285,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
if (isUpdatingSingleIssue) {
|
||||
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
||||
} else {
|
||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
||||
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
|
||||
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ import { DateFilterModal } from "components/core";
|
||||
// ui
|
||||
import { MultiLevelDropdown } from "components/ui";
|
||||
// icons
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IQuery } from "types";
|
||||
import { IIssueFilterOptions, IQuery, TStateGroups } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
// constants
|
||||
@@ -83,7 +83,7 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
||||
id: priority === null ? "null" : priority,
|
||||
label: (
|
||||
<div className="flex items-center gap-2 capitalize">
|
||||
{getPriorityIcon(priority)} {priority ?? "None"}
|
||||
<PriorityIcon priority={priority} /> {priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
@@ -104,7 +104,7 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
||||
id: key,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
{getStateGroupIcon(key as any, "16", "16")}{" "}
|
||||
<StateGroupIcon stateGroup={key as TStateGroups} />
|
||||
{GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]}
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -37,20 +37,8 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const {
|
||||
issueView,
|
||||
setIssueView,
|
||||
groupBy,
|
||||
setGroupBy,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
properties,
|
||||
setProperty,
|
||||
filters,
|
||||
setFilters,
|
||||
} = useMyIssuesFilters(workspaceSlug?.toString());
|
||||
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } =
|
||||
useMyIssuesFilters(workspaceSlug?.toString());
|
||||
|
||||
const { isEstimateActive } = useEstimateOption();
|
||||
|
||||
@@ -68,11 +56,11 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
issueView === option.type
|
||||
displayFilters?.layout === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setIssueView(option.type)}
|
||||
onClick={() => setDisplayFilters({ layout: option.type })}
|
||||
>
|
||||
<option.Icon
|
||||
sx={{
|
||||
@@ -139,81 +127,89 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||
<div className="relative divide-y-2 divide-custom-border-200">
|
||||
<div className="space-y-4 pb-3 text-xs">
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
groupBy === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find((option) => option.key === groupBy)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (
|
||||
option.key === "state" ||
|
||||
option.key === "created_by" ||
|
||||
option.key === "assignees"
|
||||
)
|
||||
return null;
|
||||
{displayFilters?.layout !== "calendar" &&
|
||||
displayFilters?.layout !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
displayFilters?.group_by === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find(
|
||||
(option) => option.key === displayFilters?.group_by
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (displayFilters?.layout === "kanban" && option.key === null)
|
||||
return null;
|
||||
if (
|
||||
option.key === "state" ||
|
||||
option.key === "created_by" ||
|
||||
option.key === "assignees"
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupBy(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setDisplayFilters({ group_by: option.key })}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) => {
|
||||
if (groupBy === "priority" && option.key === "priority")
|
||||
return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
<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) => {
|
||||
if (
|
||||
displayFilters?.group_by === "priority" &&
|
||||
option.key === "priority"
|
||||
)
|
||||
return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
return (
|
||||
<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={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
|
||||
?.name ?? "Select"
|
||||
FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters?.type
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
@@ -222,7 +218,7 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
setDisplayFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
@@ -234,16 +230,24 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
|
||||
{displayFilters?.layout !== "calendar" &&
|
||||
displayFilters?.layout !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={displayFilters?.show_empty_groups ?? true}
|
||||
onChange={() =>
|
||||
setDisplayFilters({
|
||||
show_empty_groups: !displayFilters?.show_empty_groups,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 py-3">
|
||||
@@ -253,7 +257,7 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
if (
|
||||
issueView === "spreadsheet" &&
|
||||
displayFilters?.layout === "spreadsheet" &&
|
||||
(key === "attachment_count" ||
|
||||
key === "link" ||
|
||||
key === "sub_issue_count")
|
||||
@@ -261,7 +265,7 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
return null;
|
||||
|
||||
if (
|
||||
issueView !== "spreadsheet" &&
|
||||
displayFilters?.layout !== "spreadsheet" &&
|
||||
(key === "created_on" || key === "updated_on")
|
||||
)
|
||||
return null;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue, IIssueFilterOptions } from "types";
|
||||
import { IIssue, IIssueFilterOptions, TIssuePriorities } from "types";
|
||||
// fetch-keys
|
||||
import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
@@ -57,8 +57,9 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { groupedIssues, mutateMyIssues, isEmpty, params } = useMyIssues(workspaceSlug?.toString());
|
||||
const { filters, setFilters, issueView, groupBy, orderBy, properties, showEmptyGroups } =
|
||||
useMyIssuesFilters(workspaceSlug?.toString());
|
||||
const { filters, setFilters, displayFilters, setDisplayFilters, properties } = useMyIssuesFilters(
|
||||
workspaceSlug?.toString()
|
||||
);
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug && (filters?.labels ?? []).length > 0
|
||||
@@ -81,7 +82,13 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
async (result: DropResult) => {
|
||||
setTrashBox(false);
|
||||
|
||||
if (!result.destination || !workspaceSlug || !groupedIssues || groupBy !== "priority") return;
|
||||
if (
|
||||
!result.destination ||
|
||||
!workspaceSlug ||
|
||||
!groupedIssues ||
|
||||
displayFilters?.group_by !== "priority"
|
||||
)
|
||||
return;
|
||||
|
||||
const { source, destination } = result;
|
||||
|
||||
@@ -96,7 +103,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
const sourceGroup = source.droppableId;
|
||||
const destinationGroup = destination.droppableId;
|
||||
|
||||
draggedItem[groupBy] = destinationGroup;
|
||||
draggedItem[displayFilters.group_by] = destinationGroup as TIssuePriorities;
|
||||
|
||||
mutate<{
|
||||
[key: string]: IIssue[];
|
||||
@@ -113,8 +120,14 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
[sourceGroup]: orderArrayBy(sourceGroupArray, orderBy),
|
||||
[destinationGroup]: orderArrayBy(destinationGroupArray, orderBy),
|
||||
[sourceGroup]: orderArrayBy(
|
||||
sourceGroupArray,
|
||||
displayFilters.order_by ?? "-created_at"
|
||||
),
|
||||
[destinationGroup]: orderArrayBy(
|
||||
destinationGroupArray,
|
||||
displayFilters.order_by ?? "-created_at"
|
||||
),
|
||||
};
|
||||
},
|
||||
false
|
||||
@@ -134,7 +147,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
.catch(() => mutate(USER_ISSUES(workspaceSlug.toString(), params)));
|
||||
}
|
||||
},
|
||||
[groupBy, groupedIssues, handleDeleteIssue, orderBy, params, user, workspaceSlug]
|
||||
[displayFilters, groupedIssues, handleDeleteIssue, params, user, workspaceSlug]
|
||||
);
|
||||
|
||||
const addIssueToGroup = useCallback(
|
||||
@@ -143,19 +156,19 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
|
||||
let preloadedValue: string | string[] = groupTitle;
|
||||
|
||||
if (groupBy === "labels") {
|
||||
if (displayFilters?.group_by === "labels") {
|
||||
if (groupTitle === "None") preloadedValue = [];
|
||||
else preloadedValue = [groupTitle];
|
||||
}
|
||||
|
||||
if (groupBy)
|
||||
if (displayFilters?.group_by)
|
||||
setPreloadedData({
|
||||
[groupBy]: preloadedValue,
|
||||
[displayFilters?.group_by]: preloadedValue,
|
||||
actionType: "createIssue",
|
||||
});
|
||||
else setPreloadedData({ actionType: "createIssue" });
|
||||
},
|
||||
[setCreateIssueModal, setPreloadedData, groupBy]
|
||||
[setCreateIssueModal, setPreloadedData, displayFilters?.group_by]
|
||||
);
|
||||
|
||||
const addIssueToDate = useCallback(
|
||||
@@ -263,7 +276,6 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
state_group: null,
|
||||
start_date: null,
|
||||
target_date: null,
|
||||
type: null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -275,7 +287,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
addIssueToDate={addIssueToDate}
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
disableUserActions={disableUserActions}
|
||||
dragDisabled={groupBy !== "priority"}
|
||||
dragDisabled={displayFilters?.group_by !== "priority"}
|
||||
emptyState={{
|
||||
title: filters.assignees
|
||||
? "You don't have any issue assigned to you yet"
|
||||
@@ -304,15 +316,12 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
trashBox={trashBox}
|
||||
setTrashBox={setTrashBox}
|
||||
viewProps={{
|
||||
groupByProperty: groupBy,
|
||||
displayFilters,
|
||||
groupedIssues,
|
||||
isEmpty,
|
||||
issueView,
|
||||
mutateIssues: mutateMyIssues,
|
||||
orderBy,
|
||||
params,
|
||||
properties,
|
||||
showEmptyGroups,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUser from "hooks/use-user";
|
||||
@@ -19,7 +19,7 @@ import { CustomDatePicker, Icon } from "components/ui";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IIssue, TIssuePriorities } from "types";
|
||||
|
||||
type Props = {
|
||||
handleDeleteIssue: () => void;
|
||||
@@ -66,7 +66,10 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
|
||||
{mode === "full" && (
|
||||
<div className="flex justify-between gap-2 pb-3">
|
||||
<h6 className="flex items-center gap-2 font-medium">
|
||||
{getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)}
|
||||
<StateGroupIcon
|
||||
stateGroup={issue.state_detail.group}
|
||||
color={issue.state_detail.color}
|
||||
/>
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</h6>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -114,7 +117,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
|
||||
<div className="w-3/4">
|
||||
<SidebarPrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={(val: string) => handleUpdateIssue({ priority: val })}
|
||||
onChange={(val) => handleUpdateIssue({ priority: val })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,14 @@ import React from "react";
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// icons
|
||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||
import { PriorityIcon } from "components/icons/priority-icon";
|
||||
// types
|
||||
import { TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
|
||||
type Props = {
|
||||
value: string | null;
|
||||
value: TIssuePriorities;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
@@ -18,7 +20,10 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
|
||||
label={
|
||||
<div className="flex items-center justify-center gap-2 text-xs">
|
||||
<span className="flex items-center">
|
||||
{getPriorityIcon(value, `text-xs ${value ? "" : "text-custom-text-200"}`)}
|
||||
<PriorityIcon
|
||||
priority={value}
|
||||
className={`text-xs ${value ? "" : "text-custom-text-200"}`}
|
||||
/>
|
||||
</span>
|
||||
<span className={`${value ? "" : "text-custom-text-200"} capitalize`}>
|
||||
{value ?? "Priority"}
|
||||
@@ -32,8 +37,10 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
|
||||
<CustomSelect.Option key={priority} value={priority}>
|
||||
<div className="flex w-full justify-between gap-2 rounded">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<span>{getPriorityIcon(priority)}</span>
|
||||
<span className="capitalize">{priority ?? "None"}</span>
|
||||
<span>
|
||||
<PriorityIcon priority={priority} />
|
||||
</span>
|
||||
<span className="capitalize">{priority}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CustomSelect.Option>
|
||||
|
||||
@@ -10,7 +10,7 @@ import stateService from "services/state.service";
|
||||
import { CustomSearchSelect } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// fetch keys
|
||||
@@ -41,7 +41,7 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
|
||||
query: state.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
@@ -58,9 +58,12 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedOption ? (
|
||||
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
|
||||
<StateGroupIcon stateGroup={selectedOption.group} color={selectedOption.color} />
|
||||
) : currentDefaultState ? (
|
||||
getStateGroupIcon(currentDefaultState.group, "16", "16", currentDefaultState.color)
|
||||
<StateGroupIcon
|
||||
stateGroup={currentDefaultState.group}
|
||||
color={currentDefaultState.color}
|
||||
/>
|
||||
) : (
|
||||
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
)}
|
||||
|
||||
@@ -3,13 +3,15 @@ import React from "react";
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// icons
|
||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||
import { PriorityIcon } from "components/icons/priority-icon";
|
||||
// types
|
||||
import { TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
|
||||
type Props = {
|
||||
value: string | null;
|
||||
onChange: (val: string) => void;
|
||||
value: TIssuePriorities;
|
||||
onChange: (val: TIssuePriorities) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
@@ -31,7 +33,7 @@ export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabl
|
||||
}`}
|
||||
>
|
||||
<span className="grid place-items-center -my-1">
|
||||
{getPriorityIcon(value ?? "None", "!text-sm")}
|
||||
<PriorityIcon priority={value} className="!text-sm" />
|
||||
</span>
|
||||
<span>{value ?? "None"}</span>
|
||||
</button>
|
||||
@@ -44,7 +46,7 @@ export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabl
|
||||
{PRIORITIES.map((option) => (
|
||||
<CustomSelect.Option key={option} value={option} className="capitalize">
|
||||
<>
|
||||
{getPriorityIcon(option, "text-sm")}
|
||||
<PriorityIcon priority={option} className="text-sm" />
|
||||
{option ?? "None"}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
|
||||
@@ -9,7 +9,7 @@ import stateService from "services/state.service";
|
||||
// ui
|
||||
import { Spinner, CustomSelect } from "components/ui";
|
||||
// icons
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
@@ -42,17 +42,12 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled
|
||||
<button type="button" className="bg-custom-background-80 text-xs rounded px-2.5 py-0.5">
|
||||
{selectedState ? (
|
||||
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
|
||||
{getStateGroupIcon(
|
||||
selectedState?.group ?? "backlog",
|
||||
"14",
|
||||
"14",
|
||||
selectedState?.color ?? ""
|
||||
)}
|
||||
<StateGroupIcon stateGroup={selectedState.group} color={selectedState.color} />
|
||||
{addSpaceIfCamelCase(selectedState?.name ?? "")}
|
||||
</div>
|
||||
) : inboxIssueId ? (
|
||||
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
|
||||
{getStateGroupIcon("backlog", "14", "14", "#ff7700")}
|
||||
<StateGroupIcon stateGroup="backlog" color="#ff7700" />
|
||||
Triage
|
||||
</div>
|
||||
) : (
|
||||
@@ -71,7 +66,7 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled
|
||||
states.map((state) => (
|
||||
<CustomSelect.Option key={state.id} value={state.id}>
|
||||
<>
|
||||
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
{state.name}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
|
||||
@@ -401,7 +401,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
render={({ field: { value } }) => (
|
||||
<SidebarPrioritySelect
|
||||
value={value}
|
||||
onChange={(val: string) => submitChanges({ priority: val })}
|
||||
onChange={(val) => submitChanges({ priority: val })}
|
||||
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||
/>
|
||||
)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user