[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",
|
"@headlessui/react": "^1.7.19",
|
||||||
"@plane/types": "*",
|
"@plane/types": "*",
|
||||||
"@plane/ui": "*",
|
"@plane/ui": "*",
|
||||||
|
"@plane/constants": "*",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@types/lodash": "^4.17.0",
|
"@types/lodash": "^4.17.0",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
|
|
@ -46,4 +47,4 @@
|
||||||
"tsconfig": "*",
|
"tsconfig": "*",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ class IssueViewSerializer(DynamicBaseSerializer):
|
||||||
]
|
]
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
query_params = validated_data.get("query_data", {})
|
query_params = validated_data.get("filters", {})
|
||||||
if bool(query_params):
|
if bool(query_params):
|
||||||
validated_data["query"] = issue_filters(query_params, "POST")
|
validated_data["query"] = issue_filters(query_params, "POST")
|
||||||
else:
|
else:
|
||||||
|
|
@ -31,7 +31,7 @@ class IssueViewSerializer(DynamicBaseSerializer):
|
||||||
return IssueView.objects.create(**validated_data)
|
return IssueView.objects.create(**validated_data)
|
||||||
|
|
||||||
def update(self, instance, 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):
|
if bool(query_params):
|
||||||
validated_data["query"] = issue_filters(query_params, "POST")
|
validated_data["query"] = issue_filters(query_params, "POST")
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -1391,6 +1391,7 @@ def create_issue_relation_activity(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
comment=f"added {requested_data.get('relation_type')} relation",
|
comment=f"added {requested_data.get('relation_type')} relation",
|
||||||
old_identifier=related_issue,
|
old_identifier=related_issue,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
issue = Issue.objects.get(pk=issue_id)
|
issue = Issue.objects.get(pk=issue_id)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from django.db import models
|
||||||
from .base import BaseModel
|
from .base import BaseModel
|
||||||
from .project import ProjectBaseModel
|
from .project import ProjectBaseModel
|
||||||
from .workspace import WorkspaceBaseModel
|
from .workspace import WorkspaceBaseModel
|
||||||
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
def get_default_filters():
|
def get_default_filters():
|
||||||
|
|
@ -116,6 +117,26 @@ class IssueView(WorkspaceBaseModel):
|
||||||
db_table = "issue_views"
|
db_table = "issue_views"
|
||||||
ordering = ("-created_at",)
|
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):
|
def __str__(self):
|
||||||
"""Return name of the View"""
|
"""Return name of the View"""
|
||||||
return f"{self.name} <{self.project.name}>"
|
return f"{self.name} <{self.project.name}>"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
|
|
||||||
from .issue import LabelLiteSerializer, StateLiteSerializer
|
from .issue import (
|
||||||
|
LabelLiteSerializer,
|
||||||
|
StateLiteSerializer,
|
||||||
|
IssuePublicSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
from .state import StateSerializer, StateLiteSerializer
|
from .state import StateSerializer, StateLiteSerializer
|
||||||
|
|
|
||||||
|
|
@ -188,11 +188,16 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
|
||||||
class IssueReactionSerializer(BaseSerializer):
|
class IssueReactionSerializer(BaseSerializer):
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueReaction
|
model = IssueReaction
|
||||||
fields = "__all__"
|
fields = [
|
||||||
|
"issue",
|
||||||
|
"reaction",
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"actor",
|
||||||
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
|
|
@ -454,20 +459,6 @@ class IssueCreateSerializer(BaseSerializer):
|
||||||
return super().update(instance, validated_data)
|
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 CommentReactionSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CommentReaction
|
model = CommentReaction
|
||||||
|
|
@ -476,7 +467,6 @@ class CommentReactionSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
|
||||||
class IssueVoteSerializer(BaseSerializer):
|
class IssueVoteSerializer(BaseSerializer):
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueVote
|
model = IssueVote
|
||||||
|
|
@ -486,35 +476,45 @@ class IssueVoteSerializer(BaseSerializer):
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"actor",
|
"actor",
|
||||||
"actor_detail",
|
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
class IssuePublicSerializer(BaseSerializer):
|
class IssuePublicSerializer(BaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
|
||||||
reactions = IssueReactionSerializer(
|
reactions = IssueReactionSerializer(
|
||||||
read_only=True, many=True, source="issue_reactions"
|
read_only=True, many=True, source="issue_reactions"
|
||||||
)
|
)
|
||||||
votes = IssueVoteSerializer(read_only=True, many=True)
|
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:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"description_html",
|
|
||||||
"sequence_id",
|
"sequence_id",
|
||||||
"state",
|
"state",
|
||||||
"state_detail",
|
|
||||||
"project",
|
"project",
|
||||||
"project_detail",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"priority",
|
"priority",
|
||||||
"target_date",
|
"target_date",
|
||||||
"reactions",
|
"reactions",
|
||||||
"votes",
|
"votes",
|
||||||
|
"module_ids",
|
||||||
|
"created_by",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ from plane.space.views import (
|
||||||
ProjectDeployBoardPublicSettingsEndpoint,
|
ProjectDeployBoardPublicSettingsEndpoint,
|
||||||
ProjectIssuesPublicEndpoint,
|
ProjectIssuesPublicEndpoint,
|
||||||
WorkspaceProjectAnchorEndpoint,
|
WorkspaceProjectAnchorEndpoint,
|
||||||
|
ProjectCyclesEndpoint,
|
||||||
|
ProjectModulesEndpoint,
|
||||||
|
ProjectStatesEndpoint,
|
||||||
|
ProjectLabelsEndpoint,
|
||||||
|
ProjectMembersEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
@ -23,4 +28,29 @@ urlpatterns = [
|
||||||
WorkspaceProjectAnchorEndpoint.as_view(),
|
WorkspaceProjectAnchorEndpoint.as_view(),
|
||||||
name="project-deploy-board",
|
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,
|
ProjectDeployBoardPublicSettingsEndpoint,
|
||||||
WorkspaceProjectDeployBoardEndpoint,
|
WorkspaceProjectDeployBoardEndpoint,
|
||||||
WorkspaceProjectAnchorEndpoint,
|
WorkspaceProjectAnchorEndpoint,
|
||||||
|
ProjectMembersEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .issue import (
|
from .issue import (
|
||||||
|
|
@ -14,3 +15,11 @@ from .issue import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from .inbox import InboxIssuePublicViewSet
|
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
|
# Python imports
|
||||||
import json
|
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.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Exists,
|
Exists,
|
||||||
F,
|
F,
|
||||||
Func,
|
|
||||||
OuterRef,
|
|
||||||
Q,
|
Q,
|
||||||
Prefetch,
|
Prefetch,
|
||||||
|
UUIDField,
|
||||||
Case,
|
Case,
|
||||||
When,
|
When,
|
||||||
CharField,
|
JSONField,
|
||||||
IntegerField,
|
|
||||||
Value,
|
Value,
|
||||||
Max,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
|
|
@ -25,6 +24,22 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework.response import Response
|
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 (
|
from plane.app.serializers import (
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
|
|
@ -36,21 +51,160 @@ from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
IssueComment,
|
IssueComment,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
|
||||||
ProjectMember,
|
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
|
ProjectMember,
|
||||||
CommentReaction,
|
CommentReaction,
|
||||||
DeployBoard,
|
DeployBoard,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
ProjectPublicMember,
|
ProjectPublicMember,
|
||||||
State,
|
|
||||||
Label,
|
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
# 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):
|
class IssueCommentPublicViewSet(BaseViewSet):
|
||||||
|
|
@ -503,67 +657,50 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, anchor, issue_id):
|
def get(self, request, anchor, issue_id):
|
||||||
project_deploy_board = DeployBoard.objects.get(
|
deploy_board = DeployBoard.objects.get(anchor=anchor)
|
||||||
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
|
|
||||||
|
|
||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
Issue.issue_objects.annotate(
|
Issue.issue_objects.filter(
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
pk=issue_id,
|
||||||
parent=OuterRef("id")
|
workspace__slug=deploy_board.workspace.slug,
|
||||||
)
|
project_id=deploy_board.project_id,
|
||||||
.order_by()
|
)
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.values("count")
|
.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_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_reactions",
|
"issue_reactions",
|
||||||
queryset=IssueReaction.objects.select_related("actor"),
|
queryset=IssueReaction.objects.select_related(
|
||||||
|
"issue", "actor"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
|
|
@ -572,124 +709,91 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||||
queryset=IssueVote.objects.select_related("actor"),
|
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(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
vote_items=ArrayAgg(
|
||||||
.order_by()
|
Case(
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
When(
|
||||||
.values("count")
|
votes__isnull=False,
|
||||||
)
|
then=JSONObject(
|
||||||
.annotate(
|
vote=F("votes__vote"),
|
||||||
attachment_count=IssueAttachment.objects.filter(
|
actor_details=JSONObject(
|
||||||
issue=OuterRef("id")
|
id=F("votes__actor__id"),
|
||||||
)
|
first_name=F("votes__actor__first_name"),
|
||||||
.order_by()
|
last_name=F("votes__actor__last_name"),
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
avatar=F("votes__actor__avatar"),
|
||||||
.values("count")
|
display_name=F(
|
||||||
)
|
"votes__actor__display_name"
|
||||||
)
|
),
|
||||||
|
),
|
||||||
# Priority Ordering
|
),
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
),
|
||||||
priority_order = (
|
default=None,
|
||||||
priority_order
|
output_field=JSONField(),
|
||||||
if order_by_param == "priority"
|
),
|
||||||
else priority_order[::-1]
|
filter=Case(
|
||||||
)
|
When(votes__isnull=False, then=True),
|
||||||
issue_queryset = issue_queryset.annotate(
|
default=False,
|
||||||
priority_order=Case(
|
output_field=JSONField(),
|
||||||
*[
|
),
|
||||||
When(priority=p, then=Value(i))
|
distinct=True,
|
||||||
for i, p in enumerate(priority_order)
|
),
|
||||||
],
|
reaction_items=ArrayAgg(
|
||||||
output_field=CharField(),
|
Case(
|
||||||
)
|
When(
|
||||||
).order_by("priority_order")
|
issue_reactions__isnull=False,
|
||||||
|
then=JSONObject(
|
||||||
# State Ordering
|
reaction=F("issue_reactions__reaction"),
|
||||||
elif order_by_param in [
|
actor_details=JSONObject(
|
||||||
"state__name",
|
id=F("issue_reactions__actor__id"),
|
||||||
"state__group",
|
first_name=F(
|
||||||
"-state__name",
|
"issue_reactions__actor__first_name"
|
||||||
"-state__group",
|
),
|
||||||
]:
|
last_name=F(
|
||||||
state_order = (
|
"issue_reactions__actor__last_name"
|
||||||
state_order
|
),
|
||||||
if order_by_param in ["state__name", "state__group"]
|
avatar=F("issue_reactions__actor__avatar"),
|
||||||
else state_order[::-1]
|
display_name=F(
|
||||||
)
|
"issue_reactions__actor__display_name"
|
||||||
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=None,
|
||||||
],
|
output_field=JSONField(),
|
||||||
default=Value(len(state_order)),
|
),
|
||||||
output_field=CharField(),
|
filter=Case(
|
||||||
)
|
When(issue_reactions__isnull=False, then=True),
|
||||||
).order_by("state_order")
|
default=False,
|
||||||
# assignee and label ordering
|
output_field=JSONField(),
|
||||||
elif order_by_param in [
|
),
|
||||||
"labels__name",
|
distinct=True,
|
||||||
"-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(),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.values("name", "group", "color", "id")
|
.values(
|
||||||
.order_by("custom_order", "sequence")
|
"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(
|
return Response(issue_queryset, status=status.HTTP_200_OK)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
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
|
# Module imports
|
||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
from plane.app.serializers import DeployBoardSerializer
|
from plane.app.serializers import DeployBoardSerializer
|
||||||
from plane.db.models import (
|
from plane.db.models import Project, DeployBoard, ProjectMember
|
||||||
Project,
|
|
||||||
DeployBoard,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
|
class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
|
||||||
|
|
@ -76,3 +73,27 @@ class WorkspaceProjectAnchorEndpoint(BaseAPIView):
|
||||||
)
|
)
|
||||||
serializer = DeployBoardSerializer(project_deploy_board)
|
serializer = DeployBoardSerializer(project_deploy_board)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
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:
|
if len(priorities) and "" not in priorities:
|
||||||
filter[f"{prefix}priority__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
|
return filter
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from "./auth";
|
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 { observer } from "mobx-react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import useSWR from "swr";
|
||||||
// components
|
// components
|
||||||
import { IssuesLayoutsRoot } from "@/components/issues";
|
import { IssuesLayoutsRoot } from "@/components/issues";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePublish } from "@/hooks/store";
|
import { usePublish, useLabel, useStates } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -19,6 +20,12 @@ const IssuesPage = observer((props: Props) => {
|
||||||
// params
|
// params
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const peekId = searchParams.get("peekId") || undefined;
|
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);
|
const publishSettings = usePublish(anchor);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IStateLite } from "@plane/types";
|
import { TFilters } from "@/types/issue";
|
||||||
import { IIssueLabel, TFilters } from "@/types/issue";
|
|
||||||
// components
|
// components
|
||||||
import { AppliedPriorityFilters } from "./priority";
|
import { AppliedPriorityFilters } from "./priority";
|
||||||
import { AppliedStateFilters } from "./state";
|
import { AppliedStateFilters } from "./state";
|
||||||
|
|
@ -13,14 +12,12 @@ type Props = {
|
||||||
appliedFilters: TFilters;
|
appliedFilters: TFilters;
|
||||||
handleRemoveAllFilters: () => void;
|
handleRemoveAllFilters: () => void;
|
||||||
handleRemoveFilter: (key: keyof TFilters, value: string | null) => 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 replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
|
||||||
|
|
||||||
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||||
const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter, states } = props;
|
const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-stretch gap-2">
|
<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
|
<AppliedStateFilters
|
||||||
handleRemove={(val) => handleRemoveFilter("state", val)}
|
handleRemove={(val) => handleRemoveFilter("state", val)}
|
||||||
states={states}
|
|
||||||
values={filterValue ?? []}
|
values={filterValue ?? []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import cloneDeep from "lodash/cloneDeep";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssue, useIssueFilter } from "@/hooks/store";
|
import { useIssueFilter } from "@/hooks/store";
|
||||||
// store
|
// store
|
||||||
import { TIssueQueryFilters } from "@/types/issue";
|
import { TIssueQueryFilters } from "@/types/issue";
|
||||||
// components
|
// components
|
||||||
|
|
@ -21,7 +21,6 @@ export const IssueAppliedFilters: FC<TIssueAppliedFilters> = observer((props) =>
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getIssueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter();
|
const { getIssueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter();
|
||||||
const { states, labels } = useIssue();
|
|
||||||
// derived values
|
// derived values
|
||||||
const issueFilters = getIssueFilters(anchor);
|
const issueFilters = getIssueFilters(anchor);
|
||||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||||
|
|
@ -65,14 +64,18 @@ export const IssueAppliedFilters: FC<TIssueAppliedFilters> = observer((props) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRemoveAllFilters = () => {
|
const handleRemoveAllFilters = () => {
|
||||||
initIssueFilters(anchor, {
|
initIssueFilters(
|
||||||
display_filters: { layout: activeLayout || "list" },
|
anchor,
|
||||||
filters: {
|
{
|
||||||
state: [],
|
display_filters: { layout: activeLayout || "list" },
|
||||||
priority: [],
|
filters: {
|
||||||
labels: [],
|
state: [],
|
||||||
|
priority: [],
|
||||||
|
labels: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
true
|
||||||
|
);
|
||||||
|
|
||||||
router.push(`/issues/${anchor}?${`board=${activeLayout || "list"}`}`);
|
router.push(`/issues/${anchor}?${`board=${activeLayout || "list"}`}`);
|
||||||
};
|
};
|
||||||
|
|
@ -85,8 +88,6 @@ export const IssueAppliedFilters: FC<TIssueAppliedFilters> = observer((props) =>
|
||||||
appliedFilters={appliedFilters || {}}
|
appliedFilters={appliedFilters || {}}
|
||||||
handleRemoveFilter={handleFilters as any}
|
handleRemoveFilter={handleFilters as any}
|
||||||
handleRemoveAllFilters={handleRemoveAllFilters}
|
handleRemoveAllFilters={handleRemoveAllFilters}
|
||||||
labels={labels ?? []}
|
|
||||||
states={states ?? []}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,20 @@
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
// types
|
|
||||||
import { IStateLite } from "@plane/types";
|
|
||||||
// ui
|
// ui
|
||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useStates } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleRemove: (val: string) => void;
|
handleRemove: (val: string) => void;
|
||||||
states: IStateLite[];
|
|
||||||
values: string[];
|
values: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
||||||
const { handleRemove, states, values } = props;
|
const { handleRemove, values } = props;
|
||||||
|
|
||||||
|
const { states } = useStates();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
// helpers
|
// helpers
|
||||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssue, useIssueFilter } from "@/hooks/store";
|
import { useIssueFilter } from "@/hooks/store";
|
||||||
// types
|
// types
|
||||||
import { TIssueQueryFilters } from "@/types/issue";
|
import { TIssueQueryFilters } from "@/types/issue";
|
||||||
|
|
||||||
|
|
@ -26,7 +26,6 @@ export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((pro
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// hooks
|
// hooks
|
||||||
const { getIssueFilters, updateIssueFilters } = useIssueFilter();
|
const { getIssueFilters, updateIssueFilters } = useIssueFilter();
|
||||||
const { states, labels } = useIssue();
|
|
||||||
// derived values
|
// derived values
|
||||||
const issueFilters = getIssueFilters(anchor);
|
const issueFilters = getIssueFilters(anchor);
|
||||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||||
|
|
@ -65,8 +64,6 @@ export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((pro
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
handleFilters={handleFilters as any}
|
handleFilters={handleFilters as any}
|
||||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[activeLayout]?.filters : []}
|
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[activeLayout]?.filters : []}
|
||||||
states={states ?? undefined}
|
|
||||||
labels={labels ?? undefined}
|
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ import React, { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Search, X } from "lucide-react";
|
import { Search, X } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IStateLite } from "@plane/types";
|
import { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
|
||||||
import { IIssueLabel, IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
|
|
||||||
// components
|
// components
|
||||||
import { FilterPriority, FilterState } from ".";
|
import { FilterPriority, FilterState } from ".";
|
||||||
|
|
||||||
|
|
@ -13,12 +12,10 @@ type Props = {
|
||||||
filters: IIssueFilterOptions;
|
filters: IIssueFilterOptions;
|
||||||
handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||||
layoutDisplayFiltersOptions: TIssueFilterKeys[];
|
layoutDisplayFiltersOptions: TIssueFilterKeys[];
|
||||||
labels?: IIssueLabel[] | undefined;
|
|
||||||
states?: IStateLite[] | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FilterSelection: React.FC<Props> = observer((props) => {
|
export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||||
const { filters, handleFilters, layoutDisplayFiltersOptions, states } = props;
|
const { filters, handleFilters, layoutDisplayFiltersOptions } = props;
|
||||||
|
|
||||||
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
||||||
|
|
||||||
|
|
@ -63,7 +60,6 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||||
appliedFilters={filters.state ?? null}
|
appliedFilters={filters.state ?? null}
|
||||||
handleUpdate={(val) => handleFilters("state", val)}
|
handleUpdate={(val) => handleFilters("state", val)}
|
||||||
searchQuery={filtersSearchQuery}
|
searchQuery={filtersSearchQuery}
|
||||||
states={states}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,24 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
// types
|
import { observer } from "mobx-react";
|
||||||
import { IStateLite } from "@plane/types";
|
|
||||||
// ui
|
// ui
|
||||||
import { Loader, StateGroupIcon } from "@plane/ui";
|
import { Loader, StateGroupIcon } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers";
|
import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers";
|
||||||
|
// hooks
|
||||||
|
import { useStates } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appliedFilters: string[] | null;
|
appliedFilters: string[] | null;
|
||||||
handleUpdate: (val: string) => void;
|
handleUpdate: (val: string) => void;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
states: IStateLite[] | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FilterState: React.FC<Props> = (props) => {
|
export const FilterState: React.FC<Props> = observer((props) => {
|
||||||
const { appliedFilters, handleUpdate, searchQuery, states } = props;
|
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||||
|
|
||||||
|
const { states } = useStates();
|
||||||
|
|
||||||
const [itemsToRender, setItemsToRender] = useState(5);
|
const [itemsToRender, setItemsToRender] = useState(5);
|
||||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
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
|
// helpers
|
||||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetails, usePublish } from "@/hooks/store";
|
import { useIssue, useIssueDetails, usePublish } from "@/hooks/store";
|
||||||
// interfaces
|
// interfaces
|
||||||
import { IIssue } from "@/types/issue";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
anchor: string;
|
anchor: string;
|
||||||
issue: IIssue;
|
issueId: string;
|
||||||
params: any;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueKanBanBlock: FC<Props> = observer((props) => {
|
export const IssueKanBanBlock: FC<Props> = observer((props) => {
|
||||||
const { anchor, issue } = props;
|
const { anchor, issueId } = props;
|
||||||
|
const { getIssueById } = useIssue();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
// query params
|
// query params
|
||||||
const board = searchParams.get("board");
|
const board = searchParams.get("board");
|
||||||
|
|
@ -31,12 +30,16 @@ export const IssueKanBanBlock: FC<Props> = observer((props) => {
|
||||||
const { project_details } = usePublish(anchor);
|
const { project_details } = usePublish(anchor);
|
||||||
const { setPeekId } = useIssueDetails();
|
const { setPeekId } = useIssueDetails();
|
||||||
|
|
||||||
const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels });
|
const { queryParam } = queryParamGenerator({ board, peekId: issueId, priority, state, labels });
|
||||||
|
|
||||||
const handleBlockClick = () => {
|
const handleBlockClick = () => {
|
||||||
setPeekId(issue.id);
|
setPeekId(issueId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
|
if (!issue) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/issues/${anchor}?${queryParam}`}
|
href={`/issues/${anchor}?${queryParam}`}
|
||||||
|
|
@ -61,15 +64,15 @@ export const IssueKanBanBlock: FC<Props> = observer((props) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* state */}
|
{/* state */}
|
||||||
{issue?.state_detail && (
|
{issue?.state_id && (
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<IssueBlockState state={issue?.state_detail} />
|
<IssueBlockState stateId={issue?.state_id} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* due date */}
|
{/* due date */}
|
||||||
{issue?.target_date && (
|
{issue?.target_date && (
|
||||||
<div className="flex-shrink-0">
|
<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>
|
||||||
)}
|
)}
|
||||||
</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";
|
"use client";
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// types
|
|
||||||
import { IStateLite } from "@plane/types";
|
|
||||||
// ui
|
// ui
|
||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useIssue, useStates } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
state: IStateLite;
|
stateId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueKanBanHeader: React.FC<Props> = observer((props) => {
|
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 (
|
return (
|
||||||
<div className="flex items-center gap-2 px-2 pb-2">
|
<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">
|
<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>
|
||||||
<div className="mr-1 truncate font-medium capitalize text-custom-text-200">{state?.name}</div>
|
<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">{getCountOfIssuesByState(state.id)}</span> */}
|
<span className="flex-shrink-0 rounded-full text-custom-text-300">
|
||||||
|
{getGroupIssueCount(stateId, undefined, false) ?? 0}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// components
|
|
||||||
import { IssueKanBanBlock, IssueKanBanHeader } from "@/components/issues";
|
|
||||||
// ui
|
|
||||||
import { Icon } from "@/components/ui";
|
|
||||||
// mobx hook
|
// mobx hook
|
||||||
|
import { TGroupedIssues } from "@plane/types";
|
||||||
|
// hooks
|
||||||
import { useIssue } from "@/hooks/store";
|
import { useIssue } from "@/hooks/store";
|
||||||
|
import { Column } from "./column";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
anchor: string;
|
anchor: string;
|
||||||
|
|
@ -16,34 +15,19 @@ type Props = {
|
||||||
export const IssueKanbanLayoutRoot: FC<Props> = observer((props) => {
|
export const IssueKanbanLayoutRoot: FC<Props> = observer((props) => {
|
||||||
const { anchor } = props;
|
const { anchor } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { states, getFilteredIssuesByState } = useIssue();
|
const { groupedIssueIds } = useIssue();
|
||||||
|
|
||||||
|
const groupedIssues = groupedIssueIds as TGroupedIssues | undefined;
|
||||||
|
|
||||||
|
if (!groupedIssues) return <></>;
|
||||||
|
|
||||||
|
const issueGroupIds = Object.keys(groupedIssues);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full gap-3 overflow-hidden overflow-x-auto">
|
<div className="relative flex h-full w-full gap-3 overflow-hidden overflow-x-auto">
|
||||||
{states?.map((state) => {
|
{issueGroupIds?.map((stateId) => {
|
||||||
const issues = getFilteredIssuesByState(state.id);
|
const issueIds = groupedIssues[stateId];
|
||||||
|
return <Column key={stateId} anchor={anchor} stateId={stateId} issueIds={issueIds} />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,16 @@ import { IssueBlockDueDate, IssueBlockLabels, IssueBlockPriority, IssueBlockStat
|
||||||
// helpers
|
// helpers
|
||||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||||
// hook
|
// hook
|
||||||
import { useIssueDetails, usePublish } from "@/hooks/store";
|
import { useIssue, useIssueDetails, usePublish } from "@/hooks/store";
|
||||||
// types
|
|
||||||
import { IIssue } from "@/types/issue";
|
|
||||||
|
|
||||||
type IssueListBlockProps = {
|
type IssueListBlockProps = {
|
||||||
anchor: string;
|
anchor: string;
|
||||||
issue: IIssue;
|
issueId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueListLayoutBlock: FC<IssueListBlockProps> = observer((props) => {
|
export const IssueListLayoutBlock: FC<IssueListBlockProps> = observer((props) => {
|
||||||
const { anchor, issue } = props;
|
const { anchor, issueId } = props;
|
||||||
|
const { getIssueById } = useIssue();
|
||||||
// query params
|
// query params
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const board = searchParams.get("board") || undefined;
|
const board = searchParams.get("board") || undefined;
|
||||||
|
|
@ -29,11 +28,15 @@ export const IssueListLayoutBlock: FC<IssueListBlockProps> = observer((props) =>
|
||||||
const { setPeekId } = useIssueDetails();
|
const { setPeekId } = useIssueDetails();
|
||||||
const { project_details } = usePublish(anchor);
|
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 = () => {
|
const handleBlockClick = () => {
|
||||||
setPeekId(issue.id);
|
setPeekId(issueId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
|
if (!issue) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/issues/${anchor}?${queryParam}`}
|
href={`/issues/${anchor}?${queryParam}`}
|
||||||
|
|
@ -60,23 +63,23 @@ export const IssueListLayoutBlock: FC<IssueListBlockProps> = observer((props) =>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* state */}
|
{/* state */}
|
||||||
{issue?.state_detail && (
|
{issue?.state_id && (
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<IssueBlockState state={issue?.state_detail} />
|
<IssueBlockState stateId={issue?.state_id} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* labels */}
|
{/* labels */}
|
||||||
{issue?.label_details && issue?.label_details.length > 0 && (
|
{issue?.label_ids && issue?.label_ids.length > 0 && (
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<IssueBlockLabels labels={issue?.label_details} />
|
<IssueBlockLabels labelIds={issue?.label_ids} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* due date */}
|
{/* due date */}
|
||||||
{issue?.target_date && (
|
{issue?.target_date && (
|
||||||
<div className="flex-shrink-0">
|
<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>
|
||||||
)}
|
)}
|
||||||
</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 React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// types
|
|
||||||
import { IStateLite } from "@plane/types";
|
|
||||||
// ui
|
// ui
|
||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useIssue, useStates } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
state: IStateLite;
|
stateId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueListLayoutHeader: React.FC<Props> = observer((props) => {
|
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 (
|
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">
|
<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>
|
||||||
<div className="mr-1 font-medium capitalize">{state?.name}</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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// components
|
// types
|
||||||
import { IssueListLayoutBlock, IssueListLayoutHeader } from "@/components/issues";
|
import { TGroupedIssues } from "@plane/types";
|
||||||
// mobx hook
|
// mobx hook
|
||||||
import { useIssue } from "@/hooks/store";
|
import { useIssue } from "@/hooks/store";
|
||||||
|
import { Group } from "./group";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
anchor: string;
|
anchor: string;
|
||||||
|
|
@ -13,27 +14,20 @@ type Props = {
|
||||||
export const IssuesListLayoutRoot: FC<Props> = observer((props) => {
|
export const IssuesListLayoutRoot: FC<Props> = observer((props) => {
|
||||||
const { anchor } = props;
|
const { anchor } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { states, getFilteredIssuesByState } = useIssue();
|
const { groupedIssueIds } = useIssue();
|
||||||
|
|
||||||
|
const groupedIssues = groupedIssueIds as TGroupedIssues | undefined;
|
||||||
|
|
||||||
|
if (!groupedIssues) return <></>;
|
||||||
|
|
||||||
|
const issueGroupIds = Object.keys(groupedIssues);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{states?.map((state) => {
|
{issueGroupIds?.map((stateId) => {
|
||||||
const issues = getFilteredIssuesByState(state.id);
|
const issueIds = groupedIssues[stateId];
|
||||||
|
|
||||||
return (
|
return <Group key={stateId} anchor={anchor} stateId={stateId} issueIds={issueIds} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,31 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { observer } from "mobx-react";
|
||||||
import { CalendarCheck2 } from "lucide-react";
|
import { CalendarCheck2 } from "lucide-react";
|
||||||
// types
|
|
||||||
import { TStateGroups } from "@plane/types";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||||
|
// hooks
|
||||||
|
import { useStates } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
due_date: string;
|
due_date: string;
|
||||||
group: TStateGroups;
|
stateId: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueBlockDueDate = (props: Props) => {
|
export const IssueBlockDueDate = observer((props: Props) => {
|
||||||
const { due_date, group } = props;
|
const { due_date, stateId } = props;
|
||||||
|
const { getStateById } = useStates();
|
||||||
|
|
||||||
|
const state = getStateById(stateId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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",
|
"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)}
|
{renderFormattedDate(due_date)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,41 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
export const IssueBlockLabels = ({ labels }: any) => (
|
import { observer } from "mobx-react";
|
||||||
<div className="relative flex flex-wrap items-center gap-1">
|
import { Tooltip } from "@plane/ui";
|
||||||
{labels?.map((_label: any) => (
|
import { useLabel } from "@/hooks/store";
|
||||||
<div
|
|
||||||
key={_label?.id}
|
type Props = {
|
||||||
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"
|
labelIds: string[];
|
||||||
>
|
};
|
||||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
|
||||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: `${_label?.color}` }} />
|
export const IssueBlockLabels = observer(({ labelIds }: Props) => {
|
||||||
<div className="text-xs">{_label?.name}</div>
|
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>
|
<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";
|
"use client";
|
||||||
|
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// ui
|
// ui
|
||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
|
//hooks
|
||||||
|
import { useStates } from "@/hooks/store";
|
||||||
|
|
||||||
export const IssueBlockState = ({ state }: any) => (
|
type Props = {
|
||||||
<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">
|
stateId: string;
|
||||||
<div className="flex w-full items-center gap-1.5">
|
};
|
||||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
export const IssueBlockState = observer(({ stateId }: Props) => {
|
||||||
<div className="text-xs">{state?.name}</div>
|
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>
|
||||||
</div>
|
);
|
||||||
);
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { FC, useEffect } from "react";
|
import { FC, useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useSearchParams } from "next/navigation";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// components
|
// components
|
||||||
import { IssueKanbanLayoutRoot, IssuesListLayoutRoot } from "@/components/issues";
|
import { IssueKanbanLayoutRoot, IssuesListLayoutRoot } from "@/components/issues";
|
||||||
|
|
@ -23,22 +22,22 @@ type Props = {
|
||||||
|
|
||||||
export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
|
export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
|
||||||
const { peekId, publishSettings } = 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
|
// store hooks
|
||||||
const { getIssueFilters } = useIssueFilter();
|
const { getIssueFilters } = useIssueFilter();
|
||||||
const { loader, issues, error, fetchPublicIssues } = useIssue();
|
const { loader, groupedIssueIds, fetchPublicIssues } = useIssue();
|
||||||
const issueDetailStore = useIssueDetails();
|
const issueDetailStore = useIssueDetails();
|
||||||
// derived values
|
// derived values
|
||||||
const { anchor } = publishSettings;
|
const { anchor } = publishSettings;
|
||||||
const issueFilters = anchor ? getIssueFilters(anchor) : undefined;
|
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 ? `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(() => {
|
useEffect(() => {
|
||||||
|
|
@ -47,16 +46,13 @@ export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
|
||||||
}
|
}
|
||||||
}, [peekId, issueDetailStore]);
|
}, [peekId, issueDetailStore]);
|
||||||
|
|
||||||
// derived values
|
|
||||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
|
||||||
|
|
||||||
if (!anchor) return null;
|
if (!anchor) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full overflow-hidden">
|
<div className="relative h-full w-full overflow-hidden">
|
||||||
{peekId && <IssuePeekOverview anchor={anchor} peekId={peekId} />}
|
{peekId && <IssuePeekOverview anchor={anchor} peekId={peekId} />}
|
||||||
|
|
||||||
{loader && !issues ? (
|
{loader && !groupedIssueIds ? (
|
||||||
<div className="py-10 text-center text-sm text-custom-text-100">Loading...</div>
|
<div className="py-10 text-center text-sm text-custom-text-100">Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
import { RichTextReadOnlyEditor } from "@/components/editor";
|
import { RichTextReadOnlyEditor } from "@/components/editor";
|
||||||
import { IssueReactions } from "@/components/issues/peek-overview";
|
import { IssueReactions } from "@/components/issues/peek-overview";
|
||||||
|
import { usePublish } from "@/hooks/store";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "@/types/issue";
|
import { IIssue } from "@/types/issue";
|
||||||
|
|
||||||
|
|
@ -9,15 +11,17 @@ type Props = {
|
||||||
issueDetails: IIssue;
|
issueDetails: IIssue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PeekOverviewIssueDetails: React.FC<Props> = (props) => {
|
export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => {
|
||||||
const { anchor, issueDetails } = props;
|
const { anchor, issueDetails } = props;
|
||||||
|
|
||||||
|
const { project_details } = usePublish(anchor);
|
||||||
|
|
||||||
const description = issueDetails.description_html;
|
const description = issueDetails.description_html;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h6 className="text-base font-medium text-custom-text-400">
|
<h6 className="text-base font-medium text-custom-text-400">
|
||||||
{issueDetails.project_detail?.identifier}-{issueDetails?.sequence_id}
|
{project_details?.identifier}-{issueDetails?.sequence_id}
|
||||||
</h6>
|
</h6>
|
||||||
<h4 className="break-words text-2xl font-medium">{issueDetails.name}</h4>
|
<h4 className="break-words text-2xl font-medium">{issueDetails.name}</h4>
|
||||||
{description !== "" && description !== "<p></p>" && (
|
{description !== "" && description !== "<p></p>" && (
|
||||||
|
|
@ -34,4 +38,4 @@ export const PeekOverviewIssueDetails: React.FC<Props> = (props) => {
|
||||||
<IssueReactions anchor={anchor} />
|
<IssueReactions anchor={anchor} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,10 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||||
const { data: user } = useUser();
|
const { data: user } = useUser();
|
||||||
|
|
||||||
const issueId = issueDetailsStore.peekId;
|
const issueId = issueDetailsStore.peekId;
|
||||||
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
|
const reactions = issueDetailsStore.details[issueId ?? ""]?.reaction_items ?? [];
|
||||||
const groupedReactions = groupReactions(reactions, "reaction");
|
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) => {
|
const handleAddReaction = (reactionHex: string) => {
|
||||||
if (!issueId) return;
|
if (!issueId) return;
|
||||||
|
|
@ -48,7 +48,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReactionClick = (reactionHex: string) => {
|
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);
|
if (userReaction) handleRemoveReaction(reactionHex);
|
||||||
else handleAddReaction(reactionHex);
|
else handleAddReaction(reactionHex);
|
||||||
};
|
};
|
||||||
|
|
@ -78,9 +78,9 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<div>
|
<div>
|
||||||
{reactions
|
{reactions
|
||||||
.map((r) => r.actor_detail.display_name)
|
?.map((r) => r?.actor_details?.display_name)
|
||||||
.splice(0, REACTIONS_LIMIT)
|
?.splice(0, REACTIONS_LIMIT)
|
||||||
.join(", ")}
|
?.join(", ")}
|
||||||
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +92,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
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 ${
|
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-primary-100/10"
|
||||||
: "bg-custom-background-80"
|
: "bg-custom-background-80"
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -100,7 +100,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||||
<span>{renderEmoji(reaction)}</span>
|
<span>{renderEmoji(reaction)}</span>
|
||||||
<span
|
<span
|
||||||
className={
|
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"
|
? "text-custom-primary-100"
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
import { CalendarCheck2, Signal } from "lucide-react";
|
import { CalendarCheck2, Signal } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/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 { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||||
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
|
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
|
||||||
|
// hooks
|
||||||
|
import { usePublish, useStates } from "@/hooks/store";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IPeekMode } from "@/types/issue";
|
import { IIssue, IPeekMode } from "@/types/issue";
|
||||||
|
|
||||||
|
|
@ -20,8 +24,13 @@ type Props = {
|
||||||
mode?: IPeekMode;
|
mode?: IPeekMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mode }) => {
|
export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDetails, mode }) => {
|
||||||
const state = issueDetails.state_detail;
|
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;
|
const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null;
|
||||||
|
|
||||||
|
|
@ -42,7 +51,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
||||||
{mode === "full" && (
|
{mode === "full" && (
|
||||||
<div className="flex justify-between gap-2 pb-3">
|
<div className="flex justify-between gap-2 pb-3">
|
||||||
<h6 className="flex items-center gap-2 font-medium">
|
<h6 className="flex items-center gap-2 font-medium">
|
||||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
{project_details?.identifier}-{issueDetails.sequence_id}
|
||||||
</h6>
|
</h6>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
||||||
|
|
@ -58,7 +67,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
||||||
<span>State</span>
|
<span>State</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-3/4 flex items-center gap-1.5 py-0.5 text-sm">
|
<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 ?? "")}
|
{addSpaceIfCamelCase(state?.name ?? "")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -101,10 +110,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
||||||
{issueDetails.target_date ? (
|
{issueDetails.target_date ? (
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center gap-1.5 rounded py-0.5 text-xs text-custom-text-100", {
|
className={cn("flex items-center gap-1.5 rounded py-0.5 text-xs text-custom-text-100", {
|
||||||
"text-red-500": shouldHighlightIssueDueDate(
|
"text-red-500": shouldHighlightIssueDueDate(issueDetails.target_date, state?.group),
|
||||||
issueDetails.target_date,
|
|
||||||
issueDetails.state_detail.group
|
|
||||||
),
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<CalendarCheck2 className="size-3" />
|
<CalendarCheck2 className="size-3" />
|
||||||
|
|
@ -118,4 +124,4 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
|
||||||
|
|
@ -37,20 +37,20 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||||
|
|
||||||
const issueId = issueDetailsStore.peekId;
|
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 allUpVotes = votes.filter((vote) => vote.vote === 1);
|
||||||
const allDownVotes = votes?.filter((vote) => vote.vote === -1);
|
const allDownVotes = votes.filter((vote) => vote.vote === -1);
|
||||||
|
|
||||||
const isUpVotedByUser = allUpVotes?.some((vote) => vote.actor === user?.id);
|
const isUpVotedByUser = allUpVotes.some((vote) => vote.actor_details?.id === user?.id);
|
||||||
const isDownVotedByUser = allDownVotes?.some((vote) => vote.actor === user?.id);
|
const isDownVotedByUser = allDownVotes.some((vote) => vote.actor_details?.id === user?.id);
|
||||||
|
|
||||||
const handleVote = async (e: any, voteValue: 1 | -1) => {
|
const handleVote = async (e: any, voteValue: 1 | -1) => {
|
||||||
if (!issueId) return;
|
if (!issueId) return;
|
||||||
|
|
||||||
setIsSubmitting(true);
|
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);
|
if (actionPerformed) await issueDetailsStore.removeIssueVote(anchor, issueId);
|
||||||
else {
|
else {
|
||||||
|
|
@ -76,7 +76,7 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||||
{allUpVotes.length > 0 ? (
|
{allUpVotes.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{allUpVotes
|
{allUpVotes
|
||||||
.map((r) => r.actor_detail.display_name)
|
.map((r) => r.actor_details?.display_name)
|
||||||
.splice(0, VOTES_LIMIT)
|
.splice(0, VOTES_LIMIT)
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
{allUpVotes.length > VOTES_LIMIT && " and " + (allUpVotes.length - VOTES_LIMIT) + " more"}
|
{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.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{allDownVotes
|
{allDownVotes
|
||||||
.map((r) => r.actor_detail.display_name)
|
.map((r) => r.actor_details.display_name)
|
||||||
.splice(0, VOTES_LIMIT)
|
.splice(0, VOTES_LIMIT)
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
{allDownVotes.length > VOTES_LIMIT && " and " + (allDownVotes.length - VOTES_LIMIT) + " more"}
|
{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;
|
const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (anchor && peekId && issueStore.issues && issueStore.issues.length > 0) {
|
if (anchor && peekId && issueStore.groupedIssueIds) {
|
||||||
if (!issueDetails) {
|
if (!issueDetails) {
|
||||||
issueDetailStore.fetchIssueDetails(anchor, peekId.toString());
|
issueDetailStore.fetchIssueDetails(anchor, peekId.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [anchor, issueDetailStore, issueDetails, peekId, issueStore.issues]);
|
}, [anchor, issueDetailStore, issueDetails, peekId, issueStore.groupedIssueIds]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
issueDetailStore.setPeekId(null);
|
issueDetailStore.setPeekId(null);
|
||||||
|
|
|
||||||
|
|
@ -75,4 +75,4 @@ export const issuePriorityFilter = (priorityKey: TIssuePriorities): TIssueFilter
|
||||||
|
|
||||||
if (currentIssuePriority) return currentIssuePriority;
|
if (currentIssuePriority) return currentIssuePriority;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
@ -5,3 +5,5 @@ export * from "./use-user";
|
||||||
export * from "./use-user-profile";
|
export * from "./use-user-profile";
|
||||||
export * from "./use-issue-details";
|
export * from "./use-issue-details";
|
||||||
export * from "./use-issue-filter";
|
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 => {
|
export const useIssue = (): IIssueStore => {
|
||||||
const context = useContext(StoreContext);
|
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;
|
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(() => {
|
useEffect(() => {
|
||||||
const checkClipboardWriteAccess = () => {
|
const checkClipboardWriteAccess = () => {
|
||||||
navigator.permissions
|
navigator.permissions
|
||||||
|
//eslint-disable-next-line no-undef
|
||||||
.query({ name: "clipboard-write" as PermissionName })
|
.query({ name: "clipboard-write" as PermissionName })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.state === "granted") {
|
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
|
// services
|
||||||
import { APIService } from "@/services/api.service";
|
import { APIService } from "@/services/api.service";
|
||||||
// types
|
// types
|
||||||
import { TIssuesResponse } from "@/types/issue";
|
import { TIssuesResponse, IIssue } from "@/types/issue";
|
||||||
|
|
||||||
class IssueService extends APIService {
|
class IssueService extends APIService {
|
||||||
constructor() {
|
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}/`)
|
return this.get(`/api/public/anchor/${anchor}/issues/${issueID}/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.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 { makeObservable, observable, action, runInAction } from "mobx";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
// services
|
// services
|
||||||
|
|
@ -97,7 +98,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
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);
|
const commentsResponse = await this.issueService.getIssueComments(anchor, issueID);
|
||||||
|
|
||||||
if (issueDetails) {
|
if (issueDetails) {
|
||||||
|
|
@ -119,17 +120,11 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
|
|
||||||
addIssueComment = async (anchor: string, issueID: string, data: any) => {
|
addIssueComment = async (anchor: string, issueID: string, data: any) => {
|
||||||
try {
|
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);
|
const issueCommentResponse = await this.issueService.createIssueComment(anchor, issueID, data);
|
||||||
if (issueDetails) {
|
if (issueDetails) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.details = {
|
set(this.details, [issueID, "comments"], [...this.details[issueID].comments, issueCommentResponse]);
|
||||||
...this.details,
|
|
||||||
[issueID]: {
|
|
||||||
...issueDetails,
|
|
||||||
comments: [...this.details[issueID].comments, issueCommentResponse],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return issueCommentResponse;
|
return issueCommentResponse;
|
||||||
|
|
@ -267,21 +262,17 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
addIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => {
|
addIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.details = {
|
set(
|
||||||
...this.details,
|
this.details,
|
||||||
[issueID]: {
|
[issueID, "reaction_items"],
|
||||||
...this.details[issueID],
|
[
|
||||||
reactions: [
|
...this.details[issueID].reaction_items,
|
||||||
...this.details[issueID].reactions,
|
{
|
||||||
{
|
reaction: reactionHex,
|
||||||
id: uuidv4(),
|
actor_detail: this.rootStore.user.currentActor,
|
||||||
issue: issueID,
|
},
|
||||||
reaction: reactionHex,
|
]
|
||||||
actor_detail: this.rootStore.user.currentActor,
|
);
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.issueService.createIssueReaction(anchor, issueID, {
|
await this.issueService.createIssueReaction(anchor, issueID, {
|
||||||
|
|
@ -291,31 +282,19 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
console.log("Failed to add issue vote");
|
console.log("Failed to add issue vote");
|
||||||
const issueReactions = await this.issueService.getIssueReactions(anchor, issueID);
|
const issueReactions = await this.issueService.getIssueReactions(anchor, issueID);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.details = {
|
set(this.details, [issueID, "reaction_items"], issueReactions);
|
||||||
...this.details,
|
|
||||||
[issueID]: {
|
|
||||||
...this.details[issueID],
|
|
||||||
reactions: issueReactions,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
removeIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => {
|
removeIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => {
|
||||||
try {
|
try {
|
||||||
const newReactions = this.details[issueID].reactions.filter(
|
const newReactions = this.details[issueID].reaction_items.filter(
|
||||||
(_r) => !(_r.reaction === reactionHex && _r.actor_detail.id === this.rootStore.user.data?.id)
|
(_r) => !(_r.reaction === reactionHex && _r.actor_details.id === this.rootStore.user.data?.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.details = {
|
set(this.details, [issueID, "reaction_items"], newReactions);
|
||||||
...this.details,
|
|
||||||
[issueID]: {
|
|
||||||
...this.details[issueID],
|
|
||||||
reactions: newReactions,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.issueService.deleteIssueReaction(anchor, issueID, reactionHex);
|
await this.issueService.deleteIssueReaction(anchor, issueID, reactionHex);
|
||||||
|
|
@ -323,13 +302,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
console.log("Failed to remove issue reaction");
|
console.log("Failed to remove issue reaction");
|
||||||
const reactions = await this.issueService.getIssueReactions(anchor, issueID);
|
const reactions = await this.issueService.getIssueReactions(anchor, issueID);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.details = {
|
set(this.details, [issueID, "reaction_items"], reactions);
|
||||||
...this.details,
|
|
||||||
[issueID]: {
|
|
||||||
...this.details[issueID],
|
|
||||||
reactions: reactions,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -341,25 +314,19 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
if (!projectID || !workspaceSlug) throw new Error("Publish settings not found");
|
if (!projectID || !workspaceSlug) throw new Error("Publish settings not found");
|
||||||
|
|
||||||
const newVote: IVote = {
|
const newVote: IVote = {
|
||||||
actor: this.rootStore.user.data?.id ?? "",
|
actor_details: this.rootStore.user.currentActor,
|
||||||
actor_detail: this.rootStore.user.currentActor,
|
|
||||||
issue: issueID,
|
|
||||||
project: projectID,
|
|
||||||
workspace: workspaceSlug,
|
|
||||||
vote: data.vote,
|
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 {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.details = {
|
runInAction(() => {
|
||||||
...this.details,
|
set(this.details, [issueID, "vote_items"], [...filteredVotes, newVote]);
|
||||||
[issueID]: {
|
});
|
||||||
...this.details[issueID],
|
|
||||||
votes: [...filteredVotes, newVote],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.issueService.createIssueVote(anchor, issueID, data);
|
await this.issueService.createIssueVote(anchor, issueID, data);
|
||||||
|
|
@ -368,29 +335,19 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
const issueVotes = await this.issueService.getIssueVotes(anchor, issueID);
|
const issueVotes = await this.issueService.getIssueVotes(anchor, issueID);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.details = {
|
set(this.details, [issueID, "vote_items"], issueVotes);
|
||||||
...this.details,
|
|
||||||
[issueID]: {
|
|
||||||
...this.details[issueID],
|
|
||||||
votes: issueVotes,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
removeIssueVote = async (anchor: string, issueID: string) => {
|
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 {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.details = {
|
set(this.details, [issueID, "vote_items"], newVotes);
|
||||||
...this.details,
|
|
||||||
[issueID]: {
|
|
||||||
...this.details[issueID],
|
|
||||||
votes: newVotes,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.issueService.deleteIssueVote(anchor, issueID);
|
await this.issueService.deleteIssueVote(anchor, issueID);
|
||||||
|
|
@ -399,13 +356,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
const issueVotes = await this.issueService.getIssueVotes(anchor, issueID);
|
const issueVotes = await this.issueService.getIssueVotes(anchor, issueID);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.details = {
|
set(this.details, [issueID, "vote_items"], issueVotes);
|
||||||
...this.details,
|
|
||||||
[issueID]: {
|
|
||||||
...this.details[issueID],
|
|
||||||
votes: issueVotes,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import isEqual from "lodash/isEqual";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
|
// plane types
|
||||||
|
import { IssuePaginationOptions, TIssueParams } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
// store
|
// store
|
||||||
|
|
@ -15,6 +17,7 @@ import {
|
||||||
TIssueQueryFiltersParams,
|
TIssueQueryFiltersParams,
|
||||||
TIssueFilterKeys,
|
TIssueFilterKeys,
|
||||||
} from "@/types/issue";
|
} from "@/types/issue";
|
||||||
|
import { getPaginationParams } from "./helpers/filter.helpers";
|
||||||
|
|
||||||
export interface IIssueFilterStore {
|
export interface IIssueFilterStore {
|
||||||
// observables
|
// observables
|
||||||
|
|
@ -27,13 +30,20 @@ export interface IIssueFilterStore {
|
||||||
getAppliedFilters: (anchor: string) => TIssueQueryFiltersParams | undefined;
|
getAppliedFilters: (anchor: string) => TIssueQueryFiltersParams | undefined;
|
||||||
// actions
|
// actions
|
||||||
updateLayoutOptions: (layout: TIssueLayoutOptions) => void;
|
updateLayoutOptions: (layout: TIssueLayoutOptions) => void;
|
||||||
initIssueFilters: (anchor: string, filters: TIssueFilters) => void;
|
initIssueFilters: (anchor: string, filters: TIssueFilters, shouldFetchIssues?: boolean) => void;
|
||||||
updateIssueFilters: <K extends keyof TIssueFilters>(
|
updateIssueFilters: <K extends keyof TIssueFilters>(
|
||||||
anchor: string,
|
anchor: string,
|
||||||
filterKind: K,
|
filterKind: K,
|
||||||
filterKey: keyof TIssueFilters[K],
|
filterKey: keyof TIssueFilters[K],
|
||||||
filters: TIssueFilters[K][typeof filterKey]
|
filters: TIssueFilters[K][typeof filterKey]
|
||||||
) => Promise<void>;
|
) => 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 {
|
export class IssueFilterStore implements IIssueFilterStore {
|
||||||
|
|
@ -114,14 +124,27 @@ export class IssueFilterStore implements IIssueFilterStore {
|
||||||
// actions
|
// actions
|
||||||
updateLayoutOptions = (options: TIssueLayoutOptions) => set(this, ["layoutOptions"], options);
|
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 === undefined) runInAction(() => (this.filters = {}));
|
||||||
if (this.filters && initFilters) set(this.filters, [anchor], initFilters);
|
if (this.filters && initFilters) set(this.filters, [anchor], initFilters);
|
||||||
|
|
||||||
const appliedFilters = this.getAppliedFilters(anchor);
|
if (shouldFetchIssues) await this.store.issue.fetchPublicIssuesWithExistingPagination(anchor, "mutation");
|
||||||
await this.store.issue.fetchPublicIssues(anchor, appliedFilters);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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>(
|
updateIssueFilters = async <K extends keyof TIssueFilters>(
|
||||||
anchor: string,
|
anchor: string,
|
||||||
filterKind: K,
|
filterKind: K,
|
||||||
|
|
@ -135,7 +158,6 @@ export class IssueFilterStore implements IIssueFilterStore {
|
||||||
if (this.filters) set(this.filters, [anchor, filterKind, filterKey], filterValue);
|
if (this.filters) set(this.filters, [anchor, filterKind, filterKey], filterValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
const appliedFilters = this.getAppliedFilters(anchor);
|
if (filterKey !== "layout") await this.store.issue.fetchPublicIssuesWithExistingPagination(anchor, "mutation");
|
||||||
await this.store.issue.fetchPublicIssues(anchor, appliedFilters);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,38 @@
|
||||||
import { observable, action, makeObservable, runInAction } from "mobx";
|
import { action, makeObservable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
|
||||||
// types
|
// types
|
||||||
import { IStateLite } from "@plane/types";
|
import { IssuePaginationOptions, TLoader } from "@plane/types";
|
||||||
// services
|
// services
|
||||||
import IssueService from "@/services/issue.service";
|
import IssueService from "@/services/issue.service";
|
||||||
// store
|
// store
|
||||||
import { CoreRootStore } from "@/store/root.store";
|
import { CoreRootStore } from "@/store/root.store";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueLabel } from "@/types/issue";
|
import { BaseIssuesStore, IBaseIssuesStore } from "./helpers/base-issues.store";
|
||||||
|
|
||||||
export interface IIssueStore {
|
export interface IIssueStore extends IBaseIssuesStore {
|
||||||
loader: boolean;
|
|
||||||
error: any;
|
|
||||||
// observables
|
|
||||||
issues: IIssue[];
|
|
||||||
states: IStateLite[];
|
|
||||||
labels: IIssueLabel[];
|
|
||||||
// filter observables
|
|
||||||
filteredStates: string[];
|
|
||||||
filteredLabels: string[];
|
|
||||||
filteredPriorities: string[];
|
|
||||||
// actions
|
// actions
|
||||||
fetchPublicIssues: (anchor: string, params: any) => Promise<void>;
|
fetchPublicIssues: (
|
||||||
// helpers
|
anchor: string,
|
||||||
getCountOfIssuesByState: (stateID: string) => number;
|
loadType: TLoader,
|
||||||
getFilteredIssuesByState: (stateID: string) => IIssue[];
|
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 {
|
export class IssueStore extends BaseIssuesStore implements IIssueStore {
|
||||||
loader: boolean = false;
|
|
||||||
error: any | null = null;
|
|
||||||
// observables
|
|
||||||
states: IStateLite[] = [];
|
|
||||||
labels: IIssueLabel[] = [];
|
|
||||||
issues: IIssue[] = [];
|
|
||||||
// filter observables
|
|
||||||
filteredStates: string[] = [];
|
|
||||||
filteredLabels: string[] = [];
|
|
||||||
filteredPriorities: string[] = [];
|
|
||||||
// root store
|
// root store
|
||||||
rootStore: CoreRootStore;
|
rootStore: CoreRootStore;
|
||||||
// services
|
// services
|
||||||
issueService: IssueService;
|
issueService: IssueService;
|
||||||
|
|
||||||
constructor(_rootStore: CoreRootStore) {
|
constructor(_rootStore: CoreRootStore) {
|
||||||
|
super(_rootStore, _rootStore.issueFilter);
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
loader: observable.ref,
|
|
||||||
error: observable,
|
|
||||||
// observables
|
|
||||||
states: observable,
|
|
||||||
labels: observable,
|
|
||||||
issues: observable,
|
|
||||||
// filter observables
|
|
||||||
filteredStates: observable,
|
|
||||||
filteredLabels: observable,
|
|
||||||
filteredPriorities: observable,
|
|
||||||
// actions
|
// actions
|
||||||
fetchPublicIssues: action,
|
fetchPublicIssues: action,
|
||||||
|
fetchNextPublicIssues: action,
|
||||||
|
fetchPublicIssuesWithExistingPagination: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
|
|
@ -68,45 +44,69 @@ export class IssueStore implements IIssueStore {
|
||||||
* @param {string} anchor
|
* @param {string} anchor
|
||||||
* @param params
|
* @param params
|
||||||
*/
|
*/
|
||||||
fetchPublicIssues = async (anchor: string, params: any) => {
|
fetchPublicIssues = async (
|
||||||
|
anchor: string,
|
||||||
|
loadType: TLoader = "init-loader",
|
||||||
|
options: IssuePaginationOptions,
|
||||||
|
isExistingPaginationOptions: boolean = false
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
|
// set loader and clear store
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = true;
|
this.setLoader(loadType);
|
||||||
this.error = null;
|
|
||||||
});
|
});
|
||||||
|
this.clear(!isExistingPaginationOptions);
|
||||||
|
|
||||||
|
const params = this.rootStore.issueFilter.getFilterParams(options, anchor, undefined, undefined, undefined);
|
||||||
|
|
||||||
const response = await this.issueService.fetchPublicIssues(anchor, params);
|
const response = await this.issueService.fetchPublicIssues(anchor, params);
|
||||||
|
|
||||||
if (response) {
|
// after fetching issues, call the base method to process the response further
|
||||||
runInAction(() => {
|
this.onfetchIssues(response, options);
|
||||||
this.states = response.states;
|
|
||||||
this.labels = response.labels;
|
|
||||||
this.issues = response.issues;
|
|
||||||
this.loader = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.loader = false;
|
this.setLoader(undefined);
|
||||||
this.error = error;
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get total count of issues under a particular state
|
* This Method exists to fetch the first page of the issues with the existing stored pagination
|
||||||
* @param {string} stateID
|
* This is useful for refetching when filters, groupBy, orderBy etc changes
|
||||||
* @returns {number}
|
* @param workspaceSlug
|
||||||
|
* @param projectId
|
||||||
|
* @param loadType
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
getCountOfIssuesByState = computedFn(
|
fetchPublicIssuesWithExistingPagination = async (anchor: string, loadType: TLoader = "mutation") => {
|
||||||
(stateID: string) => this.issues?.filter((issue) => issue.state == stateID).length || 0
|
if (!this.paginationOptions) return;
|
||||||
);
|
return await this.fetchPublicIssues(anchor, loadType, this.paginationOptions, true);
|
||||||
|
};
|
||||||
/**
|
|
||||||
* @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) || []
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 { IssueStore, IIssueStore } from "@/store/issue.store";
|
||||||
import { IUserStore, UserStore } from "@/store/user.store";
|
import { IUserStore, UserStore } from "@/store/user.store";
|
||||||
import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store";
|
import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store";
|
||||||
|
import { IIssueLabelStore, LabelStore } from "./label.store";
|
||||||
import { IMentionsStore, MentionsStore } from "./mentions.store";
|
import { IMentionsStore, MentionsStore } from "./mentions.store";
|
||||||
import { IPublishListStore, PublishListStore } from "./publish/publish_list.store";
|
import { IPublishListStore, PublishListStore } from "./publish/publish_list.store";
|
||||||
|
import { IStateStore, StateStore } from "./state.store";
|
||||||
|
|
||||||
enableStaticRendering(typeof window === "undefined");
|
enableStaticRendering(typeof window === "undefined");
|
||||||
|
|
||||||
|
|
@ -16,6 +18,8 @@ export class CoreRootStore {
|
||||||
issue: IIssueStore;
|
issue: IIssueStore;
|
||||||
issueDetail: IIssueDetailStore;
|
issueDetail: IIssueDetailStore;
|
||||||
mentionStore: IMentionsStore;
|
mentionStore: IMentionsStore;
|
||||||
|
state: IStateStore;
|
||||||
|
label: IIssueLabelStore;
|
||||||
issueFilter: IIssueFilterStore;
|
issueFilter: IIssueFilterStore;
|
||||||
publishList: IPublishListStore;
|
publishList: IPublishListStore;
|
||||||
|
|
||||||
|
|
@ -25,6 +29,8 @@ export class CoreRootStore {
|
||||||
this.issue = new IssueStore(this);
|
this.issue = new IssueStore(this);
|
||||||
this.issueDetail = new IssueDetailStore(this);
|
this.issueDetail = new IssueDetailStore(this);
|
||||||
this.mentionStore = new MentionsStore(this);
|
this.mentionStore = new MentionsStore(this);
|
||||||
|
this.state = new StateStore(this);
|
||||||
|
this.label = new LabelStore(this);
|
||||||
this.issueFilter = new IssueFilterStore(this);
|
this.issueFilter = new IssueFilterStore(this);
|
||||||
this.publishList = new PublishListStore(this);
|
this.publishList = new PublishListStore(this);
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +49,8 @@ export class CoreRootStore {
|
||||||
this.issue = new IssueStore(this);
|
this.issue = new IssueStore(this);
|
||||||
this.issueDetail = new IssueDetailStore(this);
|
this.issueDetail = new IssueDetailStore(this);
|
||||||
this.mentionStore = new MentionsStore(this);
|
this.mentionStore = new MentionsStore(this);
|
||||||
|
this.state = new StateStore(this);
|
||||||
|
this.label = new LabelStore(this);
|
||||||
this.issueFilter = new IssueFilterStore(this);
|
this.issueFilter = new IssueFilterStore(this);
|
||||||
this.publishList = new PublishListStore(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 TIssueLayout = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt";
|
||||||
export type TIssueLayoutOptions = {
|
export type TIssueLayoutOptions = {
|
||||||
|
|
@ -33,31 +33,61 @@ export type TIssueQueryFilters = Partial<TFilters>;
|
||||||
|
|
||||||
export type TIssueQueryFiltersParams = Partial<Record<keyof TFilters, string>>;
|
export type TIssueQueryFiltersParams = Partial<Record<keyof TFilters, string>>;
|
||||||
|
|
||||||
export type TIssuesResponse = {
|
|
||||||
states: IStateLite[];
|
|
||||||
labels: IIssueLabel[];
|
|
||||||
issues: IIssue[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface 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[];
|
comments: Comment[];
|
||||||
label_details: any;
|
reaction_items: IIssueReaction[];
|
||||||
project: string;
|
vote_items: IVote[];
|
||||||
project_detail: any;
|
|
||||||
reactions: IIssueReaction[];
|
|
||||||
state: string;
|
|
||||||
state_detail: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
group: TIssueGroupKey;
|
|
||||||
color: string;
|
|
||||||
};
|
|
||||||
votes: IVote[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IPeekMode = "side" | "modal" | "full";
|
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 {
|
export interface IIssueLabel {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -66,12 +96,8 @@ export interface IIssueLabel {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVote {
|
export interface IVote {
|
||||||
issue: string;
|
|
||||||
vote: -1 | 1;
|
vote: -1 | 1;
|
||||||
workspace: string;
|
actor_details: ActorDetail;
|
||||||
project: string;
|
|
||||||
actor: string;
|
|
||||||
actor_detail: ActorDetail;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Comment {
|
export interface Comment {
|
||||||
|
|
@ -102,9 +128,7 @@ export interface Comment {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueReaction {
|
export interface IIssueReaction {
|
||||||
actor_detail: ActorDetail;
|
actor_details: ActorDetail;
|
||||||
id: string;
|
|
||||||
issue: string;
|
|
||||||
reaction: string;
|
reaction: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,8 +136,8 @@ export interface ActorDetail {
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
first_name?: string;
|
first_name?: string;
|
||||||
id?: string;
|
|
||||||
is_bot?: boolean;
|
is_bot?: boolean;
|
||||||
|
id?: string;
|
||||||
last_name?: string;
|
last_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,19 +17,16 @@ export const renderEmoji = (
|
||||||
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
|
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const groupReactions: (reactions: any[], key: string) => { [key: string]: any[] } = (
|
export const groupReactions = <T extends { reaction: string }>(reactions: T[], key: string) => {
|
||||||
reactions: any,
|
|
||||||
key: string
|
|
||||||
) => {
|
|
||||||
const groupedReactions = reactions.reduce(
|
const groupedReactions = reactions.reduce(
|
||||||
(acc: any, reaction: any) => {
|
(acc: { [key: string]: T[] }, reaction: any) => {
|
||||||
if (!acc[reaction[key]]) {
|
if (!acc[reaction[key]]) {
|
||||||
acc[reaction[key]] = [];
|
acc[reaction[key]] = [];
|
||||||
}
|
}
|
||||||
acc[reaction[key]].push(reaction);
|
acc[reaction[key]].push(reaction);
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as { [key: string]: any[] }
|
{} as { [key: string]: T[] }
|
||||||
);
|
);
|
||||||
|
|
||||||
return groupedReactions;
|
return groupedReactions;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"@plane/editor": "*",
|
"@plane/editor": "*",
|
||||||
"@plane/types": "*",
|
"@plane/types": "*",
|
||||||
"@plane/ui": "*",
|
"@plane/ui": "*",
|
||||||
|
"@plane/constants": "*",
|
||||||
"@sentry/nextjs": "^8",
|
"@sentry/nextjs": "^8",
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.3.4",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
|
|
@ -62,4 +63,4 @@
|
||||||
"tailwind-config-custom": "*",
|
"tailwind-config-custom": "*",
|
||||||
"tsconfig": "*"
|
"tsconfig": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
import { FC, useCallback, useEffect } from "react";
|
import { FC, useCallback, useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
import { EIssueGroupByToServerOptions } from "@plane/constants";
|
||||||
import { TGroupedIssues } from "@plane/types";
|
import { TGroupedIssues } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
import { CalendarChart } from "@/components/issues";
|
import { CalendarChart } from "@/components/issues";
|
||||||
//constants
|
//constants
|
||||||
import { EIssuesStoreType, EIssueGroupByToServerOptions } from "@/constants/issue";
|
import { EIssuesStoreType } from "@/constants/issue";
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssues, useUser, useCalendarView } from "@/hooks/store";
|
import { useIssues, useUser, useCalendarView } from "@/hooks/store";
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import React, { useCallback, useEffect } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
// plane constants
|
||||||
|
import { ALL_ISSUES } from "@plane/constants";
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
import { ChartDataType, GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "@/components/gantt-chart";
|
import { ChartDataType, GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "@/components/gantt-chart";
|
||||||
import { getMonthChartItemPositionWidthInMonth } from "@/components/gantt-chart/views";
|
import { getMonthChartItemPositionWidthInMonth } from "@/components/gantt-chart/views";
|
||||||
import { GanttQuickAddIssueForm, IssueGanttBlock } from "@/components/issues";
|
import { GanttQuickAddIssueForm, IssueGanttBlock } from "@/components/issues";
|
||||||
//constants
|
//constants
|
||||||
import { ALL_ISSUES, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
import { EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
import { getIssueBlocksStructure } from "@/helpers/issue.helper";
|
import { getIssueBlocksStructure } from "@/helpers/issue.helper";
|
||||||
//hooks
|
//hooks
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { useEffect, useRef } from "react";
|
||||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
// plane constants
|
||||||
|
import { ALL_ISSUES } from "@plane/constants";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
GroupByColumnTypes,
|
GroupByColumnTypes,
|
||||||
|
|
@ -15,8 +17,7 @@ import {
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { MultipleSelectGroup } from "@/components/core";
|
import { MultipleSelectGroup } from "@/components/core";
|
||||||
// constants
|
|
||||||
import { ALL_ISSUES } from "@/constants/issue";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import isEmpty from "lodash/isEmpty";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams, useSearchParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
// plane constants
|
||||||
|
import { ALL_ISSUES } from "@plane/constants";
|
||||||
import { IIssueDisplayFilterOptions } from "@plane/types";
|
import { IIssueDisplayFilterOptions } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
// components
|
// components
|
||||||
|
|
@ -12,7 +14,6 @@ import { AllIssueQuickActions } from "@/components/issues/issue-layouts/quick-ac
|
||||||
import { SpreadsheetLayoutLoader } from "@/components/ui";
|
import { SpreadsheetLayoutLoader } from "@/components/ui";
|
||||||
// constants
|
// constants
|
||||||
import {
|
import {
|
||||||
ALL_ISSUES,
|
|
||||||
EIssueFilterType,
|
EIssueFilterType,
|
||||||
EIssueLayoutTypes,
|
EIssueLayoutTypes,
|
||||||
EIssuesStoreType,
|
EIssuesStoreType,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { FC, useCallback, useEffect } from "react";
|
import { FC, useCallback, useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
// plane constants
|
||||||
|
import { ALL_ISSUES } from "@plane/constants";
|
||||||
import { IIssueDisplayFilterOptions } from "@plane/types";
|
import { IIssueDisplayFilterOptions } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
import { ALL_ISSUES, EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssues, useUser } from "@/hooks/store";
|
import { useIssues, useUser } from "@/hooks/store";
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,6 @@ import {
|
||||||
TIssueTypeFilters,
|
TIssueTypeFilters,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
|
|
||||||
|
|
||||||
export const ALL_ISSUES = "All Issues";
|
|
||||||
|
|
||||||
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
|
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
|
||||||
"state",
|
"state",
|
||||||
"priority",
|
"priority",
|
||||||
|
|
@ -460,32 +457,6 @@ export const groupReactionEmojis = (reactions: any) => {
|
||||||
return groupedEmojis;
|
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 {
|
export enum EActivityFilterType {
|
||||||
COMMENT = "COMMENT",
|
COMMENT = "COMMENT",
|
||||||
ACTIVITY = "ACTIVITY",
|
ACTIVITY = "ACTIVITY",
|
||||||
|
|
@ -500,4 +471,4 @@ export const ACTIVITY_FILTER_TYPE_OPTIONS = [
|
||||||
value: EActivityFilterType.ACTIVITY,
|
value: EActivityFilterType.ACTIVITY,
|
||||||
label: "Updates",
|
label: "Updates",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -6,8 +6,10 @@ import set from "lodash/set";
|
||||||
import uniq from "lodash/uniq";
|
import uniq from "lodash/uniq";
|
||||||
import update from "lodash/update";
|
import update from "lodash/update";
|
||||||
import { action, observable, makeObservable, runInAction } from "mobx";
|
import { action, observable, makeObservable, runInAction } from "mobx";
|
||||||
// types
|
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
|
// types
|
||||||
|
// plane constants
|
||||||
|
import { ALL_ISSUES } from "@plane/constants";
|
||||||
import {
|
import {
|
||||||
TIssue,
|
TIssue,
|
||||||
TLoader,
|
TLoader,
|
||||||
|
|
@ -16,7 +18,6 @@ import {
|
||||||
ViewFlags,
|
ViewFlags,
|
||||||
TBulkOperationsPayload,
|
TBulkOperationsPayload,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
import { ALL_ISSUES } from "@/constants/issue";
|
|
||||||
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
|
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||||
import { IIssueRootStore } from "../root.store";
|
import { IIssueRootStore } from "../root.store";
|
||||||
import { ICycleIssuesFilter } from "./filter.store";
|
import { ICycleIssuesFilter } from "./filter.store";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import uniq from "lodash/uniq";
|
import uniq from "lodash/uniq";
|
||||||
|
import { ALL_ISSUES } from "@plane/constants";
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
import { ALL_ISSUES } from "@/constants/issue";
|
|
||||||
import { EIssueGroupedAction } from "./base-issues.store";
|
import { EIssueGroupedAction } from "./base-issues.store";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import update from "lodash/update";
|
||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// types
|
// types
|
||||||
|
import { ALL_ISSUES } from "@plane/constants";
|
||||||
import {
|
import {
|
||||||
TIssue,
|
TIssue,
|
||||||
TIssueGroupByOptions,
|
TIssueGroupByOptions,
|
||||||
|
|
@ -28,7 +29,7 @@ import {
|
||||||
TPaginationData,
|
TPaginationData,
|
||||||
TBulkOperationsPayload,
|
TBulkOperationsPayload,
|
||||||
} from "@plane/types";
|
} 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 { convertToISODateString } from "@/helpers/date-time.helper";
|
||||||
import { CycleService } from "@/services/cycle.service";
|
import { CycleService } from "@/services/cycle.service";
|
||||||
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
|
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
|
import { EIssueGroupByToServerOptions, EServerGroupByToFilterOptions } from "@plane/constants";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
IIssueDisplayFilterOptions,
|
IIssueDisplayFilterOptions,
|
||||||
|
|
@ -12,12 +13,7 @@ import {
|
||||||
TStaticViewTypes,
|
TStaticViewTypes,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import {
|
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||||
EIssueFilterType,
|
|
||||||
EIssuesStoreType,
|
|
||||||
EIssueGroupByToServerOptions,
|
|
||||||
EServerGroupByToFilterOptions,
|
|
||||||
} from "@/constants/issue";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
|
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
|
||||||
// lib
|
// lib
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue