[WEB-1255] chore: Refactor existing Space app for project publish (#5107)
* chore: paginated the issues in space app * chore: storing query using filters * chore: added filters for priority * chore: issue view model save function * chore: votes and reactions added in issues endpoint * chore: added filters in the public endpoint * chore: issue detail endpoint * chore: added labels, modules and assignees * refactor existing project publish in space app * fix clear all filters in space App * chore: removed the extra serialier * remove optional chaining and fallback to an empty array --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
22671ec8a7
commit
08d9e95a86
73 changed files with 2245 additions and 651 deletions
|
|
@ -14,6 +14,7 @@
|
|||
"@headlessui/react": "^1.7.19",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/constants": "*",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
|
|
@ -46,4 +47,4 @@
|
|||
"tsconfig": "*",
|
||||
"typescript": "^5.4.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ class IssueViewSerializer(DynamicBaseSerializer):
|
|||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
query_params = validated_data.get("query_data", {})
|
||||
query_params = validated_data.get("filters", {})
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
|
|
@ -31,7 +31,7 @@ class IssueViewSerializer(DynamicBaseSerializer):
|
|||
return IssueView.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
query_params = validated_data.get("query_data", {})
|
||||
query_params = validated_data.get("filters", {})
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1391,6 +1391,7 @@ def create_issue_relation_activity(
|
|||
workspace_id=workspace_id,
|
||||
comment=f"added {requested_data.get('relation_type')} relation",
|
||||
old_identifier=related_issue,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from django.db import models
|
|||
from .base import BaseModel
|
||||
from .project import ProjectBaseModel
|
||||
from .workspace import WorkspaceBaseModel
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
def get_default_filters():
|
||||
|
|
@ -116,6 +117,26 @@ class IssueView(WorkspaceBaseModel):
|
|||
db_table = "issue_views"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
query_params = self.filters
|
||||
self.query = (
|
||||
issue_filters(query_params, "POST") if query_params else {}
|
||||
)
|
||||
|
||||
if self._state.adding:
|
||||
if self.project:
|
||||
largest_sort_order = IssueView.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
else:
|
||||
largest_sort_order = IssueView.objects.filter(
|
||||
workspace=self.workspace, project__isnull=True
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
|
||||
super(IssueView, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the View"""
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
from .user import UserLiteSerializer
|
||||
|
||||
from .issue import LabelLiteSerializer, StateLiteSerializer
|
||||
from .issue import (
|
||||
LabelLiteSerializer,
|
||||
StateLiteSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
|
|
|
|||
|
|
@ -188,11 +188,16 @@ class IssueAttachmentSerializer(BaseSerializer):
|
|||
|
||||
|
||||
class IssueReactionSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = "__all__"
|
||||
fields = [
|
||||
"issue",
|
||||
"reaction",
|
||||
"workspace",
|
||||
"project",
|
||||
"actor",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
|
|
@ -454,20 +459,6 @@ class IssueCreateSerializer(BaseSerializer):
|
|||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class IssueReactionSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"actor",
|
||||
]
|
||||
|
||||
|
||||
class CommentReactionSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = CommentReaction
|
||||
|
|
@ -476,7 +467,6 @@ class CommentReactionSerializer(BaseSerializer):
|
|||
|
||||
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueVote
|
||||
|
|
@ -486,35 +476,45 @@ class IssueVoteSerializer(BaseSerializer):
|
|||
"workspace",
|
||||
"project",
|
||||
"actor",
|
||||
"actor_detail",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class IssuePublicSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
reactions = IssueReactionSerializer(
|
||||
read_only=True, many=True, source="issue_reactions"
|
||||
)
|
||||
votes = IssueVoteSerializer(read_only=True, many=True)
|
||||
module_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
label_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
assignee_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"description_html",
|
||||
"sequence_id",
|
||||
"state",
|
||||
"state_detail",
|
||||
"project",
|
||||
"project_detail",
|
||||
"workspace",
|
||||
"priority",
|
||||
"target_date",
|
||||
"reactions",
|
||||
"votes",
|
||||
"module_ids",
|
||||
"created_by",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ from plane.space.views import (
|
|||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
WorkspaceProjectAnchorEndpoint,
|
||||
ProjectCyclesEndpoint,
|
||||
ProjectModulesEndpoint,
|
||||
ProjectStatesEndpoint,
|
||||
ProjectLabelsEndpoint,
|
||||
ProjectMembersEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -23,4 +28,29 @@ urlpatterns = [
|
|||
WorkspaceProjectAnchorEndpoint.as_view(),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/cycles/",
|
||||
ProjectCyclesEndpoint.as_view(),
|
||||
name="project-cycles",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/modules/",
|
||||
ProjectModulesEndpoint.as_view(),
|
||||
name="project-modules",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/states/",
|
||||
ProjectStatesEndpoint.as_view(),
|
||||
name="project-states",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/labels/",
|
||||
ProjectLabelsEndpoint.as_view(),
|
||||
name="project-labels",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/members/",
|
||||
ProjectMembersEndpoint.as_view(),
|
||||
name="project-members",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
248
apiserver/plane/space/utils/grouper.py
Normal file
248
apiserver/plane/space/utils/grouper.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import Q, UUIDField, Value, F, Case, When, JSONField
|
||||
from django.db.models.functions import Coalesce, JSONObject
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
Issue,
|
||||
Label,
|
||||
Module,
|
||||
Project,
|
||||
ProjectMember,
|
||||
State,
|
||||
WorkspaceMember,
|
||||
)
|
||||
|
||||
def issue_queryset_grouper(queryset, group_by, sub_group_by):
|
||||
|
||||
FIELD_MAPPER = {
|
||||
"label_ids": "labels__id",
|
||||
"assignee_ids": "assignees__id",
|
||||
"module_ids": "issue_module__module_id",
|
||||
}
|
||||
|
||||
annotations_map = {
|
||||
"assignee_ids": ("assignees__id", ~Q(assignees__id__isnull=True)),
|
||||
"label_ids": ("labels__id", ~Q(labels__id__isnull=True)),
|
||||
"module_ids": (
|
||||
"issue_module__module_id",
|
||||
~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
}
|
||||
default_annotations = {
|
||||
key: Coalesce(
|
||||
ArrayAgg(
|
||||
field,
|
||||
distinct=True,
|
||||
filter=condition,
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
for key, (field, condition) in annotations_map.items()
|
||||
if FIELD_MAPPER.get(key) != group_by
|
||||
or FIELD_MAPPER.get(key) != sub_group_by
|
||||
}
|
||||
|
||||
return queryset.annotate(**default_annotations)
|
||||
|
||||
|
||||
def issue_on_results(issues, group_by, sub_group_by):
|
||||
|
||||
FIELD_MAPPER = {
|
||||
"labels__id": "label_ids",
|
||||
"assignees__id": "assignee_ids",
|
||||
"issue_module__module_id": "module_ids",
|
||||
}
|
||||
|
||||
original_list = ["assignee_ids", "label_ids", "module_ids"]
|
||||
|
||||
required_fields = [
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"created_by",
|
||||
"state__group",
|
||||
]
|
||||
|
||||
if group_by in FIELD_MAPPER:
|
||||
original_list.remove(FIELD_MAPPER[group_by])
|
||||
original_list.append(group_by)
|
||||
|
||||
if sub_group_by in FIELD_MAPPER:
|
||||
original_list.remove(FIELD_MAPPER[sub_group_by])
|
||||
original_list.append(sub_group_by)
|
||||
|
||||
required_fields.extend(original_list)
|
||||
|
||||
issues = issues.annotate(
|
||||
vote_items=ArrayAgg(
|
||||
Case(
|
||||
When(
|
||||
votes__isnull=False,
|
||||
then=JSONObject(
|
||||
vote=F("votes__vote"),
|
||||
actor_details=JSONObject(
|
||||
id=F("votes__actor__id"),
|
||||
first_name=F("votes__actor__first_name"),
|
||||
last_name=F("votes__actor__last_name"),
|
||||
avatar=F("votes__actor__avatar"),
|
||||
display_name=F("votes__actor__display_name"),
|
||||
)
|
||||
),
|
||||
),
|
||||
default=None,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Case(
|
||||
When(votes__isnull=False, then=True),
|
||||
default=False,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
reaction_items=ArrayAgg(
|
||||
Case(
|
||||
When(
|
||||
issue_reactions__isnull=False,
|
||||
then=JSONObject(
|
||||
reaction=F("issue_reactions__reaction"),
|
||||
actor_details=JSONObject(
|
||||
id=F("issue_reactions__actor__id"),
|
||||
first_name=F("issue_reactions__actor__first_name"),
|
||||
last_name=F("issue_reactions__actor__last_name"),
|
||||
avatar=F("issue_reactions__actor__avatar"),
|
||||
display_name=F("issue_reactions__actor__display_name"),
|
||||
),
|
||||
),
|
||||
),
|
||||
default=None,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Case(
|
||||
When(issue_reactions__isnull=False, then=True),
|
||||
default=False,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
).values(*required_fields, "vote_items", "reaction_items")
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def issue_group_values(field, slug, project_id=None, filters=dict):
|
||||
if field == "state_id":
|
||||
queryset = State.objects.filter(
|
||||
is_triage=False,
|
||||
workspace__slug=slug,
|
||||
).values_list("id", flat=True)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
if field == "labels__id":
|
||||
queryset = Label.objects.filter(workspace__slug=slug).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id)) + ["None"]
|
||||
else:
|
||||
return list(queryset) + ["None"]
|
||||
if field == "assignees__id":
|
||||
if project_id:
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).values_list("member_id", flat=True)
|
||||
else:
|
||||
return list(
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, is_active=True
|
||||
).values_list("member_id", flat=True)
|
||||
)
|
||||
if field == "issue_module__module_id":
|
||||
queryset = Module.objects.filter(
|
||||
workspace__slug=slug,
|
||||
).values_list("id", flat=True)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id)) + ["None"]
|
||||
else:
|
||||
return list(queryset) + ["None"]
|
||||
if field == "cycle_id":
|
||||
queryset = Cycle.objects.filter(
|
||||
workspace__slug=slug,
|
||||
).values_list("id", flat=True)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id)) + ["None"]
|
||||
else:
|
||||
return list(queryset) + ["None"]
|
||||
if field == "project_id":
|
||||
queryset = Project.objects.filter(workspace__slug=slug).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
return list(queryset)
|
||||
if field == "priority":
|
||||
return [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"urgent",
|
||||
"none",
|
||||
]
|
||||
if field == "state__group":
|
||||
return [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
if field == "target_date":
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
.filter(**filters)
|
||||
.values_list("target_date", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
if field == "start_date":
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
.filter(**filters)
|
||||
.values_list("start_date", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
|
||||
if field == "created_by":
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
.filter(**filters)
|
||||
.values_list("created_by", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
|
||||
return []
|
||||
|
|
@ -2,6 +2,7 @@ from .project import (
|
|||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
WorkspaceProjectAnchorEndpoint,
|
||||
ProjectMembersEndpoint,
|
||||
)
|
||||
|
||||
from .issue import (
|
||||
|
|
@ -14,3 +15,11 @@ from .issue import (
|
|||
)
|
||||
|
||||
from .inbox import InboxIssuePublicViewSet
|
||||
|
||||
from .cycle import ProjectCyclesEndpoint
|
||||
|
||||
from .module import ProjectModulesEndpoint
|
||||
|
||||
from .state import ProjectStatesEndpoint
|
||||
|
||||
from .label import ProjectLabelsEndpoint
|
||||
|
|
|
|||
35
apiserver/plane/space/views/cycle.py
Normal file
35
apiserver/plane/space/views/cycle.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Third Party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
DeployBoard,
|
||||
Cycle,
|
||||
)
|
||||
|
||||
|
||||
class ProjectCyclesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(anchor=anchor).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Invalid anchor"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
cycles = Cycle.objects.filter(
|
||||
workspace__slug=deploy_board.workspace.slug,
|
||||
project_id=deploy_board.project_id,
|
||||
).values("id", "name")
|
||||
|
||||
return Response(
|
||||
cycles,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
# Python imports
|
||||
import json
|
||||
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models.functions import Coalesce, JSONObject
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import (
|
||||
Exists,
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Q,
|
||||
Prefetch,
|
||||
UUIDField,
|
||||
Case,
|
||||
When,
|
||||
CharField,
|
||||
IntegerField,
|
||||
JSONField,
|
||||
Value,
|
||||
Max,
|
||||
)
|
||||
|
||||
# Django imports
|
||||
|
|
@ -25,6 +24,22 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
|
|||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView, BaseViewSet
|
||||
|
||||
# fetch the space app grouper function separately
|
||||
from plane.space.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
|
||||
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
CommentReactionSerializer,
|
||||
IssueCommentSerializer,
|
||||
|
|
@ -36,21 +51,160 @@ from plane.db.models import (
|
|||
Issue,
|
||||
IssueComment,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
ProjectMember,
|
||||
IssueReaction,
|
||||
ProjectMember,
|
||||
CommentReaction,
|
||||
DeployBoard,
|
||||
IssueVote,
|
||||
ProjectPublicMember,
|
||||
State,
|
||||
Label,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView, BaseViewSet
|
||||
|
||||
class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
deploy_board = DeployBoard.objects.filter(
|
||||
anchor=anchor, entity_name="project"
|
||||
).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Project is not published"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
project_id = deploy_board.entity_identifier
|
||||
slug = deploy_board.workspace.slug
|
||||
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"votes",
|
||||
queryset=IssueVote.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
).distinct()
|
||||
|
||||
issue_queryset = issue_queryset.filter(**filters)
|
||||
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class IssueCommentPublicViewSet(BaseViewSet):
|
||||
|
|
@ -503,67 +657,50 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
|||
]
|
||||
|
||||
def get(self, request, anchor, issue_id):
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
anchor=anchor, entity_name="project"
|
||||
)
|
||||
issue = Issue.objects.get(
|
||||
workspace_id=project_deploy_board.workspace_id,
|
||||
project_id=project_deploy_board.project_id,
|
||||
pk=issue_id,
|
||||
)
|
||||
serializer = IssuePublicSerializer(issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(
|
||||
anchor=anchor, entity_name="project"
|
||||
).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Project is not published"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
project_id = deploy_board.entity_identifier
|
||||
slug = deploy_board.workspace.slug
|
||||
deploy_board = DeployBoard.objects.get(anchor=anchor)
|
||||
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
Issue.issue_objects.filter(
|
||||
pk=issue_id,
|
||||
workspace__slug=deploy_board.workspace.slug,
|
||||
project_id=deploy_board.project_id,
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project", "workspace", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
queryset=IssueReaction.objects.select_related(
|
||||
"issue", "actor"
|
||||
),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
|
|
@ -572,124 +709,91 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
|||
queryset=IssueVote.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order
|
||||
if order_by_param == "priority"
|
||||
else priority_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
*[
|
||||
When(priority=p, then=Value(i))
|
||||
for i, p in enumerate(priority_order)
|
||||
],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
||||
# State Ordering
|
||||
elif order_by_param in [
|
||||
"state__name",
|
||||
"state__group",
|
||||
"-state__name",
|
||||
"-state__group",
|
||||
]:
|
||||
state_order = (
|
||||
state_order
|
||||
if order_by_param in ["state__name", "state__group"]
|
||||
else state_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[
|
||||
When(state__group=state_group, then=Value(i))
|
||||
for i, state_group in enumerate(state_order)
|
||||
],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("state_order")
|
||||
# assignee and label ordering
|
||||
elif order_by_param in [
|
||||
"labels__name",
|
||||
"-labels__name",
|
||||
"assignees__first_name",
|
||||
"-assignees__first_name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
max_values=Max(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values"
|
||||
if order_by_param.startswith("-")
|
||||
else "max_values"
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssuePublicSerializer(issue_queryset, many=True).data
|
||||
|
||||
state_group_order = [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
states = (
|
||||
State.objects.filter(
|
||||
~Q(name="Triage"),
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(
|
||||
custom_order=Case(
|
||||
*[
|
||||
When(group=value, then=Value(index))
|
||||
for index, value in enumerate(state_group_order)
|
||||
],
|
||||
default=Value(len(state_group_order)),
|
||||
output_field=IntegerField(),
|
||||
vote_items=ArrayAgg(
|
||||
Case(
|
||||
When(
|
||||
votes__isnull=False,
|
||||
then=JSONObject(
|
||||
vote=F("votes__vote"),
|
||||
actor_details=JSONObject(
|
||||
id=F("votes__actor__id"),
|
||||
first_name=F("votes__actor__first_name"),
|
||||
last_name=F("votes__actor__last_name"),
|
||||
avatar=F("votes__actor__avatar"),
|
||||
display_name=F(
|
||||
"votes__actor__display_name"
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
default=None,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Case(
|
||||
When(votes__isnull=False, then=True),
|
||||
default=False,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
reaction_items=ArrayAgg(
|
||||
Case(
|
||||
When(
|
||||
issue_reactions__isnull=False,
|
||||
then=JSONObject(
|
||||
reaction=F("issue_reactions__reaction"),
|
||||
actor_details=JSONObject(
|
||||
id=F("issue_reactions__actor__id"),
|
||||
first_name=F(
|
||||
"issue_reactions__actor__first_name"
|
||||
),
|
||||
last_name=F(
|
||||
"issue_reactions__actor__last_name"
|
||||
),
|
||||
avatar=F("issue_reactions__actor__avatar"),
|
||||
display_name=F(
|
||||
"issue_reactions__actor__display_name"
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
default=None,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Case(
|
||||
When(issue_reactions__isnull=False, then=True),
|
||||
default=False,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
)
|
||||
.values("name", "group", "color", "id")
|
||||
.order_by("custom_order", "sequence")
|
||||
)
|
||||
.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"description",
|
||||
"description_html",
|
||||
"description_stripped",
|
||||
"description_binary",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"created_by",
|
||||
"state__group",
|
||||
"vote_items",
|
||||
"reaction_items",
|
||||
)
|
||||
).first()
|
||||
|
||||
labels = Label.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).values("id", "name", "color", "parent")
|
||||
|
||||
return Response(
|
||||
{
|
||||
"issues": issues,
|
||||
"states": states,
|
||||
"labels": labels,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(issue_queryset, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
35
apiserver/plane/space/views/label.py
Normal file
35
apiserver/plane/space/views/label.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
DeployBoard,
|
||||
Label,
|
||||
)
|
||||
|
||||
|
||||
class ProjectLabelsEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(anchor=anchor).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Invalid anchor"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
labels = Label.objects.filter(
|
||||
workspace__slug=deploy_board.workspace.slug,
|
||||
project_id=deploy_board.project_id,
|
||||
).values("id", "name", "color", "parent")
|
||||
|
||||
return Response(
|
||||
labels,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
35
apiserver/plane/space/views/module.py
Normal file
35
apiserver/plane/space/views/module.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Third Party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
DeployBoard,
|
||||
Module,
|
||||
)
|
||||
|
||||
|
||||
class ProjectModulesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(anchor=anchor).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Invalid anchor"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
modules = Module.objects.filter(
|
||||
workspace__slug=deploy_board.workspace.slug,
|
||||
project_id=deploy_board.project_id,
|
||||
).values("id", "name")
|
||||
|
||||
return Response(
|
||||
modules,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
|
@ -12,10 +12,7 @@ from rest_framework.permissions import AllowAny
|
|||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.app.serializers import DeployBoardSerializer
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
DeployBoard,
|
||||
)
|
||||
from plane.db.models import Project, DeployBoard, ProjectMember
|
||||
|
||||
|
||||
class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
|
||||
|
|
@ -76,3 +73,27 @@ class WorkspaceProjectAnchorEndpoint(BaseAPIView):
|
|||
)
|
||||
serializer = DeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ProjectMembersEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(anchor=anchor).first()
|
||||
|
||||
members = ProjectMember.objects.filter(
|
||||
project=deploy_board.project,
|
||||
workspace=deploy_board.workspace,
|
||||
is_active=True,
|
||||
).values(
|
||||
"id",
|
||||
"member",
|
||||
"member__first_name",
|
||||
"member__last_name",
|
||||
"member__display_name",
|
||||
"project",
|
||||
"workspace",
|
||||
)
|
||||
return Response(members, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
42
apiserver/plane/space/views/state.py
Normal file
42
apiserver/plane/space/views/state.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Django imports
|
||||
from django.db.models import Q
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
DeployBoard,
|
||||
State,
|
||||
)
|
||||
|
||||
|
||||
class ProjectStatesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(anchor=anchor).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Invalid anchor"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
states = (
|
||||
State.objects.filter(
|
||||
~Q(name="Triage"),
|
||||
workspace__slug=deploy_board.workspace.slug,
|
||||
project_id=deploy_board.project_id,
|
||||
)
|
||||
.values("name", "group", "color", "id")
|
||||
)
|
||||
|
||||
return Response(
|
||||
states,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
|
@ -154,6 +154,13 @@ def filter_priority(params, filter, method, prefix=""):
|
|||
]
|
||||
if len(priorities) and "" not in priorities:
|
||||
filter[f"{prefix}priority__in"] = priorities
|
||||
else:
|
||||
if (
|
||||
params.get("priority", None)
|
||||
and len(params.get("priority"))
|
||||
and params.get("priority") != "null"
|
||||
):
|
||||
filter[f"{prefix}priority__in"] = params.get("priority")
|
||||
return filter
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from "./auth";
|
||||
export * from "./issue";
|
||||
27
packages/constants/issue.ts
Normal file
27
packages/constants/issue.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
export const ALL_ISSUES = "All Issues";
|
||||
|
||||
export enum EIssueGroupByToServerOptions {
|
||||
"state" = "state_id",
|
||||
"priority" = "priority",
|
||||
"labels" = "labels__id",
|
||||
"state_detail.group" = "state__group",
|
||||
"assignees" = "assignees__id",
|
||||
"cycle" = "cycle_id",
|
||||
"module" = "issue_module__module_id",
|
||||
"target_date" = "target_date",
|
||||
"project" = "project_id",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
|
||||
export enum EServerGroupByToFilterOptions {
|
||||
"state_id" = "state",
|
||||
"priority" = "priority",
|
||||
"labels__id" = "labels",
|
||||
"state__group" = "state_group",
|
||||
"assignees__id" = "assignees",
|
||||
"cycle_id" = "cycle",
|
||||
"issue_module__module_id" = "module",
|
||||
"target_date" = "target_date",
|
||||
"project_id" = "project",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { IssuesLayoutsRoot } from "@/components/issues";
|
||||
// hooks
|
||||
import { usePublish } from "@/hooks/store";
|
||||
import { usePublish, useLabel, useStates } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
params: {
|
||||
|
|
@ -19,6 +20,12 @@ const IssuesPage = observer((props: Props) => {
|
|||
// params
|
||||
const searchParams = useSearchParams();
|
||||
const peekId = searchParams.get("peekId") || undefined;
|
||||
// store
|
||||
const { fetchStates } = useStates();
|
||||
const { fetchLabels } = useLabel();
|
||||
|
||||
useSWR(anchor ? `PUBLIC_STATES_${anchor}` : null, anchor ? () => fetchStates(anchor) : null);
|
||||
useSWR(anchor ? `PUBLIC_LABELS_${anchor}` : null, anchor ? () => fetchLabels(anchor) : null);
|
||||
|
||||
const publishSettings = usePublish(anchor);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// types
|
||||
import { IStateLite } from "@plane/types";
|
||||
import { IIssueLabel, TFilters } from "@/types/issue";
|
||||
import { TFilters } from "@/types/issue";
|
||||
// components
|
||||
import { AppliedPriorityFilters } from "./priority";
|
||||
import { AppliedStateFilters } from "./state";
|
||||
|
|
@ -13,14 +12,12 @@ type Props = {
|
|||
appliedFilters: TFilters;
|
||||
handleRemoveAllFilters: () => void;
|
||||
handleRemoveFilter: (key: keyof TFilters, value: string | null) => void;
|
||||
labels?: IIssueLabel[] | undefined;
|
||||
states?: IStateLite[] | undefined;
|
||||
};
|
||||
|
||||
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
|
||||
|
||||
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter, states } = props;
|
||||
const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch gap-2">
|
||||
|
|
@ -52,10 +49,9 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
|||
/>
|
||||
)} */}
|
||||
|
||||
{filterKey === "state" && states && (
|
||||
{filterKey === "state" && (
|
||||
<AppliedStateFilters
|
||||
handleRemove={(val) => handleRemoveFilter("state", val)}
|
||||
states={states}
|
||||
values={filterValue ?? []}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import cloneDeep from "lodash/cloneDeep";
|
|||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
// hooks
|
||||
import { useIssue, useIssueFilter } from "@/hooks/store";
|
||||
import { useIssueFilter } from "@/hooks/store";
|
||||
// store
|
||||
import { TIssueQueryFilters } from "@/types/issue";
|
||||
// components
|
||||
|
|
@ -21,7 +21,6 @@ export const IssueAppliedFilters: FC<TIssueAppliedFilters> = observer((props) =>
|
|||
const router = useRouter();
|
||||
// store hooks
|
||||
const { getIssueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter();
|
||||
const { states, labels } = useIssue();
|
||||
// derived values
|
||||
const issueFilters = getIssueFilters(anchor);
|
||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||
|
|
@ -65,14 +64,18 @@ export const IssueAppliedFilters: FC<TIssueAppliedFilters> = observer((props) =>
|
|||
);
|
||||
|
||||
const handleRemoveAllFilters = () => {
|
||||
initIssueFilters(anchor, {
|
||||
display_filters: { layout: activeLayout || "list" },
|
||||
filters: {
|
||||
state: [],
|
||||
priority: [],
|
||||
labels: [],
|
||||
initIssueFilters(
|
||||
anchor,
|
||||
{
|
||||
display_filters: { layout: activeLayout || "list" },
|
||||
filters: {
|
||||
state: [],
|
||||
priority: [],
|
||||
labels: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
true
|
||||
);
|
||||
|
||||
router.push(`/issues/${anchor}?${`board=${activeLayout || "list"}`}`);
|
||||
};
|
||||
|
|
@ -85,8 +88,6 @@ export const IssueAppliedFilters: FC<TIssueAppliedFilters> = observer((props) =>
|
|||
appliedFilters={appliedFilters || {}}
|
||||
handleRemoveFilter={handleFilters as any}
|
||||
handleRemoveAllFilters={handleRemoveAllFilters}
|
||||
labels={labels ?? []}
|
||||
states={states ?? []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,19 +2,20 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// types
|
||||
import { IStateLite } from "@plane/types";
|
||||
// ui
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useStates } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
states: IStateLite[];
|
||||
values: string[];
|
||||
};
|
||||
|
||||
export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, states, values } = props;
|
||||
const { handleRemove, values } = props;
|
||||
|
||||
const { states } = useStates();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
|||
// helpers
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssue, useIssueFilter } from "@/hooks/store";
|
||||
import { useIssueFilter } from "@/hooks/store";
|
||||
// types
|
||||
import { TIssueQueryFilters } from "@/types/issue";
|
||||
|
||||
|
|
@ -26,7 +26,6 @@ export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((pro
|
|||
const router = useRouter();
|
||||
// hooks
|
||||
const { getIssueFilters, updateIssueFilters } = useIssueFilter();
|
||||
const { states, labels } = useIssue();
|
||||
// derived values
|
||||
const issueFilters = getIssueFilters(anchor);
|
||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||
|
|
@ -65,8 +64,6 @@ export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((pro
|
|||
filters={issueFilters?.filters ?? {}}
|
||||
handleFilters={handleFilters as any}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[activeLayout]?.filters : []}
|
||||
states={states ?? undefined}
|
||||
labels={labels ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import React, { useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { Search, X } from "lucide-react";
|
||||
// types
|
||||
import { IStateLite } from "@plane/types";
|
||||
import { IIssueLabel, IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
|
||||
import { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
|
||||
// components
|
||||
import { FilterPriority, FilterState } from ".";
|
||||
|
||||
|
|
@ -13,12 +12,10 @@ type Props = {
|
|||
filters: IIssueFilterOptions;
|
||||
handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
layoutDisplayFiltersOptions: TIssueFilterKeys[];
|
||||
labels?: IIssueLabel[] | undefined;
|
||||
states?: IStateLite[] | undefined;
|
||||
};
|
||||
|
||||
export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
const { filters, handleFilters, layoutDisplayFiltersOptions, states } = props;
|
||||
const { filters, handleFilters, layoutDisplayFiltersOptions } = props;
|
||||
|
||||
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
||||
|
||||
|
|
@ -63,7 +60,6 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
|||
appliedFilters={filters.state ?? null}
|
||||
handleUpdate={(val) => handleFilters("state", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
states={states}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
// types
|
||||
import { IStateLite } from "@plane/types";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { Loader, StateGroupIcon } from "@plane/ui";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers";
|
||||
// hooks
|
||||
import { useStates } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
searchQuery: string;
|
||||
states: IStateLite[] | undefined;
|
||||
};
|
||||
|
||||
export const FilterState: React.FC<Props> = (props) => {
|
||||
const { appliedFilters, handleUpdate, searchQuery, states } = props;
|
||||
export const FilterState: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||
|
||||
const { states } = useStates();
|
||||
|
||||
const [itemsToRender, setItemsToRender] = useState(5);
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
|
@ -77,4 +79,4 @@ export const FilterState: React.FC<Props> = (props) => {
|
|||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,18 +9,17 @@ import { IssueBlockDueDate, IssueBlockPriority, IssueBlockState } from "@/compon
|
|||
// helpers
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, usePublish } from "@/hooks/store";
|
||||
import { useIssue, useIssueDetails, usePublish } from "@/hooks/store";
|
||||
// interfaces
|
||||
import { IIssue } from "@/types/issue";
|
||||
|
||||
type Props = {
|
||||
anchor: string;
|
||||
issue: IIssue;
|
||||
params: any;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const IssueKanBanBlock: FC<Props> = observer((props) => {
|
||||
const { anchor, issue } = props;
|
||||
const { anchor, issueId } = props;
|
||||
const { getIssueById } = useIssue();
|
||||
const searchParams = useSearchParams();
|
||||
// query params
|
||||
const board = searchParams.get("board");
|
||||
|
|
@ -31,12 +30,16 @@ export const IssueKanBanBlock: FC<Props> = observer((props) => {
|
|||
const { project_details } = usePublish(anchor);
|
||||
const { setPeekId } = useIssueDetails();
|
||||
|
||||
const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels });
|
||||
const { queryParam } = queryParamGenerator({ board, peekId: issueId, priority, state, labels });
|
||||
|
||||
const handleBlockClick = () => {
|
||||
setPeekId(issue.id);
|
||||
setPeekId(issueId);
|
||||
};
|
||||
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return <></>;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/issues/${anchor}?${queryParam}`}
|
||||
|
|
@ -61,15 +64,15 @@ export const IssueKanBanBlock: FC<Props> = observer((props) => {
|
|||
</div>
|
||||
)}
|
||||
{/* state */}
|
||||
{issue?.state_detail && (
|
||||
{issue?.state_id && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockState state={issue?.state_detail} />
|
||||
<IssueBlockState stateId={issue?.state_id} />
|
||||
</div>
|
||||
)}
|
||||
{/* due date */}
|
||||
{issue?.target_date && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockDueDate due_date={issue?.target_date} group={issue?.state_detail?.group} />
|
||||
<IssueBlockDueDate due_date={issue?.target_date} stateId={issue?.state_id ?? undefined} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
70
space/core/components/issues/issue-layouts/kanban/column.tsx
Normal file
70
space/core/components/issues/issue-layouts/kanban/column.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { Icon } from "@/components/ui";
|
||||
// hooks
|
||||
import { useIssue } from "@/hooks/store";
|
||||
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
||||
// components
|
||||
import { IssueKanBanBlock } from "./block";
|
||||
import { IssueKanBanHeader } from "./header";
|
||||
|
||||
type Props = {
|
||||
anchor: string;
|
||||
stateId: string;
|
||||
issueIds: string[];
|
||||
};
|
||||
|
||||
export const Column = observer((props: Props) => {
|
||||
const { anchor, stateId, issueIds } = props;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { fetchNextPublicIssues, getPaginationData, getIssueLoader, getGroupIssueCount } = useIssue();
|
||||
|
||||
const loadMoreIssuesInThisGroup = useCallback(() => {
|
||||
fetchNextPublicIssues(anchor, stateId);
|
||||
}, [fetchNextPublicIssues, anchor, stateId]);
|
||||
|
||||
const isPaginating = !!getIssueLoader(stateId);
|
||||
const nextPageResults = getPaginationData(stateId, undefined)?.nextPageResults;
|
||||
|
||||
useIntersectionObserver(
|
||||
containerRef,
|
||||
isPaginating ? null : intersectionElement,
|
||||
loadMoreIssuesInThisGroup,
|
||||
`0% 100% 100% 100%`
|
||||
);
|
||||
|
||||
const groupIssueCount = getGroupIssueCount(stateId, undefined, false);
|
||||
const shouldLoadMore =
|
||||
nextPageResults === undefined && groupIssueCount !== undefined
|
||||
? issueIds?.length < groupIssueCount
|
||||
: !!nextPageResults;
|
||||
|
||||
return (
|
||||
<div key={stateId} className="relative flex h-full w-[340px] flex-shrink-0 flex-col">
|
||||
<div className="flex-shrink-0">
|
||||
<IssueKanBanHeader stateId={stateId} />
|
||||
</div>
|
||||
<div className="hide-vertical-scrollbar h-full w-full overflow-hidden overflow-y-auto" ref={containerRef}>
|
||||
{issueIds && issueIds.length > 0 ? (
|
||||
<div className="space-y-3 px-2 pb-2">
|
||||
{issueIds.map((issueId) => (
|
||||
<IssueKanBanBlock key={issueId} anchor={anchor} issueId={issueId} />
|
||||
))}
|
||||
{shouldLoadMore && (
|
||||
<div className="w-full h-[100px] bg-custom-background-80 animate-pulse" ref={setIntersectionElement} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2 pt-10 text-center text-sm font-medium text-custom-text-200">
|
||||
<Icon iconName="stack" />
|
||||
No issues in this state
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,25 +1,32 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { IStateLite } from "@plane/types";
|
||||
// ui
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssue, useStates } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
state: IStateLite;
|
||||
stateId: string;
|
||||
};
|
||||
|
||||
export const IssueKanBanHeader: React.FC<Props> = observer((props) => {
|
||||
const { state } = props;
|
||||
const { stateId } = props;
|
||||
|
||||
const { getStateById } = useStates();
|
||||
const { getGroupIssueCount } = useIssue();
|
||||
|
||||
const state = getStateById(stateId);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-2 pb-2">
|
||||
<div className="flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="14" width="14" />
|
||||
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} height="14" width="14" />
|
||||
</div>
|
||||
<div className="mr-1 truncate font-medium capitalize text-custom-text-200">{state?.name}</div>
|
||||
{/* <span className="flex-shrink-0 rounded-full text-custom-text-300">{getCountOfIssuesByState(state.id)}</span> */}
|
||||
<div className="mr-1 truncate font-medium capitalize text-custom-text-200">{state?.name ?? "State"}</div>
|
||||
<span className="flex-shrink-0 rounded-full text-custom-text-300">
|
||||
{getGroupIssueCount(stateId, undefined, false) ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@
|
|||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { IssueKanBanBlock, IssueKanBanHeader } from "@/components/issues";
|
||||
// ui
|
||||
import { Icon } from "@/components/ui";
|
||||
// mobx hook
|
||||
import { TGroupedIssues } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssue } from "@/hooks/store";
|
||||
import { Column } from "./column";
|
||||
|
||||
type Props = {
|
||||
anchor: string;
|
||||
|
|
@ -16,34 +15,19 @@ type Props = {
|
|||
export const IssueKanbanLayoutRoot: FC<Props> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
// store hooks
|
||||
const { states, getFilteredIssuesByState } = useIssue();
|
||||
const { groupedIssueIds } = useIssue();
|
||||
|
||||
const groupedIssues = groupedIssueIds as TGroupedIssues | undefined;
|
||||
|
||||
if (!groupedIssues) return <></>;
|
||||
|
||||
const issueGroupIds = Object.keys(groupedIssues);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full gap-3 overflow-hidden overflow-x-auto">
|
||||
{states?.map((state) => {
|
||||
const issues = getFilteredIssuesByState(state.id);
|
||||
|
||||
return (
|
||||
<div key={state.id} className="relative flex h-full w-[340px] flex-shrink-0 flex-col">
|
||||
<div className="flex-shrink-0">
|
||||
<IssueKanBanHeader state={state} />
|
||||
</div>
|
||||
<div className="hide-vertical-scrollbar h-full w-full overflow-hidden overflow-y-auto">
|
||||
{issues && issues.length > 0 ? (
|
||||
<div className="space-y-3 px-2 pb-2">
|
||||
{issues.map((issue) => (
|
||||
<IssueKanBanBlock key={issue.id} anchor={anchor} issue={issue} params={{}} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2 pt-10 text-center text-sm font-medium text-custom-text-200">
|
||||
<Icon iconName="stack" />
|
||||
No issues in this state
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{issueGroupIds?.map((stateId) => {
|
||||
const issueIds = groupedIssues[stateId];
|
||||
return <Column key={stateId} anchor={anchor} stateId={stateId} issueIds={issueIds} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,17 +8,16 @@ import { IssueBlockDueDate, IssueBlockLabels, IssueBlockPriority, IssueBlockStat
|
|||
// helpers
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hook
|
||||
import { useIssueDetails, usePublish } from "@/hooks/store";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
import { useIssue, useIssueDetails, usePublish } from "@/hooks/store";
|
||||
|
||||
type IssueListBlockProps = {
|
||||
anchor: string;
|
||||
issue: IIssue;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const IssueListLayoutBlock: FC<IssueListBlockProps> = observer((props) => {
|
||||
const { anchor, issue } = props;
|
||||
const { anchor, issueId } = props;
|
||||
const { getIssueById } = useIssue();
|
||||
// query params
|
||||
const searchParams = useSearchParams();
|
||||
const board = searchParams.get("board") || undefined;
|
||||
|
|
@ -29,11 +28,15 @@ export const IssueListLayoutBlock: FC<IssueListBlockProps> = observer((props) =>
|
|||
const { setPeekId } = useIssueDetails();
|
||||
const { project_details } = usePublish(anchor);
|
||||
|
||||
const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels });
|
||||
const { queryParam } = queryParamGenerator({ board, peekId: issueId, priority, state, labels });
|
||||
const handleBlockClick = () => {
|
||||
setPeekId(issue.id);
|
||||
setPeekId(issueId);
|
||||
};
|
||||
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return <></>;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/issues/${anchor}?${queryParam}`}
|
||||
|
|
@ -60,23 +63,23 @@ export const IssueListLayoutBlock: FC<IssueListBlockProps> = observer((props) =>
|
|||
)}
|
||||
|
||||
{/* state */}
|
||||
{issue?.state_detail && (
|
||||
{issue?.state_id && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockState state={issue?.state_detail} />
|
||||
<IssueBlockState stateId={issue?.state_id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* labels */}
|
||||
{issue?.label_details && issue?.label_details.length > 0 && (
|
||||
{issue?.label_ids && issue?.label_ids.length > 0 && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockLabels labels={issue?.label_details} />
|
||||
<IssueBlockLabels labelIds={issue?.label_ids} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* due date */}
|
||||
{issue?.target_date && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockDueDate due_date={issue?.target_date} group={issue?.state_detail?.group} />
|
||||
<IssueBlockDueDate due_date={issue?.target_date} stateId={issue?.state_id ?? undefined} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
59
space/core/components/issues/issue-layouts/list/group.tsx
Normal file
59
space/core/components/issues/issue-layouts/list/group.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useIssue } from "@/hooks/store";
|
||||
// components
|
||||
import { IssueListLayoutBlock } from "./block";
|
||||
import { IssueListLayoutHeader } from "./header";
|
||||
|
||||
type Props = {
|
||||
anchor: string;
|
||||
stateId: string;
|
||||
issueIds: string[];
|
||||
};
|
||||
|
||||
export const Group = observer((props: Props) => {
|
||||
const { anchor, stateId, issueIds } = props;
|
||||
|
||||
const { fetchNextPublicIssues, getPaginationData, getIssueLoader, getGroupIssueCount } = useIssue();
|
||||
|
||||
const loadMoreIssuesInThisGroup = useCallback(() => {
|
||||
fetchNextPublicIssues(anchor, stateId);
|
||||
}, [stateId]);
|
||||
|
||||
const isPaginating = !!getIssueLoader(stateId);
|
||||
const nextPageResults = getPaginationData(stateId, undefined)?.nextPageResults;
|
||||
|
||||
const groupIssueCount = getGroupIssueCount(stateId, undefined, false);
|
||||
const shouldLoadMore =
|
||||
nextPageResults === undefined && groupIssueCount !== undefined
|
||||
? issueIds?.length < groupIssueCount
|
||||
: !!nextPageResults;
|
||||
|
||||
return (
|
||||
<div key={stateId} className="relative w-full">
|
||||
<IssueListLayoutHeader stateId={stateId} />
|
||||
{issueIds && issueIds.length > 0 ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{issueIds.map((issueId) => (
|
||||
<IssueListLayoutBlock key={issueId} anchor={anchor} issueId={issueId} />
|
||||
))}
|
||||
{isPaginating ? (
|
||||
<div className="w-full h-[46px] bg-custom-background-80 animate-pulse" />
|
||||
) : (
|
||||
shouldLoadMore && (
|
||||
<div
|
||||
className="w-full min-h-[45px] bg-custom-background-100 p-3 text-sm border-b-[1px] cursor-pointer text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={loadMoreIssuesInThisGroup}
|
||||
>
|
||||
Load More ↓
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-custom-background-100 p-3 text-sm text-custom-text-400">No issues.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -2,25 +2,32 @@
|
|||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { IStateLite } from "@plane/types";
|
||||
// ui
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssue, useStates } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
state: IStateLite;
|
||||
stateId: string;
|
||||
};
|
||||
|
||||
export const IssueListLayoutHeader: React.FC<Props> = observer((props) => {
|
||||
const { state } = props;
|
||||
const { stateId } = props;
|
||||
|
||||
const { getStateById } = useStates();
|
||||
const { getGroupIssueCount } = useIssue();
|
||||
|
||||
const state = getStateById(stateId);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
<div className="flex sticky top-0 items-center gap-2 p-3 bg-custom-background-90 z-[1] border-b-[1px] border-custom-border-200">
|
||||
<div className="flex h-3.5 w-3.5 items-center justify-center">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="14" width="14" />
|
||||
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} height="14" width="14" />
|
||||
</div>
|
||||
<div className="mr-1 font-medium capitalize">{state?.name}</div>
|
||||
{/* <div className="text-sm font-medium text-custom-text-200">{count}</div> */}
|
||||
<div className="text-sm font-medium text-custom-text-200">
|
||||
{getGroupIssueCount(stateId, undefined, false) ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
"use client";
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { IssueListLayoutBlock, IssueListLayoutHeader } from "@/components/issues";
|
||||
// types
|
||||
import { TGroupedIssues } from "@plane/types";
|
||||
// mobx hook
|
||||
import { useIssue } from "@/hooks/store";
|
||||
import { Group } from "./group";
|
||||
|
||||
type Props = {
|
||||
anchor: string;
|
||||
|
|
@ -13,27 +14,20 @@ type Props = {
|
|||
export const IssuesListLayoutRoot: FC<Props> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
// store hooks
|
||||
const { states, getFilteredIssuesByState } = useIssue();
|
||||
const { groupedIssueIds } = useIssue();
|
||||
|
||||
const groupedIssues = groupedIssueIds as TGroupedIssues | undefined;
|
||||
|
||||
if (!groupedIssues) return <></>;
|
||||
|
||||
const issueGroupIds = Object.keys(groupedIssues);
|
||||
|
||||
return (
|
||||
<>
|
||||
{states?.map((state) => {
|
||||
const issues = getFilteredIssuesByState(state.id);
|
||||
{issueGroupIds?.map((stateId) => {
|
||||
const issueIds = groupedIssues[stateId];
|
||||
|
||||
return (
|
||||
<div key={state.id} className="relative w-full">
|
||||
<IssueListLayoutHeader state={state} />
|
||||
{issues && issues.length > 0 ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{issues.map((issue) => (
|
||||
<IssueListLayoutBlock key={issue.id} anchor={anchor} issue={issue} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-custom-background-100 p-3 text-sm text-custom-text-400">No issues.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return <Group key={stateId} anchor={anchor} stateId={stateId} issueIds={issueIds} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,27 +1,31 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { CalendarCheck2 } from "lucide-react";
|
||||
// types
|
||||
import { TStateGroups } from "@plane/types";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useStates } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
due_date: string;
|
||||
group: TStateGroups;
|
||||
stateId: string | undefined;
|
||||
};
|
||||
|
||||
export const IssueBlockDueDate = (props: Props) => {
|
||||
const { due_date, group } = props;
|
||||
export const IssueBlockDueDate = observer((props: Props) => {
|
||||
const { due_date, stateId } = props;
|
||||
const { getStateById } = useStates();
|
||||
|
||||
const state = getStateById(stateId);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs text-custom-text-100",
|
||||
{
|
||||
"text-red-500": shouldHighlightIssueDueDate(due_date, group),
|
||||
"text-red-500": shouldHighlightIssueDueDate(due_date, state?.group),
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
|
@ -29,4 +33,4 @@ export const IssueBlockDueDate = (props: Props) => {
|
|||
{renderFormattedDate(due_date)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,41 @@
|
|||
"use client";
|
||||
|
||||
export const IssueBlockLabels = ({ labels }: any) => (
|
||||
<div className="relative flex flex-wrap items-center gap-1">
|
||||
{labels?.map((_label: any) => (
|
||||
<div
|
||||
key={_label?.id}
|
||||
className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: `${_label?.color}` }} />
|
||||
<div className="text-xs">{_label?.name}</div>
|
||||
import { observer } from "mobx-react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { useLabel } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
labelIds: string[];
|
||||
};
|
||||
|
||||
export const IssueBlockLabels = observer(({ labelIds }: Props) => {
|
||||
const { getLabelsByIds } = useLabel();
|
||||
|
||||
const labels = getLabelsByIds(labelIds);
|
||||
|
||||
const labelsString = labels.map((label) => label.name).join(", ");
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-wrap items-center gap-1">
|
||||
{labels.length === 1 ? (
|
||||
<div
|
||||
key={labels[0].id}
|
||||
className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: `${labels[0].color}` }} />
|
||||
<div className="text-xs">{labels[0].name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
) : (
|
||||
<Tooltip tooltipContent={labelsString}>
|
||||
<div className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<div className="text-xs">{labels.length} Labels</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,27 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
//hooks
|
||||
import { useStates } from "@/hooks/store";
|
||||
|
||||
export const IssueBlockState = ({ state }: any) => (
|
||||
<div className="flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs shadow-sm duration-300 focus:outline-none">
|
||||
<div className="flex w-full items-center gap-1.5">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
<div className="text-xs">{state?.name}</div>
|
||||
type Props = {
|
||||
stateId: string;
|
||||
};
|
||||
export const IssueBlockState = observer(({ stateId }: Props) => {
|
||||
const { getStateById } = useStates();
|
||||
|
||||
const state = getStateById(stateId);
|
||||
|
||||
if (!state) return <></>;
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs shadow-sm duration-300 focus:outline-none">
|
||||
<div className="flex w-full items-center gap-1.5">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
<div className="text-xs">{state?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { FC, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { IssueKanbanLayoutRoot, IssuesListLayoutRoot } from "@/components/issues";
|
||||
|
|
@ -23,22 +22,22 @@ type Props = {
|
|||
|
||||
export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
|
||||
const { peekId, publishSettings } = props;
|
||||
// query params
|
||||
const searchParams = useSearchParams();
|
||||
const states = searchParams.get("states") || undefined;
|
||||
const priority = searchParams.get("priority") || undefined;
|
||||
const labels = searchParams.get("labels") || undefined;
|
||||
// store hooks
|
||||
const { getIssueFilters } = useIssueFilter();
|
||||
const { loader, issues, error, fetchPublicIssues } = useIssue();
|
||||
const { loader, groupedIssueIds, fetchPublicIssues } = useIssue();
|
||||
const issueDetailStore = useIssueDetails();
|
||||
// derived values
|
||||
const { anchor } = publishSettings;
|
||||
const issueFilters = anchor ? getIssueFilters(anchor) : undefined;
|
||||
// derived values
|
||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||
|
||||
useSWR(
|
||||
const { error } = useSWR(
|
||||
anchor ? `PUBLIC_ISSUES_${anchor}` : null,
|
||||
anchor ? () => fetchPublicIssues(anchor, { states, priority, labels }) : null
|
||||
anchor
|
||||
? () => fetchPublicIssues(anchor, "init-loader", { groupedBy: "state", canGroup: true, perPageCount: 50 })
|
||||
: null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -47,16 +46,13 @@ export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
|
|||
}
|
||||
}, [peekId, issueDetailStore]);
|
||||
|
||||
// derived values
|
||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||
|
||||
if (!anchor) return null;
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
{peekId && <IssuePeekOverview anchor={anchor} peekId={peekId} />}
|
||||
|
||||
{loader && !issues ? (
|
||||
{loader && !groupedIssueIds ? (
|
||||
<div className="py-10 text-center text-sm text-custom-text-100">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor";
|
||||
import { IssueReactions } from "@/components/issues/peek-overview";
|
||||
import { usePublish } from "@/hooks/store";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
|
||||
|
|
@ -9,15 +11,17 @@ type Props = {
|
|||
issueDetails: IIssue;
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueDetails: React.FC<Props> = (props) => {
|
||||
export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => {
|
||||
const { anchor, issueDetails } = props;
|
||||
|
||||
const { project_details } = usePublish(anchor);
|
||||
|
||||
const description = issueDetails.description_html;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h6 className="text-base font-medium text-custom-text-400">
|
||||
{issueDetails.project_detail?.identifier}-{issueDetails?.sequence_id}
|
||||
{project_details?.identifier}-{issueDetails?.sequence_id}
|
||||
</h6>
|
||||
<h4 className="break-words text-2xl font-medium">{issueDetails.name}</h4>
|
||||
{description !== "" && description !== "<p></p>" && (
|
||||
|
|
@ -34,4 +38,4 @@ export const PeekOverviewIssueDetails: React.FC<Props> = (props) => {
|
|||
<IssueReactions anchor={anchor} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
|||
const { data: user } = useUser();
|
||||
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
|
||||
const reactions = issueDetailsStore.details[issueId ?? ""]?.reaction_items ?? [];
|
||||
const groupedReactions = groupReactions(reactions, "reaction");
|
||||
|
||||
const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);
|
||||
const userReactions = reactions.filter((r) => r.actor_details?.id === user?.id);
|
||||
|
||||
const handleAddReaction = (reactionHex: string) => {
|
||||
if (!issueId) return;
|
||||
|
|
@ -48,7 +48,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
|||
};
|
||||
|
||||
const handleReactionClick = (reactionHex: string) => {
|
||||
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||
const userReaction = userReactions?.find((r) => r.actor_details?.id === user?.id && r.reaction === reactionHex);
|
||||
if (userReaction) handleRemoveReaction(reactionHex);
|
||||
else handleAddReaction(reactionHex);
|
||||
};
|
||||
|
|
@ -78,9 +78,9 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
|||
tooltipContent={
|
||||
<div>
|
||||
{reactions
|
||||
.map((r) => r.actor_detail.display_name)
|
||||
.splice(0, REACTIONS_LIMIT)
|
||||
.join(", ")}
|
||||
?.map((r) => r?.actor_details?.display_name)
|
||||
?.splice(0, REACTIONS_LIMIT)
|
||||
?.join(", ")}
|
||||
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
|||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
reactions?.some((r) => r.actor_detail.id === user?.id && r.reaction === reaction)
|
||||
reactions.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
|
|
@ -100,7 +100,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
|||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={
|
||||
reactions?.some((r) => r.actor_detail.id === user?.id && r.reaction === reaction)
|
||||
reactions.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction)
|
||||
? "text-custom-primary-100"
|
||||
: ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { CalendarCheck2, Signal } from "lucide-react";
|
||||
// ui
|
||||
import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
|
@ -12,6 +14,8 @@ import { cn } from "@/helpers/common.helper";
|
|||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { usePublish, useStates } from "@/hooks/store";
|
||||
// types
|
||||
import { IIssue, IPeekMode } from "@/types/issue";
|
||||
|
||||
|
|
@ -20,8 +24,13 @@ type Props = {
|
|||
mode?: IPeekMode;
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mode }) => {
|
||||
const state = issueDetails.state_detail;
|
||||
export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDetails, mode }) => {
|
||||
const { getStateById } = useStates();
|
||||
const state = getStateById(issueDetails?.state_id ?? undefined);
|
||||
|
||||
const { anchor } = useParams();
|
||||
|
||||
const { project_details } = usePublish(anchor?.toString());
|
||||
|
||||
const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null;
|
||||
|
||||
|
|
@ -42,7 +51,7 @@ 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">
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||
{project_details?.identifier}-{issueDetails.sequence_id}
|
||||
</h6>
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
||||
|
|
@ -58,7 +67,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
|||
<span>State</span>
|
||||
</div>
|
||||
<div className="w-3/4 flex items-center gap-1.5 py-0.5 text-sm">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} />
|
||||
{addSpaceIfCamelCase(state?.name ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -101,10 +110,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
|||
{issueDetails.target_date ? (
|
||||
<div
|
||||
className={cn("flex items-center gap-1.5 rounded py-0.5 text-xs text-custom-text-100", {
|
||||
"text-red-500": shouldHighlightIssueDueDate(
|
||||
issueDetails.target_date,
|
||||
issueDetails.state_detail.group
|
||||
),
|
||||
"text-red-500": shouldHighlightIssueDueDate(issueDetails.target_date, state?.group),
|
||||
})}
|
||||
>
|
||||
<CalendarCheck2 className="size-3" />
|
||||
|
|
@ -118,4 +124,4 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,20 +37,20 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
|||
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
|
||||
const votes = issueId ? issueDetailsStore.details[issueId]?.votes : [];
|
||||
const votes = issueDetailsStore.details[issueId ?? ""]?.vote_items ?? [];
|
||||
|
||||
const allUpVotes = votes?.filter((vote) => vote.vote === 1);
|
||||
const allDownVotes = votes?.filter((vote) => vote.vote === -1);
|
||||
const allUpVotes = votes.filter((vote) => vote.vote === 1);
|
||||
const allDownVotes = votes.filter((vote) => vote.vote === -1);
|
||||
|
||||
const isUpVotedByUser = allUpVotes?.some((vote) => vote.actor === user?.id);
|
||||
const isDownVotedByUser = allDownVotes?.some((vote) => vote.actor === user?.id);
|
||||
const isUpVotedByUser = allUpVotes.some((vote) => vote.actor_details?.id === user?.id);
|
||||
const isDownVotedByUser = allDownVotes.some((vote) => vote.actor_details?.id === user?.id);
|
||||
|
||||
const handleVote = async (e: any, voteValue: 1 | -1) => {
|
||||
if (!issueId) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const actionPerformed = votes?.find((vote) => vote.actor === user?.id && vote.vote === voteValue);
|
||||
const actionPerformed = votes?.find((vote) => vote.actor_details?.id === user?.id && vote.vote === voteValue);
|
||||
|
||||
if (actionPerformed) await issueDetailsStore.removeIssueVote(anchor, issueId);
|
||||
else {
|
||||
|
|
@ -76,7 +76,7 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
|||
{allUpVotes.length > 0 ? (
|
||||
<>
|
||||
{allUpVotes
|
||||
.map((r) => r.actor_detail.display_name)
|
||||
.map((r) => r.actor_details?.display_name)
|
||||
.splice(0, VOTES_LIMIT)
|
||||
.join(", ")}
|
||||
{allUpVotes.length > VOTES_LIMIT && " and " + (allUpVotes.length - VOTES_LIMIT) + " more"}
|
||||
|
|
@ -116,7 +116,7 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
|||
{allDownVotes.length > 0 ? (
|
||||
<>
|
||||
{allDownVotes
|
||||
.map((r) => r.actor_detail.display_name)
|
||||
.map((r) => r.actor_details.display_name)
|
||||
.splice(0, VOTES_LIMIT)
|
||||
.join(", ")}
|
||||
{allDownVotes.length > VOTES_LIMIT && " and " + (allDownVotes.length - VOTES_LIMIT) + " more"}
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ export const IssuePeekOverview: FC<TIssuePeekOverview> = observer((props) => {
|
|||
const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (anchor && peekId && issueStore.issues && issueStore.issues.length > 0) {
|
||||
if (anchor && peekId && issueStore.groupedIssueIds) {
|
||||
if (!issueDetails) {
|
||||
issueDetailStore.fetchIssueDetails(anchor, peekId.toString());
|
||||
}
|
||||
}
|
||||
}, [anchor, issueDetailStore, issueDetails, peekId, issueStore.issues]);
|
||||
}, [anchor, issueDetailStore, issueDetails, peekId, issueStore.groupedIssueIds]);
|
||||
|
||||
const handleClose = () => {
|
||||
issueDetailStore.setPeekId(null);
|
||||
|
|
|
|||
|
|
@ -75,4 +75,4 @@ export const issuePriorityFilter = (priorityKey: TIssuePriorities): TIssueFilter
|
|||
|
||||
if (currentIssuePriority) return currentIssuePriority;
|
||||
return undefined;
|
||||
};
|
||||
};
|
||||
|
|
@ -5,3 +5,5 @@ export * from "./use-user";
|
|||
export * from "./use-user-profile";
|
||||
export * from "./use-issue-details";
|
||||
export * from "./use-issue-filter";
|
||||
export * from "./use-state";
|
||||
export * from "./use-label";
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ import { IIssueStore } from "@/store/issue.store";
|
|||
|
||||
export const useIssue = (): IIssueStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
|
||||
if (context === undefined) throw new Error("useIssue must be used within StoreProvider");
|
||||
return context.issue;
|
||||
};
|
||||
|
|
|
|||
11
space/core/hooks/store/use-label.ts
Normal file
11
space/core/hooks/store/use-label.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { useContext } from "react";
|
||||
// lib
|
||||
import { StoreContext } from "@/lib/store-provider";
|
||||
// store
|
||||
import { IIssueLabelStore } from "@/store/label.store";
|
||||
|
||||
export const useLabel = (): IIssueLabelStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useLabel must be used within StoreProvider");
|
||||
return context.label;
|
||||
};
|
||||
11
space/core/hooks/store/use-state.ts
Normal file
11
space/core/hooks/store/use-state.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { useContext } from "react";
|
||||
// lib
|
||||
import { StoreContext } from "@/lib/store-provider";
|
||||
// store
|
||||
import { IStateStore } from "@/store/state.store";
|
||||
|
||||
export const useStates = (): IStateStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useState must be used within StoreProvider");
|
||||
return context.state;
|
||||
};
|
||||
|
|
@ -6,6 +6,7 @@ const useClipboardWritePermission = () => {
|
|||
useEffect(() => {
|
||||
const checkClipboardWriteAccess = () => {
|
||||
navigator.permissions
|
||||
//eslint-disable-next-line no-undef
|
||||
.query({ name: "clipboard-write" as PermissionName })
|
||||
.then((result) => {
|
||||
if (result.state === "granted") {
|
||||
|
|
|
|||
41
space/core/hooks/use-intersection-observer.tsx
Normal file
41
space/core/hooks/use-intersection-observer.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { RefObject, useEffect } from "react";
|
||||
|
||||
export type UseIntersectionObserverProps = {
|
||||
containerRef: RefObject<HTMLDivElement | null> | undefined;
|
||||
elementRef: HTMLElement | null;
|
||||
callback: () => void;
|
||||
rootMargin?: string;
|
||||
};
|
||||
|
||||
export const useIntersectionObserver = (
|
||||
containerRef: RefObject<HTMLDivElement | null>,
|
||||
elementRef: HTMLElement | null,
|
||||
callback: (() => void) | undefined,
|
||||
rootMargin?: string
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (elementRef) {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[entries.length - 1].isIntersecting) {
|
||||
callback && callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
root: containerRef?.current,
|
||||
rootMargin,
|
||||
}
|
||||
);
|
||||
observer.observe(elementRef);
|
||||
return () => {
|
||||
if (elementRef) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
observer.unobserve(elementRef);
|
||||
}
|
||||
};
|
||||
}
|
||||
// When i am passing callback as a dependency, it is causing infinite loop,
|
||||
// Please make sure you fix this eslint lint disable error with caution
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rootMargin, callback, elementRef, containerRef.current]);
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@ import { API_BASE_URL } from "@/helpers/common.helper";
|
|||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
// types
|
||||
import { TIssuesResponse } from "@/types/issue";
|
||||
import { TIssuesResponse, IIssue } from "@/types/issue";
|
||||
|
||||
class IssueService extends APIService {
|
||||
constructor() {
|
||||
|
|
@ -19,7 +19,7 @@ class IssueService extends APIService {
|
|||
});
|
||||
}
|
||||
|
||||
async getIssueById(anchor: string, issueID: string): Promise<any> {
|
||||
async getIssueById(anchor: string, issueID: string): Promise<IIssue> {
|
||||
return this.get(`/api/public/anchor/${anchor}/issues/${issueID}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
|
|
|||
17
space/core/services/label.service.ts
Normal file
17
space/core/services/label.service.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { IIssueLabel } from "@plane/types";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "./api.service";
|
||||
|
||||
export class LabelService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getLabels(anchor: string): Promise<IIssueLabel[]> {
|
||||
return this.get(`api/public/anchor/${anchor}/labels/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
17
space/core/services/state.service.ts
Normal file
17
space/core/services/state.service.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { IState } from "@plane/types";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "./api.service";
|
||||
|
||||
export class StateService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getStates(anchor: string): Promise<IState[]> {
|
||||
return this.get(`api/public/anchor/${anchor}/states/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
553
space/core/store/helpers/base-issues.store.ts
Normal file
553
space/core/store/helpers/base-issues.store.ts
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
import concat from "lodash/concat";
|
||||
import get from "lodash/get";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import set from "lodash/set";
|
||||
import uniq from "lodash/uniq";
|
||||
import update from "lodash/update";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane constants
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
// types
|
||||
import {
|
||||
TIssueGroupByOptions,
|
||||
TGroupedIssues,
|
||||
TSubGroupedIssues,
|
||||
TLoader,
|
||||
IssuePaginationOptions,
|
||||
TIssues,
|
||||
TIssuePaginationData,
|
||||
TGroupedIssueCount,
|
||||
TPaginationData,
|
||||
} from "@plane/types";
|
||||
// services
|
||||
import IssueService from "@/services/issue.service";
|
||||
import { IIssue, TIssuesResponse } from "@/types/issue";
|
||||
import { IIssueFilterStore } from "../issue-filters.store";
|
||||
import { CoreRootStore } from "../root.store";
|
||||
// constants
|
||||
// helpers
|
||||
|
||||
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null> | "target_date";
|
||||
|
||||
export enum EIssueGroupedAction {
|
||||
ADD = "ADD",
|
||||
DELETE = "DELETE",
|
||||
REORDER = "REORDER",
|
||||
}
|
||||
|
||||
export interface IBaseIssuesStore {
|
||||
// observable
|
||||
loader: Record<string, TLoader>;
|
||||
issuesMap: Record<string, IIssue>; // Record defines issue_id as key and IIssue as value
|
||||
// actions
|
||||
addIssue(issues: IIssue[], shouldReplace?: boolean): void;
|
||||
// helper methods
|
||||
getIssueById(issueId: string): undefined | IIssue;
|
||||
|
||||
fetchIssueById(anchorId: string, issueId: string): Promise<IIssue | undefined>;
|
||||
|
||||
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined; // object to store Issue Ids based on group or subgroup
|
||||
groupedIssueCount: TGroupedIssueCount; // map of groupId/subgroup and issue count of that particular group/subgroup
|
||||
issuePaginationData: TIssuePaginationData; // map of groupId/subgroup and pagination Data of that particular group/subgroup
|
||||
|
||||
// helper methods
|
||||
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
|
||||
getPaginationData(groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined;
|
||||
getIssueLoader(groupId?: string, subGroupId?: string): TLoader;
|
||||
getGroupIssueCount: (
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined,
|
||||
isSubGroupCumulative: boolean
|
||||
) => number | undefined;
|
||||
}
|
||||
|
||||
export const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof IIssue> = {
|
||||
project: "project_id",
|
||||
cycle: "cycle_id",
|
||||
module: "module_ids",
|
||||
state: "state_id",
|
||||
"state_detail.group": "state_group" as keyof IIssue, // state_detail.group is only being used for state_group display,
|
||||
priority: "priority",
|
||||
labels: "label_ids",
|
||||
created_by: "created_by",
|
||||
assignees: "assignee_ids",
|
||||
target_date: "target_date",
|
||||
};
|
||||
|
||||
export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
loader: Record<string, TLoader> = {};
|
||||
groupedIssueIds: TIssues | undefined = undefined;
|
||||
issuePaginationData: TIssuePaginationData = {};
|
||||
issuesMap: Record<string, IIssue> = {}; // Record defines issue_id as key and TIssue as value
|
||||
groupedIssueCount: TGroupedIssueCount = {};
|
||||
//
|
||||
paginationOptions: IssuePaginationOptions | undefined = undefined;
|
||||
|
||||
issueService;
|
||||
// root store
|
||||
rootIssueStore;
|
||||
issueFilterStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore, issueFilterStore: IIssueFilterStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
loader: observable,
|
||||
groupedIssueIds: observable,
|
||||
issuePaginationData: observable,
|
||||
groupedIssueCount: observable,
|
||||
|
||||
paginationOptions: observable,
|
||||
// action
|
||||
storePreviousPaginationValues: action.bound,
|
||||
|
||||
onfetchIssues: action.bound,
|
||||
onfetchNexIssues: action.bound,
|
||||
clear: action.bound,
|
||||
setLoader: action.bound,
|
||||
});
|
||||
this.rootIssueStore = _rootStore;
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
this.issueService = new IssueService();
|
||||
}
|
||||
|
||||
getIssueIds = (groupId?: string, subGroupId?: string) => {
|
||||
const groupedIssueIds = this.groupedIssueIds;
|
||||
|
||||
if (!groupedIssueIds) return undefined;
|
||||
|
||||
const allIssues = groupedIssueIds[ALL_ISSUES] ?? [];
|
||||
if (allIssues && Array.isArray(allIssues)) {
|
||||
return allIssues as string[];
|
||||
}
|
||||
|
||||
if (groupId && groupedIssueIds?.[groupId] && Array.isArray(groupedIssueIds[groupId])) {
|
||||
return (groupedIssueIds[groupId] ?? []) as string[];
|
||||
}
|
||||
|
||||
if (groupId && subGroupId) {
|
||||
return ((groupedIssueIds as TSubGroupedIssues)[groupId]?.[subGroupId] ?? []) as string[];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method will add issues to the issuesMap
|
||||
* @param {IIssue[]} issues
|
||||
* @returns {void}
|
||||
*/
|
||||
addIssue = (issues: IIssue[], shouldReplace = false) => {
|
||||
if (issues && issues.length <= 0) return;
|
||||
runInAction(() => {
|
||||
issues.forEach((issue) => {
|
||||
if (!this.issuesMap[issue.id] || shouldReplace) set(this.issuesMap, issue.id, issue);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method will return the issue from the issuesMap
|
||||
* @param {string} issueId
|
||||
* @returns {IIssue | undefined}
|
||||
*/
|
||||
getIssueById = computedFn((issueId: string) => {
|
||||
if (!issueId || isEmpty(this.issuesMap) || !this.issuesMap[issueId]) return undefined;
|
||||
return this.issuesMap[issueId];
|
||||
});
|
||||
|
||||
fetchIssueById = async (anchorId: string, issueId: string) => {
|
||||
try {
|
||||
const issueDetails = await this.issueService.getIssueById(anchorId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
set(this.issuesMap, [issueId], issueDetails);
|
||||
});
|
||||
|
||||
return issueDetails;
|
||||
} catch (e) {
|
||||
console.error("error fetching issue details");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Store the pagination data required for next subsequent issue pagination calls
|
||||
* @param prevCursor cursor value of previous page
|
||||
* @param nextCursor cursor value of next page
|
||||
* @param nextPageResults boolean to indicate if the next page results exist i.e, have we reached end of pages
|
||||
* @param groupId groupId and subGroupId to add the pagination data for the particular group/subgroup
|
||||
* @param subGroupId
|
||||
*/
|
||||
setPaginationData(
|
||||
prevCursor: string,
|
||||
nextCursor: string,
|
||||
nextPageResults: boolean,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) {
|
||||
const cursorObject = {
|
||||
prevCursor,
|
||||
nextCursor,
|
||||
nextPageResults,
|
||||
};
|
||||
|
||||
set(this.issuePaginationData, [this.getGroupKey(groupId, subGroupId)], cursorObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the loader value of the particular groupId/subGroupId, or to ALL_ISSUES if both are undefined
|
||||
* @param loaderValue
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
*/
|
||||
setLoader(loaderValue: TLoader, groupId?: string, subGroupId?: string) {
|
||||
runInAction(() => {
|
||||
set(this.loader, this.getGroupKey(groupId, subGroupId), loaderValue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the Loader value of particular group/subgroup/ALL_ISSUES
|
||||
*/
|
||||
getIssueLoader = (groupId?: string, subGroupId?: string) => get(this.loader, this.getGroupKey(groupId, subGroupId));
|
||||
|
||||
/**
|
||||
* gets the pagination data of particular group/subgroup/ALL_ISSUES
|
||||
*/
|
||||
getPaginationData = computedFn(
|
||||
(groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined =>
|
||||
get(this.issuePaginationData, [this.getGroupKey(groupId, subGroupId)])
|
||||
);
|
||||
|
||||
/**
|
||||
* gets the issue count of particular group/subgroup/ALL_ISSUES
|
||||
*
|
||||
* if isSubGroupCumulative is true, sum up all the issueCount of the subGroupId, across all the groupIds
|
||||
*/
|
||||
getGroupIssueCount = computedFn(
|
||||
(
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined,
|
||||
isSubGroupCumulative: boolean
|
||||
): number | undefined => {
|
||||
if (isSubGroupCumulative && subGroupId) {
|
||||
const groupIssuesKeys = Object.keys(this.groupedIssueCount);
|
||||
let subGroupCumulativeCount = 0;
|
||||
|
||||
for (const groupKey of groupIssuesKeys) {
|
||||
if (groupKey.includes(`_${subGroupId}`)) subGroupCumulativeCount += this.groupedIssueCount[groupKey];
|
||||
}
|
||||
|
||||
return subGroupCumulativeCount;
|
||||
}
|
||||
|
||||
return get(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)]);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* This Method is called after fetching the first paginated issues
|
||||
*
|
||||
* This method updates the appropriate issue list based on if groupByKey or subGroupByKey are defined
|
||||
* If both groupByKey and subGroupByKey are not defined, then the issue list are added to another group called ALL_ISSUES
|
||||
* @param issuesResponse Paginated Response received from the API
|
||||
* @param options Pagination options
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param id Id can be anything from cycleId, moduleId, viewId or userId based on the store
|
||||
*/
|
||||
onfetchIssues(issuesResponse: TIssuesResponse, options: IssuePaginationOptions) {
|
||||
// Process the Issue Response to get the following data from it
|
||||
const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse);
|
||||
|
||||
// The Issue list is added to the main Issue Map
|
||||
this.addIssue(issueList);
|
||||
|
||||
// Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts
|
||||
runInAction(() => {
|
||||
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount);
|
||||
this.loader[this.getGroupKey()] = undefined;
|
||||
});
|
||||
|
||||
// store Pagination options for next subsequent calls and data like next cursor etc
|
||||
this.storePreviousPaginationValues(issuesResponse, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* This Method is called on the subsequent pagination calls after the first initial call
|
||||
*
|
||||
* This method updates the appropriate issue list based on if groupId or subgroupIds are Passed
|
||||
* @param issuesResponse Paginated Response received from the API
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
*/
|
||||
onfetchNexIssues(issuesResponse: TIssuesResponse, groupId?: string, subGroupId?: string) {
|
||||
// Process the Issue Response to get the following data from it
|
||||
const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse);
|
||||
|
||||
// The Issue list is added to the main Issue Map
|
||||
this.addIssue(issueList);
|
||||
|
||||
// Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts
|
||||
runInAction(() => {
|
||||
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount, groupId, subGroupId);
|
||||
this.loader[this.getGroupKey(groupId, subGroupId)] = undefined;
|
||||
});
|
||||
|
||||
// store Pagination data like next cursor etc
|
||||
this.storePreviousPaginationValues(issuesResponse, undefined, groupId, subGroupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called to clear out the current store
|
||||
*/
|
||||
clear(shouldClearPaginationOptions = true) {
|
||||
runInAction(() => {
|
||||
this.groupedIssueIds = undefined;
|
||||
this.issuePaginationData = {};
|
||||
this.groupedIssueCount = {};
|
||||
if (shouldClearPaginationOptions) {
|
||||
this.paginationOptions = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method processes the issueResponse to provide data that can be used to update the store
|
||||
* @param issueResponse
|
||||
* @returns issueList, list of issue Data
|
||||
* @returns groupedIssues, grouped issue Ids
|
||||
* @returns groupedIssueCount, object containing issue counts of individual groups
|
||||
*/
|
||||
processIssueResponse(issueResponse: TIssuesResponse): {
|
||||
issueList: IIssue[];
|
||||
groupedIssues: TIssues;
|
||||
groupedIssueCount: TGroupedIssueCount;
|
||||
} {
|
||||
const issueResult = issueResponse?.results;
|
||||
|
||||
// if undefined return empty objects
|
||||
if (!issueResult)
|
||||
return {
|
||||
issueList: [],
|
||||
groupedIssues: {},
|
||||
groupedIssueCount: {},
|
||||
};
|
||||
|
||||
//if is an array then it's an ungrouped response. return values with groupId as ALL_ISSUES
|
||||
if (Array.isArray(issueResult)) {
|
||||
return {
|
||||
issueList: issueResult,
|
||||
groupedIssues: {
|
||||
[ALL_ISSUES]: issueResult.map((issue) => issue.id),
|
||||
},
|
||||
groupedIssueCount: {
|
||||
[ALL_ISSUES]: issueResponse.total_count,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const issueList: IIssue[] = [];
|
||||
const groupedIssues: TGroupedIssues | TSubGroupedIssues = {};
|
||||
const groupedIssueCount: TGroupedIssueCount = {};
|
||||
|
||||
// update total issue count to ALL_ISSUES
|
||||
set(groupedIssueCount, [ALL_ISSUES], issueResponse.total_count);
|
||||
|
||||
// loop through all the groupIds from issue Result
|
||||
for (const groupId in issueResult) {
|
||||
const groupIssuesObject = issueResult[groupId];
|
||||
const groupIssueResult = groupIssuesObject?.results;
|
||||
|
||||
// if groupIssueResult is undefined then continue the loop
|
||||
if (!groupIssueResult) continue;
|
||||
|
||||
// set grouped Issue count of the current groupId
|
||||
set(groupedIssueCount, [groupId], groupIssuesObject.total_results);
|
||||
|
||||
// if groupIssueResult, the it is not subGrouped
|
||||
if (Array.isArray(groupIssueResult)) {
|
||||
// add the result to issueList
|
||||
issueList.push(...groupIssueResult);
|
||||
// set the issue Ids to the groupId path
|
||||
set(
|
||||
groupedIssues,
|
||||
[groupId],
|
||||
groupIssueResult.map((issue) => issue.id)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// loop through all the subGroupIds from issue Result
|
||||
for (const subGroupId in groupIssueResult) {
|
||||
const subGroupIssuesObject = groupIssueResult[subGroupId];
|
||||
const subGroupIssueResult = subGroupIssuesObject?.results;
|
||||
|
||||
// if subGroupIssueResult is undefined then continue the loop
|
||||
if (!subGroupIssueResult) continue;
|
||||
|
||||
// set sub grouped Issue count of the current groupId
|
||||
set(groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], subGroupIssuesObject.total_results);
|
||||
|
||||
if (Array.isArray(subGroupIssueResult)) {
|
||||
// add the result to issueList
|
||||
issueList.push(...subGroupIssueResult);
|
||||
// set the issue Ids to the [groupId, subGroupId] path
|
||||
set(
|
||||
groupedIssues,
|
||||
[groupId, subGroupId],
|
||||
subGroupIssueResult.map((issue) => issue.id)
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { issueList, groupedIssues, groupedIssueCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to update the grouped issue Ids to it's respected lists and also to update group Issue Counts
|
||||
* @param groupedIssues Object that contains list of issueIds with respect to their groups/subgroups
|
||||
* @param groupedIssueCount Object the contains the issue count of each groups
|
||||
* @param groupId groupId string
|
||||
* @param subGroupId subGroupId string
|
||||
* @returns updates the store with the values
|
||||
*/
|
||||
updateGroupedIssueIds(
|
||||
groupedIssues: TIssues,
|
||||
groupedIssueCount: TGroupedIssueCount,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) {
|
||||
// if groupId exists and groupedIssues has ALL_ISSUES as a group,
|
||||
// then it's an individual group/subgroup pagination
|
||||
if (groupId && groupedIssues[ALL_ISSUES] && Array.isArray(groupedIssues[ALL_ISSUES])) {
|
||||
const issueGroup = groupedIssues[ALL_ISSUES];
|
||||
const issueGroupCount = groupedIssueCount[ALL_ISSUES];
|
||||
const issuesPath = [groupId];
|
||||
// issuesPath is the path for the issue List in the Grouped Issue List
|
||||
// issuePath is either [groupId] for grouped pagination or [groupId, subGroupId] for subGrouped pagination
|
||||
if (subGroupId) issuesPath.push(subGroupId);
|
||||
|
||||
// update the issue Count of the particular group/subGroup
|
||||
set(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], issueGroupCount);
|
||||
|
||||
// update the issue list in the issuePath
|
||||
this.updateIssueGroup(issueGroup, issuesPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// if not in the above condition the it's a complete grouped pagination not individual group/subgroup pagination
|
||||
// update total issue count as ALL_ISSUES count in `groupedIssueCount` object
|
||||
set(this.groupedIssueCount, [ALL_ISSUES], groupedIssueCount[ALL_ISSUES]);
|
||||
|
||||
// loop through the groups of groupedIssues.
|
||||
for (const groupId in groupedIssues) {
|
||||
const issueGroup = groupedIssues[groupId];
|
||||
const issueGroupCount = groupedIssueCount[groupId];
|
||||
|
||||
// update the groupId's issue count
|
||||
set(this.groupedIssueCount, [groupId], issueGroupCount);
|
||||
|
||||
// This updates the group issue list in the store, if the issueGroup is a string
|
||||
const storeUpdated = this.updateIssueGroup(issueGroup, [groupId]);
|
||||
// if issueGroup is indeed a string, continue
|
||||
if (storeUpdated) continue;
|
||||
|
||||
// if issueGroup is not a string, loop through the sub group Issues
|
||||
for (const subGroupId in issueGroup) {
|
||||
const issueSubGroup = (issueGroup as TGroupedIssues)[subGroupId];
|
||||
const issueSubGroupCount = groupedIssueCount[this.getGroupKey(groupId, subGroupId)];
|
||||
|
||||
// update the subGroupId's issue count
|
||||
set(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], issueSubGroupCount);
|
||||
// This updates the subgroup issue list in the store
|
||||
this.updateIssueGroup(issueSubGroup, [groupId, subGroupId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This Method is used to update the issue Id list at the particular issuePath
|
||||
* @param groupedIssueIds could be an issue Id List for grouped issues or an object that contains a issue Id list in case of subGrouped
|
||||
* @param issuePath array of string, to identify the path of the issueList to be updated with the above issue Id list
|
||||
* @returns a boolean that indicates if the groupedIssueIds is indeed a array Id list, in which case the issue Id list is added to the store at issuePath
|
||||
*/
|
||||
updateIssueGroup(groupedIssueIds: TGroupedIssues | string[], issuePath: string[]): boolean {
|
||||
if (!groupedIssueIds) return true;
|
||||
|
||||
// if groupedIssueIds is an array, update the `groupedIssueIds` store at the issuePath
|
||||
if (groupedIssueIds && Array.isArray(groupedIssueIds)) {
|
||||
update(this, ["groupedIssueIds", ...issuePath], (issueIds: string[] = []) =>
|
||||
uniq(concat(issueIds, groupedIssueIds as string[]))
|
||||
);
|
||||
// return true to indicate the store has been updated
|
||||
return true;
|
||||
}
|
||||
|
||||
// return false to indicate the store has been updated and the groupedIssueIds is likely Object for subGrouped Issues
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to update the count of the issues at the path with the increment
|
||||
* @param path issuePath, corresponding key is to be incremented
|
||||
* @param increment
|
||||
*/
|
||||
updateIssueCount(accumulatedUpdatesForCount: { [key: string]: EIssueGroupedAction }) {
|
||||
const updateKeys = Object.keys(accumulatedUpdatesForCount);
|
||||
for (const updateKey of updateKeys) {
|
||||
const update = accumulatedUpdatesForCount[updateKey];
|
||||
if (!update) continue;
|
||||
|
||||
const increment = update === EIssueGroupedAction.ADD ? 1 : -1;
|
||||
// get current count at the key
|
||||
const issueCount = get(this.groupedIssueCount, updateKey) ?? 0;
|
||||
// update the count at the key
|
||||
set(this.groupedIssueCount, updateKey, issueCount + increment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This Method is called to store the pagination options and paginated data from response
|
||||
* @param issuesResponse issue list response
|
||||
* @param options pagination options to be stored for next page call
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
*/
|
||||
storePreviousPaginationValues = (
|
||||
issuesResponse: TIssuesResponse,
|
||||
options?: IssuePaginationOptions,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => {
|
||||
if (options) this.paginationOptions = options;
|
||||
|
||||
this.setPaginationData(
|
||||
issuesResponse.prev_cursor,
|
||||
issuesResponse.next_cursor,
|
||||
issuesResponse.next_page_results,
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns,
|
||||
* A compound key, if both groupId & subGroupId are defined
|
||||
* groupId, only if groupId is defined
|
||||
* ALL_ISSUES, if both groupId & subGroupId are not defined
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
* @returns
|
||||
*/
|
||||
getGroupKey = (groupId?: string, subGroupId?: string) => {
|
||||
if (groupId && subGroupId && subGroupId !== "null") return `${groupId}_${subGroupId}`;
|
||||
|
||||
if (groupId) return groupId;
|
||||
|
||||
return ALL_ISSUES;
|
||||
};
|
||||
}
|
||||
63
space/core/store/helpers/filter.helpers.ts
Normal file
63
space/core/store/helpers/filter.helpers.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { EIssueGroupByToServerOptions, EServerGroupByToFilterOptions } from "@plane/constants";
|
||||
import { IssuePaginationOptions, TIssueParams } from "@plane/types";
|
||||
|
||||
/**
|
||||
* This Method is used to construct the url params along with paginated values
|
||||
* @param filterParams params generated from filters
|
||||
* @param options pagination options
|
||||
* @param cursor cursor if exists
|
||||
* @param groupId groupId if to fetch By group
|
||||
* @param subGroupId groupId if to fetch By sub group
|
||||
* @returns
|
||||
*/
|
||||
export const getPaginationParams = (
|
||||
filterParams: Partial<Record<TIssueParams, string | boolean>> | undefined,
|
||||
options: IssuePaginationOptions,
|
||||
cursor: string | undefined,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => {
|
||||
// if cursor exists, use the cursor. If it doesn't exist construct the cursor based on per page count
|
||||
const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount}:1:0` : `${options.perPageCount}:0:0`;
|
||||
|
||||
// pagination params
|
||||
const paginationParams: Partial<Record<TIssueParams, string | boolean>> = {
|
||||
...filterParams,
|
||||
cursor: pageCursor,
|
||||
per_page: options.perPageCount.toString(),
|
||||
};
|
||||
|
||||
// If group by is specifically sent through options, like that for calendar layout, use that to group
|
||||
if (options.groupedBy) {
|
||||
paginationParams.group_by = EIssueGroupByToServerOptions[options.groupedBy];
|
||||
}
|
||||
|
||||
// If before and after dates are sent from option to filter by then, add them to filter the options
|
||||
if (options.after && options.before) {
|
||||
paginationParams["target_date"] = `${options.after};after,${options.before};before`;
|
||||
}
|
||||
|
||||
// If groupId is passed down, add a filter param for that group Id
|
||||
if (groupId) {
|
||||
const groupBy = paginationParams["group_by"] as EIssueGroupByToServerOptions | undefined;
|
||||
delete paginationParams["group_by"];
|
||||
|
||||
if (groupBy) {
|
||||
const groupByFilterOption = EServerGroupByToFilterOptions[groupBy];
|
||||
paginationParams[groupByFilterOption] = groupId;
|
||||
}
|
||||
}
|
||||
|
||||
// If subGroupId is passed down, add a filter param for that subGroup Id
|
||||
if (subGroupId) {
|
||||
const subGroupBy = paginationParams["sub_group_by"] as EIssueGroupByToServerOptions | undefined;
|
||||
delete paginationParams["sub_group_by"];
|
||||
|
||||
if (subGroupBy) {
|
||||
const subGroupByFilterOption = EServerGroupByToFilterOptions[subGroupBy];
|
||||
paginationParams[subGroupByFilterOption] = subGroupId;
|
||||
}
|
||||
}
|
||||
|
||||
return paginationParams;
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import set from "lodash/set";
|
||||
import { makeObservable, observable, action, runInAction } from "mobx";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// services
|
||||
|
|
@ -97,7 +98,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||
this.loader = true;
|
||||
this.error = null;
|
||||
|
||||
const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueID);
|
||||
const issueDetails = await this.rootStore.issue.fetchIssueById(anchor, issueID);
|
||||
const commentsResponse = await this.issueService.getIssueComments(anchor, issueID);
|
||||
|
||||
if (issueDetails) {
|
||||
|
|
@ -119,17 +120,11 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||
|
||||
addIssueComment = async (anchor: string, issueID: string, data: any) => {
|
||||
try {
|
||||
const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueID);
|
||||
const issueDetails = this.rootStore.issue.getIssueById(issueID);
|
||||
const issueCommentResponse = await this.issueService.createIssueComment(anchor, issueID, data);
|
||||
if (issueDetails) {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...issueDetails,
|
||||
comments: [...this.details[issueID].comments, issueCommentResponse],
|
||||
},
|
||||
};
|
||||
set(this.details, [issueID, "comments"], [...this.details[issueID].comments, issueCommentResponse]);
|
||||
});
|
||||
}
|
||||
return issueCommentResponse;
|
||||
|
|
@ -267,21 +262,17 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||
addIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
reactions: [
|
||||
...this.details[issueID].reactions,
|
||||
{
|
||||
id: uuidv4(),
|
||||
issue: issueID,
|
||||
reaction: reactionHex,
|
||||
actor_detail: this.rootStore.user.currentActor,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
set(
|
||||
this.details,
|
||||
[issueID, "reaction_items"],
|
||||
[
|
||||
...this.details[issueID].reaction_items,
|
||||
{
|
||||
reaction: reactionHex,
|
||||
actor_detail: this.rootStore.user.currentActor,
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
await this.issueService.createIssueReaction(anchor, issueID, {
|
||||
|
|
@ -291,31 +282,19 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||
console.log("Failed to add issue vote");
|
||||
const issueReactions = await this.issueService.getIssueReactions(anchor, issueID);
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
reactions: issueReactions,
|
||||
},
|
||||
};
|
||||
set(this.details, [issueID, "reaction_items"], issueReactions);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
removeIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => {
|
||||
try {
|
||||
const newReactions = this.details[issueID].reactions.filter(
|
||||
(_r) => !(_r.reaction === reactionHex && _r.actor_detail.id === this.rootStore.user.data?.id)
|
||||
const newReactions = this.details[issueID].reaction_items.filter(
|
||||
(_r) => !(_r.reaction === reactionHex && _r.actor_details.id === this.rootStore.user.data?.id)
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
reactions: newReactions,
|
||||
},
|
||||
};
|
||||
set(this.details, [issueID, "reaction_items"], newReactions);
|
||||
});
|
||||
|
||||
await this.issueService.deleteIssueReaction(anchor, issueID, reactionHex);
|
||||
|
|
@ -323,13 +302,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||
console.log("Failed to remove issue reaction");
|
||||
const reactions = await this.issueService.getIssueReactions(anchor, issueID);
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
reactions: reactions,
|
||||
},
|
||||
};
|
||||
set(this.details, [issueID, "reaction_items"], reactions);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -341,25 +314,19 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||
if (!projectID || !workspaceSlug) throw new Error("Publish settings not found");
|
||||
|
||||
const newVote: IVote = {
|
||||
actor: this.rootStore.user.data?.id ?? "",
|
||||
actor_detail: this.rootStore.user.currentActor,
|
||||
issue: issueID,
|
||||
project: projectID,
|
||||
workspace: workspaceSlug,
|
||||
actor_details: this.rootStore.user.currentActor,
|
||||
vote: data.vote,
|
||||
};
|
||||
|
||||
const filteredVotes = this.details[issueID].votes.filter((v) => v.actor !== this.rootStore.user.data?.id);
|
||||
const filteredVotes = this.details[issueID].vote_items.filter(
|
||||
(v) => v.actor_details?.id !== this.rootStore.user.data?.id
|
||||
);
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
votes: [...filteredVotes, newVote],
|
||||
},
|
||||
};
|
||||
runInAction(() => {
|
||||
set(this.details, [issueID, "vote_items"], [...filteredVotes, newVote]);
|
||||
});
|
||||
});
|
||||
|
||||
await this.issueService.createIssueVote(anchor, issueID, data);
|
||||
|
|
@ -368,29 +335,19 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||
const issueVotes = await this.issueService.getIssueVotes(anchor, issueID);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
votes: issueVotes,
|
||||
},
|
||||
};
|
||||
set(this.details, [issueID, "vote_items"], issueVotes);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
removeIssueVote = async (anchor: string, issueID: string) => {
|
||||
const newVotes = this.details[issueID].votes.filter((v) => v.actor !== this.rootStore.user.data?.id);
|
||||
const newVotes = this.details[issueID].vote_items.filter(
|
||||
(v) => v.actor_details?.id !== this.rootStore.user.data?.id
|
||||
);
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
votes: newVotes,
|
||||
},
|
||||
};
|
||||
set(this.details, [issueID, "vote_items"], newVotes);
|
||||
});
|
||||
|
||||
await this.issueService.deleteIssueVote(anchor, issueID);
|
||||
|
|
@ -399,13 +356,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||
const issueVotes = await this.issueService.getIssueVotes(anchor, issueID);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
votes: issueVotes,
|
||||
},
|
||||
};
|
||||
set(this.details, [issueID, "vote_items"], issueVotes);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import isEqual from "lodash/isEqual";
|
|||
import set from "lodash/set";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane types
|
||||
import { IssuePaginationOptions, TIssueParams } from "@plane/types";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// store
|
||||
|
|
@ -15,6 +17,7 @@ import {
|
|||
TIssueQueryFiltersParams,
|
||||
TIssueFilterKeys,
|
||||
} from "@/types/issue";
|
||||
import { getPaginationParams } from "./helpers/filter.helpers";
|
||||
|
||||
export interface IIssueFilterStore {
|
||||
// observables
|
||||
|
|
@ -27,13 +30,20 @@ export interface IIssueFilterStore {
|
|||
getAppliedFilters: (anchor: string) => TIssueQueryFiltersParams | undefined;
|
||||
// actions
|
||||
updateLayoutOptions: (layout: TIssueLayoutOptions) => void;
|
||||
initIssueFilters: (anchor: string, filters: TIssueFilters) => void;
|
||||
initIssueFilters: (anchor: string, filters: TIssueFilters, shouldFetchIssues?: boolean) => void;
|
||||
updateIssueFilters: <K extends keyof TIssueFilters>(
|
||||
anchor: string,
|
||||
filterKind: K,
|
||||
filterKey: keyof TIssueFilters[K],
|
||||
filters: TIssueFilters[K][typeof filterKey]
|
||||
) => Promise<void>;
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
anchor: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
}
|
||||
|
||||
export class IssueFilterStore implements IIssueFilterStore {
|
||||
|
|
@ -114,14 +124,27 @@ export class IssueFilterStore implements IIssueFilterStore {
|
|||
// actions
|
||||
updateLayoutOptions = (options: TIssueLayoutOptions) => set(this, ["layoutOptions"], options);
|
||||
|
||||
initIssueFilters = async (anchor: string, initFilters: TIssueFilters) => {
|
||||
initIssueFilters = async (anchor: string, initFilters: TIssueFilters, shouldFetchIssues: boolean = false) => {
|
||||
if (this.filters === undefined) runInAction(() => (this.filters = {}));
|
||||
if (this.filters && initFilters) set(this.filters, [anchor], initFilters);
|
||||
|
||||
const appliedFilters = this.getAppliedFilters(anchor);
|
||||
await this.store.issue.fetchPublicIssues(anchor, appliedFilters);
|
||||
if (shouldFetchIssues) await this.store.issue.fetchPublicIssuesWithExistingPagination(anchor, "mutation");
|
||||
};
|
||||
|
||||
getFilterParams = computedFn(
|
||||
(
|
||||
options: IssuePaginationOptions,
|
||||
anchor: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => {
|
||||
const filterParams = this.getAppliedFilters(anchor);
|
||||
const paginationParams = getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
|
||||
return paginationParams;
|
||||
}
|
||||
);
|
||||
|
||||
updateIssueFilters = async <K extends keyof TIssueFilters>(
|
||||
anchor: string,
|
||||
filterKind: K,
|
||||
|
|
@ -135,7 +158,6 @@ export class IssueFilterStore implements IIssueFilterStore {
|
|||
if (this.filters) set(this.filters, [anchor, filterKind, filterKey], filterValue);
|
||||
});
|
||||
|
||||
const appliedFilters = this.getAppliedFilters(anchor);
|
||||
await this.store.issue.fetchPublicIssues(anchor, appliedFilters);
|
||||
if (filterKey !== "layout") await this.store.issue.fetchPublicIssuesWithExistingPagination(anchor, "mutation");
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +1,38 @@
|
|||
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// types
|
||||
import { IStateLite } from "@plane/types";
|
||||
import { IssuePaginationOptions, TLoader } from "@plane/types";
|
||||
// services
|
||||
import IssueService from "@/services/issue.service";
|
||||
// store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
// types
|
||||
import { IIssue, IIssueLabel } from "@/types/issue";
|
||||
import { BaseIssuesStore, IBaseIssuesStore } from "./helpers/base-issues.store";
|
||||
|
||||
export interface IIssueStore {
|
||||
loader: boolean;
|
||||
error: any;
|
||||
// observables
|
||||
issues: IIssue[];
|
||||
states: IStateLite[];
|
||||
labels: IIssueLabel[];
|
||||
// filter observables
|
||||
filteredStates: string[];
|
||||
filteredLabels: string[];
|
||||
filteredPriorities: string[];
|
||||
export interface IIssueStore extends IBaseIssuesStore {
|
||||
// actions
|
||||
fetchPublicIssues: (anchor: string, params: any) => Promise<void>;
|
||||
// helpers
|
||||
getCountOfIssuesByState: (stateID: string) => number;
|
||||
getFilteredIssuesByState: (stateID: string) => IIssue[];
|
||||
fetchPublicIssues: (
|
||||
anchor: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions,
|
||||
isExistingPaginationOptions?: boolean
|
||||
) => Promise<void>;
|
||||
fetchNextPublicIssues: (anchor: string, groupId?: string, subGroupId?: string) => Promise<void>;
|
||||
fetchPublicIssuesWithExistingPagination: (anchor: string, loadType?: TLoader) => Promise<void>;
|
||||
}
|
||||
|
||||
export class IssueStore implements IIssueStore {
|
||||
loader: boolean = false;
|
||||
error: any | null = null;
|
||||
// observables
|
||||
states: IStateLite[] = [];
|
||||
labels: IIssueLabel[] = [];
|
||||
issues: IIssue[] = [];
|
||||
// filter observables
|
||||
filteredStates: string[] = [];
|
||||
filteredLabels: string[] = [];
|
||||
filteredPriorities: string[] = [];
|
||||
export class IssueStore extends BaseIssuesStore implements IIssueStore {
|
||||
// root store
|
||||
rootStore: CoreRootStore;
|
||||
// services
|
||||
issueService: IssueService;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
super(_rootStore, _rootStore.issueFilter);
|
||||
makeObservable(this, {
|
||||
loader: observable.ref,
|
||||
error: observable,
|
||||
// observables
|
||||
states: observable,
|
||||
labels: observable,
|
||||
issues: observable,
|
||||
// filter observables
|
||||
filteredStates: observable,
|
||||
filteredLabels: observable,
|
||||
filteredPriorities: observable,
|
||||
// actions
|
||||
fetchPublicIssues: action,
|
||||
fetchNextPublicIssues: action,
|
||||
fetchPublicIssuesWithExistingPagination: action,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
|
|
@ -68,45 +44,69 @@ export class IssueStore implements IIssueStore {
|
|||
* @param {string} anchor
|
||||
* @param params
|
||||
*/
|
||||
fetchPublicIssues = async (anchor: string, params: any) => {
|
||||
fetchPublicIssues = async (
|
||||
anchor: string,
|
||||
loadType: TLoader = "init-loader",
|
||||
options: IssuePaginationOptions,
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
this.setLoader(loadType);
|
||||
});
|
||||
this.clear(!isExistingPaginationOptions);
|
||||
|
||||
const params = this.rootStore.issueFilter.getFilterParams(options, anchor, undefined, undefined, undefined);
|
||||
|
||||
const response = await this.issueService.fetchPublicIssues(anchor, params);
|
||||
|
||||
if (response) {
|
||||
runInAction(() => {
|
||||
this.states = response.states;
|
||||
this.labels = response.labels;
|
||||
this.issues = response.issues;
|
||||
this.loader = false;
|
||||
});
|
||||
}
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options);
|
||||
} catch (error) {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
fetchNextPublicIssues = async (anchor: string, groupId?: string, subGroupId?: string) => {
|
||||
const cursorObject = this.getPaginationData(groupId, subGroupId);
|
||||
// if there are no pagination options and the next page results do not exist the return
|
||||
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
|
||||
try {
|
||||
// set Loader
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
|
||||
// get params from stored pagination options
|
||||
const params = this.rootStore.issueFilter.getFilterParams(
|
||||
this.paginationOptions,
|
||||
anchor,
|
||||
cursorObject?.nextCursor,
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
// call the fetch issues API with the params for next page in issues
|
||||
const response = await this.issueService.fetchPublicIssues(anchor, params);
|
||||
|
||||
// after the next page of issues are fetched, call the base method to process the response
|
||||
this.onfetchNexIssues(response, groupId, subGroupId);
|
||||
} catch (error) {
|
||||
// set Loader as undefined if errored out
|
||||
this.setLoader(undefined, groupId, subGroupId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description get total count of issues under a particular state
|
||||
* @param {string} stateID
|
||||
* @returns {number}
|
||||
* This Method exists to fetch the first page of the issues with the existing stored pagination
|
||||
* This is useful for refetching when filters, groupBy, orderBy etc changes
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @returns
|
||||
*/
|
||||
getCountOfIssuesByState = computedFn(
|
||||
(stateID: string) => this.issues?.filter((issue) => issue.state == stateID).length || 0
|
||||
);
|
||||
|
||||
/**
|
||||
* @description get array of issues under a particular state
|
||||
* @param {string} stateID
|
||||
* @returns {IIssue[]}
|
||||
*/
|
||||
getFilteredIssuesByState = computedFn(
|
||||
(stateID: string) => this.issues?.filter((issue) => issue.state == stateID) || []
|
||||
);
|
||||
fetchPublicIssuesWithExistingPagination = async (anchor: string, loadType: TLoader = "mutation") => {
|
||||
if (!this.paginationOptions) return;
|
||||
return await this.fetchPublicIssues(anchor, loadType, this.paginationOptions, true);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
63
space/core/store/label.store.ts
Normal file
63
space/core/store/label.store.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import set from "lodash/set";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { IIssueLabel } from "@plane/types";
|
||||
import { LabelService } from "@/services/label.service";
|
||||
import { CoreRootStore } from "./root.store";
|
||||
|
||||
export interface IIssueLabelStore {
|
||||
// observables
|
||||
labels: IIssueLabel[] | undefined;
|
||||
// computed actions
|
||||
getLabelById: (labelId: string | undefined) => IIssueLabel | undefined;
|
||||
getLabelsByIds: (labelIds: string[]) => IIssueLabel[];
|
||||
// fetch actions
|
||||
fetchLabels: (anchor: string) => Promise<IIssueLabel[]>;
|
||||
}
|
||||
|
||||
export class LabelStore implements IIssueLabelStore {
|
||||
labelMap: Record<string, IIssueLabel> = {};
|
||||
labelService: LabelService;
|
||||
rootStore: CoreRootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
labelMap: observable,
|
||||
// computed
|
||||
labels: computed,
|
||||
// fetch action
|
||||
fetchLabels: action,
|
||||
});
|
||||
this.labelService = new LabelService();
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
get labels() {
|
||||
return Object.values(this.labelMap);
|
||||
}
|
||||
|
||||
getLabelById = (labelId: string | undefined) => (labelId ? this.labelMap[labelId] : undefined);
|
||||
|
||||
getLabelsByIds = (labelIds: string[]) => {
|
||||
const currLabels = [];
|
||||
for (const labelId of labelIds) {
|
||||
const label = this.getLabelById(labelId);
|
||||
if (label) {
|
||||
currLabels.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
return currLabels;
|
||||
};
|
||||
|
||||
fetchLabels = async (anchor: string) => {
|
||||
const labelsResponse = await this.labelService.getLabels(anchor);
|
||||
runInAction(() => {
|
||||
this.labelMap = {};
|
||||
for (const label of labelsResponse) {
|
||||
set(this.labelMap, [label.id], label);
|
||||
}
|
||||
});
|
||||
return labelsResponse;
|
||||
};
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ import { IssueDetailStore, IIssueDetailStore } from "@/store/issue-detail.store"
|
|||
import { IssueStore, IIssueStore } from "@/store/issue.store";
|
||||
import { IUserStore, UserStore } from "@/store/user.store";
|
||||
import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store";
|
||||
import { IIssueLabelStore, LabelStore } from "./label.store";
|
||||
import { IMentionsStore, MentionsStore } from "./mentions.store";
|
||||
import { IPublishListStore, PublishListStore } from "./publish/publish_list.store";
|
||||
import { IStateStore, StateStore } from "./state.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
|
|
@ -16,6 +18,8 @@ export class CoreRootStore {
|
|||
issue: IIssueStore;
|
||||
issueDetail: IIssueDetailStore;
|
||||
mentionStore: IMentionsStore;
|
||||
state: IStateStore;
|
||||
label: IIssueLabelStore;
|
||||
issueFilter: IIssueFilterStore;
|
||||
publishList: IPublishListStore;
|
||||
|
||||
|
|
@ -25,6 +29,8 @@ export class CoreRootStore {
|
|||
this.issue = new IssueStore(this);
|
||||
this.issueDetail = new IssueDetailStore(this);
|
||||
this.mentionStore = new MentionsStore(this);
|
||||
this.state = new StateStore(this);
|
||||
this.label = new LabelStore(this);
|
||||
this.issueFilter = new IssueFilterStore(this);
|
||||
this.publishList = new PublishListStore(this);
|
||||
}
|
||||
|
|
@ -43,6 +49,8 @@ export class CoreRootStore {
|
|||
this.issue = new IssueStore(this);
|
||||
this.issueDetail = new IssueDetailStore(this);
|
||||
this.mentionStore = new MentionsStore(this);
|
||||
this.state = new StateStore(this);
|
||||
this.label = new LabelStore(this);
|
||||
this.issueFilter = new IssueFilterStore(this);
|
||||
this.publishList = new PublishListStore(this);
|
||||
}
|
||||
|
|
|
|||
40
space/core/store/state.store.ts
Normal file
40
space/core/store/state.store.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { IState } from "@plane/types";
|
||||
import { StateService } from "@/services/state.service";
|
||||
import { CoreRootStore } from "./root.store";
|
||||
|
||||
export interface IStateStore {
|
||||
// observables
|
||||
states: IState[] | undefined;
|
||||
// computed actions
|
||||
getStateById: (stateId: string | undefined) => IState | undefined;
|
||||
// fetch actions
|
||||
fetchStates: (anchor: string) => Promise<IState[]>;
|
||||
}
|
||||
|
||||
export class StateStore implements IStateStore {
|
||||
states: IState[] | undefined = undefined;
|
||||
stateService: StateService;
|
||||
rootStore: CoreRootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
states: observable,
|
||||
// fetch action
|
||||
fetchStates: action,
|
||||
});
|
||||
this.stateService = new StateService();
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
getStateById = (stateId: string | undefined) => this.states?.find((state) => state.id === stateId);
|
||||
|
||||
fetchStates = async (anchor: string) => {
|
||||
const statesResponse = await this.stateService.getStates(anchor);
|
||||
runInAction(() => {
|
||||
this.states = statesResponse;
|
||||
});
|
||||
return statesResponse;
|
||||
};
|
||||
}
|
||||
82
space/core/types/issue.d.ts
vendored
82
space/core/types/issue.d.ts
vendored
|
|
@ -1,4 +1,4 @@
|
|||
import { IStateLite, IWorkspaceLite, TIssue, TIssuePriorities, TStateGroups } from "@plane/types";
|
||||
import { IWorkspaceLite, TIssue, TIssuePriorities, TStateGroups } from "@plane/types";
|
||||
|
||||
export type TIssueLayout = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt";
|
||||
export type TIssueLayoutOptions = {
|
||||
|
|
@ -33,31 +33,61 @@ export type TIssueQueryFilters = Partial<TFilters>;
|
|||
|
||||
export type TIssueQueryFiltersParams = Partial<Record<keyof TFilters, string>>;
|
||||
|
||||
export type TIssuesResponse = {
|
||||
states: IStateLite[];
|
||||
labels: IIssueLabel[];
|
||||
issues: IIssue[];
|
||||
};
|
||||
|
||||
export interface IIssue
|
||||
extends Pick<TIssue, "description_html" | "id" | "name" | "priority" | "sequence_id" | "start_date" | "target_date"> {
|
||||
extends Pick<
|
||||
TIssue,
|
||||
| "description_html"
|
||||
| "created_by"
|
||||
| "id"
|
||||
| "name"
|
||||
| "priority"
|
||||
| "state_id"
|
||||
| "project_id"
|
||||
| "sequence_id"
|
||||
| "sort_order"
|
||||
| "start_date"
|
||||
| "target_date"
|
||||
| "cycle_id"
|
||||
| "module_ids"
|
||||
| "label_ids"
|
||||
| "assignee_ids"
|
||||
> {
|
||||
comments: Comment[];
|
||||
label_details: any;
|
||||
project: string;
|
||||
project_detail: any;
|
||||
reactions: IIssueReaction[];
|
||||
state: string;
|
||||
state_detail: {
|
||||
id: string;
|
||||
name: string;
|
||||
group: TIssueGroupKey;
|
||||
color: string;
|
||||
};
|
||||
votes: IVote[];
|
||||
reaction_items: IIssueReaction[];
|
||||
vote_items: IVote[];
|
||||
}
|
||||
|
||||
export type IPeekMode = "side" | "modal" | "full";
|
||||
|
||||
type TIssueResponseResults =
|
||||
| IIssue[]
|
||||
| {
|
||||
[key: string]: {
|
||||
results:
|
||||
| IIssue[]
|
||||
| {
|
||||
[key: string]: {
|
||||
results: IIssue[];
|
||||
total_results: number;
|
||||
};
|
||||
};
|
||||
total_results: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TIssuesResponse = {
|
||||
grouped_by: string;
|
||||
next_cursor: string;
|
||||
prev_cursor: string;
|
||||
next_page_results: boolean;
|
||||
prev_page_results: boolean;
|
||||
total_count: number;
|
||||
count: number;
|
||||
total_pages: number;
|
||||
extra_stats: null;
|
||||
results: TIssueResponseResults;
|
||||
};
|
||||
|
||||
export interface IIssueLabel {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -66,12 +96,8 @@ export interface IIssueLabel {
|
|||
}
|
||||
|
||||
export interface IVote {
|
||||
issue: string;
|
||||
vote: -1 | 1;
|
||||
workspace: string;
|
||||
project: string;
|
||||
actor: string;
|
||||
actor_detail: ActorDetail;
|
||||
actor_details: ActorDetail;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
|
|
@ -102,9 +128,7 @@ export interface Comment {
|
|||
}
|
||||
|
||||
export interface IIssueReaction {
|
||||
actor_detail: ActorDetail;
|
||||
id: string;
|
||||
issue: string;
|
||||
actor_details: ActorDetail;
|
||||
reaction: string;
|
||||
}
|
||||
|
||||
|
|
@ -112,8 +136,8 @@ export interface ActorDetail {
|
|||
avatar?: string;
|
||||
display_name?: string;
|
||||
first_name?: string;
|
||||
id?: string;
|
||||
is_bot?: boolean;
|
||||
id?: string;
|
||||
last_name?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,19 +17,16 @@ export const renderEmoji = (
|
|||
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
|
||||
};
|
||||
|
||||
export const groupReactions: (reactions: any[], key: string) => { [key: string]: any[] } = (
|
||||
reactions: any,
|
||||
key: string
|
||||
) => {
|
||||
export const groupReactions = <T extends { reaction: string }>(reactions: T[], key: string) => {
|
||||
const groupedReactions = reactions.reduce(
|
||||
(acc: any, reaction: any) => {
|
||||
(acc: { [key: string]: T[] }, reaction: any) => {
|
||||
if (!acc[reaction[key]]) {
|
||||
acc[reaction[key]] = [];
|
||||
}
|
||||
acc[reaction[key]].push(reaction);
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: any[] }
|
||||
{} as { [key: string]: T[] }
|
||||
);
|
||||
|
||||
return groupedReactions;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"@plane/editor": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/constants": "*",
|
||||
"@sentry/nextjs": "^8",
|
||||
"axios": "^1.3.4",
|
||||
"clsx": "^2.0.0",
|
||||
|
|
@ -62,4 +63,4 @@
|
|||
"tailwind-config-custom": "*",
|
||||
"tsconfig": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,13 @@
|
|||
import { FC, useCallback, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { EIssueGroupByToServerOptions } from "@plane/constants";
|
||||
import { TGroupedIssues } from "@plane/types";
|
||||
// components
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { CalendarChart } from "@/components/issues";
|
||||
//constants
|
||||
import { EIssuesStoreType, EIssueGroupByToServerOptions } from "@/constants/issue";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// hooks
|
||||
import { useIssues, useUser, useCalendarView } from "@/hooks/store";
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import React, { useCallback, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane constants
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
// hooks
|
||||
import { ChartDataType, GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "@/components/gantt-chart";
|
||||
import { getMonthChartItemPositionWidthInMonth } from "@/components/gantt-chart/views";
|
||||
import { GanttQuickAddIssueForm, IssueGanttBlock } from "@/components/issues";
|
||||
//constants
|
||||
import { ALL_ISSUES, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { getIssueBlocksStructure } from "@/helpers/issue.helper";
|
||||
//hooks
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { useEffect, useRef } from "react";
|
|||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||
import { observer } from "mobx-react";
|
||||
// plane constants
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
// types
|
||||
import {
|
||||
GroupByColumnTypes,
|
||||
|
|
@ -15,8 +17,7 @@ import {
|
|||
} from "@plane/types";
|
||||
// components
|
||||
import { MultipleSelectGroup } from "@/components/core";
|
||||
// constants
|
||||
import { ALL_ISSUES } from "@/constants/issue";
|
||||
|
||||
// hooks
|
||||
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import isEmpty from "lodash/isEmpty";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane constants
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import { IIssueDisplayFilterOptions } from "@plane/types";
|
||||
// hooks
|
||||
// components
|
||||
|
|
@ -12,7 +14,6 @@ import { AllIssueQuickActions } from "@/components/issues/issue-layouts/quick-ac
|
|||
import { SpreadsheetLayoutLoader } from "@/components/ui";
|
||||
// constants
|
||||
import {
|
||||
ALL_ISSUES,
|
||||
EIssueFilterType,
|
||||
EIssueLayoutTypes,
|
||||
EIssuesStoreType,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { FC, useCallback, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane constants
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import { IIssueDisplayFilterOptions } from "@plane/types";
|
||||
// hooks
|
||||
import { ALL_ISSUES, EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// hooks
|
||||
import { useIssues, useUser } from "@/hooks/store";
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ import {
|
|||
TIssueTypeFilters,
|
||||
} from "@plane/types";
|
||||
|
||||
|
||||
export const ALL_ISSUES = "All Issues";
|
||||
|
||||
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
|
||||
"state",
|
||||
"priority",
|
||||
|
|
@ -460,32 +457,6 @@ export const groupReactionEmojis = (reactions: any) => {
|
|||
return groupedEmojis;
|
||||
};
|
||||
|
||||
export enum EIssueGroupByToServerOptions {
|
||||
"state" = "state_id",
|
||||
"priority" = "priority",
|
||||
"labels" = "labels__id",
|
||||
"state_detail.group" = "state__group",
|
||||
"assignees" = "assignees__id",
|
||||
"cycle" = "cycle_id",
|
||||
"module" = "issue_module__module_id",
|
||||
"target_date" = "target_date",
|
||||
"project" = "project_id",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
|
||||
export enum EServerGroupByToFilterOptions {
|
||||
"state_id" = "state",
|
||||
"priority" = "priority",
|
||||
"labels__id" = "labels",
|
||||
"state__group" = "state_group",
|
||||
"assignees__id" = "assignees",
|
||||
"cycle_id" = "cycle",
|
||||
"issue_module__module_id" = "module",
|
||||
"target_date" = "target_date",
|
||||
"project_id" = "project",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
|
||||
export enum EActivityFilterType {
|
||||
COMMENT = "COMMENT",
|
||||
ACTIVITY = "ACTIVITY",
|
||||
|
|
@ -500,4 +471,4 @@ export const ACTIVITY_FILTER_TYPE_OPTIONS = [
|
|||
value: EActivityFilterType.ACTIVITY,
|
||||
label: "Updates",
|
||||
},
|
||||
];
|
||||
];
|
||||
|
|
@ -6,8 +6,10 @@ import set from "lodash/set";
|
|||
import uniq from "lodash/uniq";
|
||||
import update from "lodash/update";
|
||||
import { action, observable, makeObservable, runInAction } from "mobx";
|
||||
// types
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
// plane constants
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import {
|
||||
TIssue,
|
||||
TLoader,
|
||||
|
|
@ -16,7 +18,6 @@ import {
|
|||
ViewFlags,
|
||||
TBulkOperationsPayload,
|
||||
} from "@plane/types";
|
||||
import { ALL_ISSUES } from "@/constants/issue";
|
||||
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
import { ICycleIssuesFilter } from "./filter.store";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import isEmpty from "lodash/isEmpty";
|
||||
import uniq from "lodash/uniq";
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { ALL_ISSUES } from "@/constants/issue";
|
||||
import { EIssueGroupedAction } from "./base-issues.store";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import update from "lodash/update";
|
|||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import {
|
||||
TIssue,
|
||||
TIssueGroupByOptions,
|
||||
|
|
@ -28,7 +29,7 @@ import {
|
|||
TPaginationData,
|
||||
TBulkOperationsPayload,
|
||||
} from "@plane/types";
|
||||
import { ALL_ISSUES, EIssueLayoutTypes, ISSUE_PRIORITIES } from "@/constants/issue";
|
||||
import { EIssueLayoutTypes, ISSUE_PRIORITIES } from "@/constants/issue";
|
||||
import { convertToISODateString } from "@/helpers/date-time.helper";
|
||||
import { CycleService } from "@/services/cycle.service";
|
||||
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import isEmpty from "lodash/isEmpty";
|
||||
import { EIssueGroupByToServerOptions, EServerGroupByToFilterOptions } from "@plane/constants";
|
||||
// types
|
||||
import {
|
||||
IIssueDisplayFilterOptions,
|
||||
|
|
@ -12,12 +13,7 @@ import {
|
|||
TStaticViewTypes,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
import {
|
||||
EIssueFilterType,
|
||||
EIssuesStoreType,
|
||||
EIssueGroupByToServerOptions,
|
||||
EServerGroupByToFilterOptions,
|
||||
} from "@/constants/issue";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||
// helpers
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
|
||||
// lib
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue