mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
58 Commits
v0.12-dev
...
fix/issue_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f25bebd92 | ||
|
|
85b838e0df | ||
|
|
42d38f7531 | ||
|
|
61672f47ac | ||
|
|
23e62c83eb | ||
|
|
e58b76c8a6 | ||
|
|
4ce01ca4f8 | ||
|
|
a34b0b059d | ||
|
|
164e0b9301 | ||
|
|
5a91031243 | ||
|
|
47bec7704b | ||
|
|
b9c935092a | ||
|
|
3a2a329000 | ||
|
|
8e9a4dca78 | ||
|
|
cdb888c23e | ||
|
|
2186db8bba | ||
|
|
9bff10de6d | ||
|
|
6867154963 | ||
|
|
7bb73b74ba | ||
|
|
991258084e | ||
|
|
1a37668f0b | ||
|
|
4447a4b519 | ||
|
|
7842c4b2ea | ||
|
|
8de93d0081 | ||
|
|
5b228bd1eb | ||
|
|
ad8a011bb9 | ||
|
|
49d0b3f4a1 | ||
|
|
1872dff00d | ||
|
|
faa6a2bcbc | ||
|
|
6d52707ff5 | ||
|
|
8ba482bc9c | ||
|
|
5989f2476a | ||
|
|
8ea6dd4e84 | ||
|
|
39bc975994 | ||
|
|
866eead35f | ||
|
|
9c3510851d | ||
|
|
81436902a3 | ||
|
|
d26aa1b2da | ||
|
|
b47c7d363f | ||
|
|
85f797058d | ||
|
|
1655d0cb1c | ||
|
|
58562dc4b7 | ||
|
|
2ad46d7bfa | ||
|
|
4f0cac37db | ||
|
|
b46a7481ae | ||
|
|
f11ae00201 | ||
|
|
c5612ee7a3 | ||
|
|
0dd336aec8 | ||
|
|
4b364f72b5 | ||
|
|
6d13332818 | ||
|
|
ac4127c93d | ||
|
|
60c3d1a6e9 | ||
|
|
70ed3c1fdf | ||
|
|
b40059ea21 | ||
|
|
90276073cd | ||
|
|
8d5ff1a628 | ||
|
|
065a4a3cf7 | ||
|
|
928ae775f4 |
@@ -23,6 +23,8 @@ NEXT_PUBLIC_SLACK_CLIENT_ID=""
|
||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
|
||||
# public boards deploy url
|
||||
NEXT_PUBLIC_DEPLOY_URL=""
|
||||
# plane deploy using nginx
|
||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
# Backend
|
||||
# Debug value for api server use it as 0 for production use
|
||||
|
||||
2
.github/workflows/Update_Docker_Images.yml
vendored
2
.github/workflows/Update_Docker_Images.yml
vendored
@@ -2,7 +2,7 @@ name: Update Docker Images for Plane on Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
types: [released, prereleased]
|
||||
|
||||
jobs:
|
||||
build_push_backend:
|
||||
|
||||
@@ -31,8 +31,6 @@ from .issue import (
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssuePropertySerializer,
|
||||
BlockerIssueSerializer,
|
||||
BlockedIssueSerializer,
|
||||
IssueAssigneeSerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
@@ -45,6 +43,8 @@ from .issue import (
|
||||
IssueReactionSerializer,
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssueRelationSerializer,
|
||||
RelatedIssueSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
|
||||
|
||||
@@ -17,12 +17,10 @@ from plane.db.models import (
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
IssueProperty,
|
||||
IssueBlocker,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
IssueLabel,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
CycleIssue,
|
||||
Cycle,
|
||||
Module,
|
||||
@@ -32,6 +30,7 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
IssueVote,
|
||||
IssueRelation,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,25 +80,12 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
required=False,
|
||||
)
|
||||
|
||||
# List of issues that are blocking this issue
|
||||
blockers_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
labels_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
# List of issues that are blocked by this issue
|
||||
blocks_list = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = "__all__"
|
||||
@@ -122,10 +108,8 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
blockers = validated_data.pop("blockers_list", None)
|
||||
assignees = validated_data.pop("assignees_list", None)
|
||||
labels = validated_data.pop("labels_list", None)
|
||||
blocks = validated_data.pop("blocks_list", None)
|
||||
|
||||
project_id = self.context["project_id"]
|
||||
workspace_id = self.context["workspace_id"]
|
||||
@@ -137,22 +121,6 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
created_by_id = issue.created_by_id
|
||||
updated_by_id = issue.updated_by_id
|
||||
|
||||
if blockers is not None and len(blockers):
|
||||
IssueBlocker.objects.bulk_create(
|
||||
[
|
||||
IssueBlocker(
|
||||
block=issue,
|
||||
blocked_by=blocker,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for blocker in blockers
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if assignees is not None and len(assignees):
|
||||
IssueAssignee.objects.bulk_create(
|
||||
[
|
||||
@@ -196,29 +164,11 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if blocks is not None and len(blocks):
|
||||
IssueBlocker.objects.bulk_create(
|
||||
[
|
||||
IssueBlocker(
|
||||
block=block,
|
||||
blocked_by=issue,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for block in blocks
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
return issue
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
blockers = validated_data.pop("blockers_list", None)
|
||||
assignees = validated_data.pop("assignees_list", None)
|
||||
labels = validated_data.pop("labels_list", None)
|
||||
blocks = validated_data.pop("blocks_list", None)
|
||||
|
||||
# Related models
|
||||
project_id = instance.project_id
|
||||
@@ -226,23 +176,6 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
created_by_id = instance.created_by_id
|
||||
updated_by_id = instance.updated_by_id
|
||||
|
||||
if blockers is not None:
|
||||
IssueBlocker.objects.filter(block=instance).delete()
|
||||
IssueBlocker.objects.bulk_create(
|
||||
[
|
||||
IssueBlocker(
|
||||
block=instance,
|
||||
blocked_by=blocker,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for blocker in blockers
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if assignees is not None:
|
||||
IssueAssignee.objects.filter(issue=instance).delete()
|
||||
IssueAssignee.objects.bulk_create(
|
||||
@@ -277,23 +210,6 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if blocks is not None:
|
||||
IssueBlocker.objects.filter(blocked_by=instance).delete()
|
||||
IssueBlocker.objects.bulk_create(
|
||||
[
|
||||
IssueBlocker(
|
||||
block=block,
|
||||
blocked_by=instance,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for block in blocks
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
# Time updation occues even when other related models are updated
|
||||
instance.updated_at = timezone.now()
|
||||
return super().update(instance, validated_data)
|
||||
@@ -375,32 +291,39 @@ class IssueLabelSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class BlockedIssueSerializer(BaseSerializer):
|
||||
blocked_issue_detail = IssueProjectLiteSerializer(source="block", read_only=True)
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
related_issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||
|
||||
class Meta:
|
||||
model = IssueBlocker
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"blocked_issue_detail",
|
||||
"blocked_by",
|
||||
"block",
|
||||
"related_issue_detail",
|
||||
"relation_type",
|
||||
"related_issue",
|
||||
"issue",
|
||||
"id"
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class BlockerIssueSerializer(BaseSerializer):
|
||||
blocker_issue_detail = IssueProjectLiteSerializer(
|
||||
source="blocked_by", read_only=True
|
||||
)
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
||||
|
||||
class Meta:
|
||||
model = IssueBlocker
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"blocker_issue_detail",
|
||||
"blocked_by",
|
||||
"block",
|
||||
"issue_detail",
|
||||
"relation_type",
|
||||
"related_issue",
|
||||
"issue",
|
||||
"id"
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class IssueAssigneeSerializer(BaseSerializer):
|
||||
@@ -617,10 +540,8 @@ class IssueSerializer(BaseSerializer):
|
||||
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
# List of issues blocked by this issue
|
||||
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
|
||||
# List of issues that block this issue
|
||||
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
|
||||
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
||||
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||
|
||||
@@ -90,7 +90,9 @@ from plane.api.views import (
|
||||
IssueSubscriberViewSet,
|
||||
IssueCommentPublicViewSet,
|
||||
IssueReactionViewSet,
|
||||
IssueRelationViewSet,
|
||||
CommentReactionViewSet,
|
||||
IssueDraftViewSet,
|
||||
## End Issues
|
||||
# States
|
||||
StateViewSet,
|
||||
@@ -1010,6 +1012,47 @@ urlpatterns = [
|
||||
name="project-issue-archive",
|
||||
),
|
||||
## End Issue Archives
|
||||
## Issue Relation
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||
IssueRelationViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="issue-relation",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
|
||||
IssueRelationViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-relation",
|
||||
),
|
||||
## End Issue Relation
|
||||
## Issue Drafts
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
|
||||
IssueDraftViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
name="project-issue-draft",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
|
||||
IssueDraftViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-draft",
|
||||
),
|
||||
## End Issue Drafts
|
||||
## File Assets
|
||||
path(
|
||||
"workspaces/<str:slug>/file-assets/",
|
||||
|
||||
@@ -86,8 +86,10 @@ from .issue import (
|
||||
IssueReactionPublicViewSet,
|
||||
CommentReactionPublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
IssueRelationViewSet,
|
||||
IssueRetrievePublicEndpoint,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
IssueDraftViewSet,
|
||||
)
|
||||
|
||||
from .auth_extended import (
|
||||
@@ -167,6 +169,4 @@ from .analytic import (
|
||||
|
||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
||||
|
||||
from .exporter import (
|
||||
ExportIssuesEndpoint,
|
||||
)
|
||||
from .exporter import ExportIssuesEndpoint
|
||||
@@ -517,6 +517,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
try:
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||
@@ -555,9 +556,15 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
|
||||
issues_data = IssueStateSerializer(issues, many=True).data
|
||||
|
||||
if sub_group_by and sub_group_by == group_by:
|
||||
return Response(
|
||||
{"error": "Group by and sub group by cannot be same"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
return Response(
|
||||
group_results(issues_data, group_by),
|
||||
group_results(issues_data, group_by, sub_group_by),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
final_text = task + "\n" + prompt
|
||||
|
||||
openai.api_key = settings.OPENAI_API_KEY
|
||||
response = openai.Completion.create(
|
||||
response = openai.ChatCompletion.create(
|
||||
model=settings.GPT_ENGINE,
|
||||
prompt=final_text,
|
||||
messages=[{"role": "user", "content": final_text}],
|
||||
temperature=0.7,
|
||||
max_tokens=1024,
|
||||
)
|
||||
@@ -51,7 +51,7 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
text = response.choices[0].text.strip()
|
||||
text = response.choices[0].message.content.strip()
|
||||
text_html = text.replace("\n", "<br/>")
|
||||
return Response(
|
||||
{
|
||||
|
||||
@@ -24,6 +24,7 @@ from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.db import IntegrityError
|
||||
from django.conf import settings
|
||||
from django.db import IntegrityError
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -51,6 +52,7 @@ from plane.api.serializers import (
|
||||
IssueReactionSerializer,
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssueRelationSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
from plane.api.permissions import (
|
||||
@@ -76,6 +78,7 @@ from plane.db.models import (
|
||||
CommentReaction,
|
||||
ProjectDeployBoard,
|
||||
IssueVote,
|
||||
IssueRelation,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
@@ -178,7 +181,7 @@ class IssueViewSet(BaseViewSet):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
@@ -266,9 +269,16 @@ class IssueViewSet(BaseViewSet):
|
||||
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
if sub_group_by and sub_group_by == group_by:
|
||||
return Response(
|
||||
{"error": "Group by and sub group by cannot be same"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
return Response(
|
||||
group_results(issues, group_by), status=status.HTTP_200_OK
|
||||
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
@@ -331,7 +341,7 @@ class UserWorkSpaceIssues(BaseAPIView):
|
||||
try:
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
@@ -443,9 +453,16 @@ class UserWorkSpaceIssues(BaseAPIView):
|
||||
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
if sub_group_by and sub_group_by == group_by:
|
||||
return Response(
|
||||
{"error": "Group by and sub group by cannot be same"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
return Response(
|
||||
group_results(issues, group_by), status=status.HTTP_200_OK
|
||||
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
@@ -1068,7 +1085,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
@@ -1575,7 +1592,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
).order_by("created_at")
|
||||
else:
|
||||
return IssueComment.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
@@ -2040,6 +2057,98 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
|
||||
class IssueRelationViewSet(BaseViewSet):
|
||||
serializer_class = IssueRelationSerializer
|
||||
model = IssueRelation
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
current_instance = (
|
||||
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||
)
|
||||
if current_instance is not None:
|
||||
issue_activity.delay(
|
||||
type="issue_relation.activity.deleted",
|
||||
requested_data=json.dumps({"related_list": None}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
IssueRelationSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
try:
|
||||
related_list = request.data.get("related_list", [])
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
issueRelation = IssueRelation.objects.bulk_create(
|
||||
[
|
||||
IssueRelation(
|
||||
issue_id=related_issue["issue"],
|
||||
related_issue_id=related_issue["related_issue"],
|
||||
relation_type=related_issue["relation_type"],
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
for related_issue in related_list
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
issue_activity.delay(
|
||||
type="issue_relation.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
)
|
||||
|
||||
return Response(
|
||||
IssueRelationSerializer(issueRelation, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The issue is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
else:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("issue")
|
||||
.distinct()
|
||||
)
|
||||
class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
@@ -2078,7 +2187,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
@@ -2100,6 +2209,12 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"votes",
|
||||
queryset=IssueVote.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
@@ -2189,6 +2304,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
|
||||
states = (
|
||||
State.objects.filter(
|
||||
~Q(name="Triage"),
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -2233,3 +2349,157 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class IssueDraftViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
serializer_class = IssueFlatSerializer
|
||||
model = Issue
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Issue.objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(is_draft=True)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, project_id):
|
||||
try:
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order
|
||||
if order_by_param == "priority"
|
||||
else priority_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
*[
|
||||
When(priority=p, then=Value(i))
|
||||
for i, p in enumerate(priority_order)
|
||||
],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
||||
# State Ordering
|
||||
elif order_by_param in [
|
||||
"state__name",
|
||||
"state__group",
|
||||
"-state__name",
|
||||
"-state__group",
|
||||
]:
|
||||
state_order = (
|
||||
state_order
|
||||
if order_by_param in ["state__name", "state__group"]
|
||||
else state_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[
|
||||
When(state__group=state_group, then=Value(i))
|
||||
for i, state_group in enumerate(state_order)
|
||||
],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("state_order")
|
||||
# assignee and label ordering
|
||||
elif order_by_param in [
|
||||
"labels__name",
|
||||
"-labels__name",
|
||||
"assignees__first_name",
|
||||
"-assignees__first_name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
max_values=Max(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
||||
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
if group_by:
|
||||
return Response(
|
||||
group_results(issues, group_by), status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
try:
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True
|
||||
)
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||
except Issue.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
@@ -308,6 +308,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
try:
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||
@@ -346,9 +347,15 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
|
||||
issues_data = IssueStateSerializer(issues, many=True).data
|
||||
|
||||
if sub_group_by and sub_group_by == group_by:
|
||||
return Response(
|
||||
{"error": "Group by and sub group by cannot be same"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
return Response(
|
||||
group_results(issues_data, group_by),
|
||||
group_results(issues_data, group_by, sub_group_by),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
# Delete joined project invites
|
||||
project_invitations.delete()
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
@@ -924,8 +924,7 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
||||
|
||||
project_member.save()
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except Project.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "The requested resource does not exists"},
|
||||
|
||||
@@ -220,7 +220,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
query = request.query_params.get("search", False)
|
||||
workspace_search = request.query_params.get("workspace_search", "false")
|
||||
parent = request.query_params.get("parent", "false")
|
||||
blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false")
|
||||
issue_relation = request.query_params.get("issue_relation", "false")
|
||||
cycle = request.query_params.get("cycle", "false")
|
||||
module = request.query_params.get("module", "false")
|
||||
sub_issue = request.query_params.get("sub_issue", "false")
|
||||
@@ -247,12 +247,12 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
"parent_id", flat=True
|
||||
)
|
||||
)
|
||||
if blocker_blocked_by == "true" and issue_id:
|
||||
if issue_relation == "true" and issue_id:
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id),
|
||||
~Q(blocked_issues__block=issue),
|
||||
~Q(blocker_issues__blocked_by=issue),
|
||||
~Q(issue_related__issue=issue),
|
||||
~Q(issue_relation__related_issue=issue),
|
||||
)
|
||||
if sub_issue == "true" and issue_id:
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
|
||||
@@ -116,7 +116,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
issue_count = (
|
||||
Issue.objects.filter(workspace=OuterRef("id"))
|
||||
Issue.issue_objects.filter(workspace=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -203,7 +203,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
issue_count = (
|
||||
Issue.objects.filter(workspace=OuterRef("id"))
|
||||
Issue.issue_objects.filter(workspace=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -532,7 +532,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
||||
# Delete joined workspace invites
|
||||
workspace_invitations.delete()
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
@@ -846,7 +846,7 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
|
||||
workspace_member.view_props = request.data.get("view_props", {})
|
||||
workspace_member.save()
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except WorkspaceMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "User not a member of workspace"},
|
||||
@@ -1072,10 +1072,10 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
.order_by("state_group")
|
||||
)
|
||||
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
|
||||
priority_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
assignees__in=[user_id],
|
||||
project__project_projectmember__member=request.user,
|
||||
|
||||
@@ -393,130 +393,6 @@ def track_assignees(
|
||||
)
|
||||
|
||||
|
||||
# Track changes in blocking issues
|
||||
def track_blocks(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if len(requested_data.get("blocks_list")) > len(
|
||||
current_instance.get("blocked_issues")
|
||||
):
|
||||
for block in requested_data.get("blocks_list"):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
blocked
|
||||
for blocked in current_instance.get("blocked_issues")
|
||||
if blocked.get("block") == block
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
issue = Issue.objects.get(pk=block)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"added blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Blocked Issue Removal
|
||||
if len(requested_data.get("blocks_list")) < len(
|
||||
current_instance.get("blocked_issues")
|
||||
):
|
||||
for blocked in current_instance.get("blocked_issues"):
|
||||
if blocked.get("block") not in requested_data.get("blocks_list"):
|
||||
issue = Issue.objects.get(pk=blocked.get("block"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"removed blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in blocked_by issues
|
||||
def track_blockings(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if len(requested_data.get("blockers_list")) > len(
|
||||
current_instance.get("blocker_issues")
|
||||
):
|
||||
for block in requested_data.get("blockers_list"):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
blocked
|
||||
for blocked in current_instance.get("blocker_issues")
|
||||
if blocked.get("blocked_by") == block
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
issue = Issue.objects.get(pk=block)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"added blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Blocked Issue Removal
|
||||
if len(requested_data.get("blockers_list")) < len(
|
||||
current_instance.get("blocker_issues")
|
||||
):
|
||||
for blocked in current_instance.get("blocker_issues"):
|
||||
if blocked.get("blocked_by") not in requested_data.get("blockers_list"):
|
||||
issue = Issue.objects.get(pk=blocked.get("blocked_by"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"removed blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
@@ -637,8 +513,6 @@ def update_issue_activity(
|
||||
"start_date": track_start_date,
|
||||
"labels_list": track_labels,
|
||||
"assignees_list": track_assignees,
|
||||
"blocks_list": track_blocks,
|
||||
"blockers_list": track_blockings,
|
||||
"estimate_point": track_estimate_points,
|
||||
"archived_at": track_archive_at,
|
||||
"closed_to": track_closed_to,
|
||||
@@ -1170,6 +1044,57 @@ def delete_issue_vote_activity(
|
||||
)
|
||||
|
||||
|
||||
def create_issue_relation_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance is None and requested_data.get("related_list") is not None:
|
||||
for issue_relation in requested_data.get("related_list"):
|
||||
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_relation.get("issue"),
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field=f'{issue_relation.get("relation_type")}',
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f'added {issue_relation.get("relation_type")} relation',
|
||||
old_identifier=issue_relation.get("issue"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_relation_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance is not None and requested_data.get("related_list") is None:
|
||||
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=current_instance.get("issue"),
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field=f'{current_instance.get("relation_type")}',
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f'deleted the {current_instance.get("relation_type")} relation',
|
||||
old_identifier=current_instance.get("issue"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Receive message from room group
|
||||
@shared_task
|
||||
def issue_activity(
|
||||
@@ -1233,6 +1158,8 @@ def issue_activity(
|
||||
"link.activity.deleted": delete_link_activity,
|
||||
"attachment.activity.created": create_attachment_activity,
|
||||
"attachment.activity.deleted": delete_attachment_activity,
|
||||
"issue_relation.activity.created": create_issue_relation_activity,
|
||||
"issue_relation.activity.deleted": delete_issue_relation_activity,
|
||||
"issue_reaction.activity.created": create_issue_reaction_activity,
|
||||
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
|
||||
"comment_reaction.activity.created": create_comment_reaction_activity,
|
||||
|
||||
@@ -32,7 +32,7 @@ def archive_old_issues():
|
||||
archive_in = project.archive_in
|
||||
|
||||
# Get all the issues whose updated_at in less that the archive_in month
|
||||
issues = Issue.objects.filter(
|
||||
issues = Issue.issue_objects.filter(
|
||||
Q(
|
||||
project=project_id,
|
||||
archived_at__isnull=True,
|
||||
@@ -64,21 +64,22 @@ def archive_old_issues():
|
||||
issues_to_update.append(issue)
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
updated_issues = Issue.objects.bulk_update(
|
||||
issues_to_update, ["archived_at"], batch_size=100
|
||||
)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
if issues_to_update:
|
||||
updated_issues = Issue.objects.bulk_update(
|
||||
issues_to_update, ["archived_at"], batch_size=100
|
||||
)
|
||||
for issue in updated_issues
|
||||
]
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
)
|
||||
for issue in updated_issues
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
if settings.DEBUG:
|
||||
@@ -99,7 +100,7 @@ def close_old_issues():
|
||||
close_in = project.close_in
|
||||
|
||||
# Get all the issues whose updated_at in less that the close_in month
|
||||
issues = Issue.objects.filter(
|
||||
issues = Issue.issue_objects.filter(
|
||||
Q(
|
||||
project=project_id,
|
||||
archived_at__isnull=True,
|
||||
@@ -136,19 +137,20 @@ def close_old_issues():
|
||||
issues_to_update.append(issue)
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
)
|
||||
for issue in updated_issues
|
||||
]
|
||||
if issues_to_update:
|
||||
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
)
|
||||
for issue in updated_issues
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
if settings.DEBUG:
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-12 07:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from plane.db.models import IssueRelation
|
||||
from sentry_sdk import capture_exception
|
||||
import uuid
|
||||
|
||||
|
||||
def create_issue_relation(apps, schema_editor):
|
||||
try:
|
||||
IssueBlockerModel = apps.get_model("db", "IssueBlocker")
|
||||
updated_issue_relation = []
|
||||
for blocked_issue in IssueBlockerModel.objects.all():
|
||||
updated_issue_relation.append(
|
||||
IssueRelation(
|
||||
issue_id=blocked_issue.block_id,
|
||||
related_issue_id=blocked_issue.blocked_by_id,
|
||||
relation_type="blocked_by",
|
||||
project_id=blocked_issue.project_id,
|
||||
workspace_id=blocked_issue.workspace_id,
|
||||
created_by_id=blocked_issue.created_by_id,
|
||||
updated_by_id=blocked_issue.updated_by_id,
|
||||
)
|
||||
)
|
||||
IssueRelation.objects.bulk_create(updated_issue_relation, batch_size=100)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
|
||||
|
||||
def update_issue_priority_choice(apps, schema_editor):
|
||||
IssueModel = apps.get_model("db", "Issue")
|
||||
updated_issues = []
|
||||
for obj in IssueModel.objects.all():
|
||||
if obj.priority is None:
|
||||
obj.priority = "none"
|
||||
updated_issues.append(obj)
|
||||
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0042_alter_analyticview_created_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IssueRelation',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('relation_type', models.CharField(choices=[('duplicate', 'Duplicate'), ('relates_to', 'Relates To'), ('blocked_by', 'Blocked By')], default='blocked_by', max_length=20, verbose_name='Issue Relation Type')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_relation', to='db.issue')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||
('related_issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_related', to='db.issue')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Relation',
|
||||
'verbose_name_plural': 'Issue Relations',
|
||||
'db_table': 'issue_relations',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('issue', 'related_issue')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='is_draft',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issue',
|
||||
name='priority',
|
||||
field=models.CharField(choices=[('urgent', 'Urgent'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low'), ('none', 'None')], default='none', max_length=30, verbose_name='Issue Priority'),
|
||||
),
|
||||
migrations.RunPython(create_issue_relation),
|
||||
migrations.RunPython(update_issue_priority_choice),
|
||||
]
|
||||
138
apiserver/plane/db/migrations/0044_auto_20230913_0709.py
Normal file
138
apiserver/plane/db/migrations/0044_auto_20230913_0709.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-13 07:09
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def workspace_member_props(old_props):
|
||||
new_props = {
|
||||
"filters": {
|
||||
"priority": old_props.get("filters", {}).get("priority", None),
|
||||
"state": old_props.get("filters", {}).get("state", None),
|
||||
"state_group": old_props.get("filters", {}).get("state_group", None),
|
||||
"assignees": old_props.get("filters", {}).get("assignees", None),
|
||||
"created_by": old_props.get("filters", {}).get("created_by", None),
|
||||
"labels": old_props.get("filters", {}).get("labels", None),
|
||||
"start_date": old_props.get("filters", {}).get("start_date", None),
|
||||
"target_date": old_props.get("filters", {}).get("target_date", None),
|
||||
"subscriber": old_props.get("filters", {}).get("subscriber", None),
|
||||
},
|
||||
"display_filters": {
|
||||
"group_by": old_props.get("groupByProperty", None),
|
||||
"order_by": old_props.get("orderBy", "-created_at"),
|
||||
"type": old_props.get("filters", {}).get("type", None),
|
||||
"sub_issue": old_props.get("showSubIssues", True),
|
||||
"show_empty_groups": old_props.get("showEmptyGroups", True),
|
||||
"layout": old_props.get("issueView", "list"),
|
||||
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||
},
|
||||
"display_properties": {
|
||||
"assignee": old_props.get("properties", {}).get("assignee",None),
|
||||
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
|
||||
"created_on": old_props.get("properties", {}).get("created_on", None),
|
||||
"due_date": old_props.get("properties", {}).get("due_date", None),
|
||||
"estimate": old_props.get("properties", {}).get("estimate", None),
|
||||
"key": old_props.get("properties", {}).get("key", None),
|
||||
"labels": old_props.get("properties", {}).get("labels", None),
|
||||
"link": old_props.get("properties", {}).get("link", None),
|
||||
"priority": old_props.get("properties", {}).get("priority", None),
|
||||
"start_date": old_props.get("properties", {}).get("start_date", None),
|
||||
"state": old_props.get("properties", {}).get("state", None),
|
||||
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
|
||||
"updated_on": old_props.get("properties", {}).get("updated_on", None),
|
||||
},
|
||||
}
|
||||
return new_props
|
||||
|
||||
|
||||
def project_member_props(old_props):
|
||||
new_props = {
|
||||
"filters": {
|
||||
"priority": old_props.get("filters", {}).get("priority", None),
|
||||
"state": old_props.get("filters", {}).get("state", None),
|
||||
"state_group": old_props.get("filters", {}).get("state_group", None),
|
||||
"assignees": old_props.get("filters", {}).get("assignees", None),
|
||||
"created_by": old_props.get("filters", {}).get("created_by", None),
|
||||
"labels": old_props.get("filters", {}).get("labels", None),
|
||||
"start_date": old_props.get("filters", {}).get("start_date", None),
|
||||
"target_date": old_props.get("filters", {}).get("target_date", None),
|
||||
"subscriber": old_props.get("filters", {}).get("subscriber", None),
|
||||
},
|
||||
"display_filters": {
|
||||
"group_by": old_props.get("groupByProperty", None),
|
||||
"order_by": old_props.get("orderBy", "-created_at"),
|
||||
"type": old_props.get("filters", {}).get("type", None),
|
||||
"sub_issue": old_props.get("showSubIssues", True),
|
||||
"show_empty_groups": old_props.get("showEmptyGroups", True),
|
||||
"layout": old_props.get("issueView", "list"),
|
||||
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||
},
|
||||
}
|
||||
return new_props
|
||||
|
||||
|
||||
def cycle_module_props(old_props):
|
||||
new_props = {
|
||||
"filters": {
|
||||
"priority": old_props.get("filters", {}).get("priority", None),
|
||||
"state": old_props.get("filters", {}).get("state", None),
|
||||
"state_group": old_props.get("filters", {}).get("state_group", None),
|
||||
"assignees": old_props.get("filters", {}).get("assignees", None),
|
||||
"created_by": old_props.get("filters", {}).get("created_by", None),
|
||||
"labels": old_props.get("filters", {}).get("labels", None),
|
||||
"start_date": old_props.get("filters", {}).get("start_date", None),
|
||||
"target_date": old_props.get("filters", {}).get("target_date", None),
|
||||
"subscriber": old_props.get("filters", {}).get("subscriber", None),
|
||||
},
|
||||
}
|
||||
return new_props
|
||||
|
||||
|
||||
def update_workspace_member_view_props(apps, schema_editor):
|
||||
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
|
||||
updated_workspace_member = []
|
||||
for obj in WorkspaceMemberModel.objects.all():
|
||||
obj.view_props = workspace_member_props(obj.view_props)
|
||||
obj.default_props = workspace_member_props(obj.default_props)
|
||||
updated_workspace_member.append(obj)
|
||||
WorkspaceMemberModel.objects.bulk_update(updated_workspace_member, ["view_props", "default_props"], batch_size=100)
|
||||
|
||||
def update_project_member_view_props(apps, schema_editor):
|
||||
ProjectMemberModel = apps.get_model("db", "ProjectMember")
|
||||
updated_project_member = []
|
||||
for obj in ProjectMemberModel.objects.all():
|
||||
obj.view_props = project_member_props(obj.view_props)
|
||||
obj.default_props = project_member_props(obj.default_props)
|
||||
updated_project_member.append(obj)
|
||||
ProjectMemberModel.objects.bulk_update(updated_project_member, ["view_props", "default_props"], batch_size=100)
|
||||
|
||||
def update_cycle_props(apps, schema_editor):
|
||||
CycleModel = apps.get_model("db", "Cycle")
|
||||
updated_cycle = []
|
||||
for obj in CycleModel.objects.all():
|
||||
if "filter" in obj.view_props:
|
||||
obj.view_props = cycle_module_props(obj.view_props)
|
||||
updated_cycle.append(obj)
|
||||
CycleModel.objects.bulk_update(updated_cycle, ["view_props"], batch_size=100)
|
||||
|
||||
def update_module_props(apps, schema_editor):
|
||||
ModuleModel = apps.get_model("db", "Module")
|
||||
updated_module = []
|
||||
for obj in ModuleModel.objects.all():
|
||||
if "filter" in obj.view_props:
|
||||
obj.view_props = cycle_module_props(obj.view_props)
|
||||
updated_module.append(obj)
|
||||
ModuleModel.objects.bulk_update(updated_module, ["view_props"], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0043_alter_analyticview_created_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_workspace_member_view_props),
|
||||
migrations.RunPython(update_project_member_view_props),
|
||||
migrations.RunPython(update_cycle_props),
|
||||
migrations.RunPython(update_module_props),
|
||||
]
|
||||
@@ -32,6 +32,7 @@ from .issue import (
|
||||
IssueAssignee,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
IssueRelation,
|
||||
IssueLink,
|
||||
IssueSequence,
|
||||
IssueAttachment,
|
||||
|
||||
@@ -29,6 +29,7 @@ class IssueManager(models.Manager):
|
||||
| models.Q(issue_inbox__isnull=True)
|
||||
)
|
||||
.exclude(archived_at__isnull=False)
|
||||
.exclude(is_draft=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -38,6 +39,7 @@ class Issue(ProjectBaseModel):
|
||||
("high", "High"),
|
||||
("medium", "Medium"),
|
||||
("low", "Low"),
|
||||
("none", "None")
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
@@ -64,8 +66,7 @@ class Issue(ProjectBaseModel):
|
||||
max_length=30,
|
||||
choices=PRIORITY_CHOICES,
|
||||
verbose_name="Issue Priority",
|
||||
null=True,
|
||||
blank=True,
|
||||
default="none",
|
||||
)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
target_date = models.DateField(null=True, blank=True)
|
||||
@@ -83,6 +84,7 @@ class Issue(ProjectBaseModel):
|
||||
sort_order = models.FloatField(default=65535)
|
||||
completed_at = models.DateTimeField(null=True)
|
||||
archived_at = models.DateField(null=True)
|
||||
is_draft = models.BooleanField(default=False)
|
||||
|
||||
objects = models.Manager()
|
||||
issue_objects = IssueManager()
|
||||
@@ -178,6 +180,37 @@ class IssueBlocker(ProjectBaseModel):
|
||||
return f"{self.block.name} {self.blocked_by.name}"
|
||||
|
||||
|
||||
class IssueRelation(ProjectBaseModel):
|
||||
RELATION_CHOICES = (
|
||||
("duplicate", "Duplicate"),
|
||||
("relates_to", "Relates To"),
|
||||
("blocked_by", "Blocked By"),
|
||||
)
|
||||
|
||||
issue = models.ForeignKey(
|
||||
Issue, related_name="issue_relation", on_delete=models.CASCADE
|
||||
)
|
||||
related_issue = models.ForeignKey(
|
||||
Issue, related_name="issue_related", on_delete=models.CASCADE
|
||||
)
|
||||
relation_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=RELATION_CHOICES,
|
||||
verbose_name="Issue Relation Type",
|
||||
default="blocked_by",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "related_issue"]
|
||||
verbose_name = "Issue Relation"
|
||||
verbose_name_plural = "Issue Relations"
|
||||
db_table = "issue_relations"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.issue.name} {self.related_issue.name}"
|
||||
|
||||
|
||||
class IssueAssignee(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
|
||||
|
||||
@@ -96,7 +96,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
||||
chart_data = {str(date): 0 for date in date_range}
|
||||
|
||||
completed_issues_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
@@ -118,7 +118,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
||||
chart_data = {str(date): 0 for date in date_range}
|
||||
|
||||
completed_issues_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_module__module_id=module_id,
|
||||
|
||||
@@ -15,7 +15,7 @@ def resolve_keys(group_keys, value):
|
||||
return value
|
||||
|
||||
|
||||
def group_results(results_data, group_by):
|
||||
def group_results(results_data, group_by, sub_group_by=False):
|
||||
"""group results data into certain group_by
|
||||
|
||||
Args:
|
||||
@@ -25,38 +25,140 @@ def group_results(results_data, group_by):
|
||||
Returns:
|
||||
obj: grouped results
|
||||
"""
|
||||
response_dict = dict()
|
||||
if sub_group_by:
|
||||
main_responsive_dict = dict()
|
||||
|
||||
if group_by == "priority":
|
||||
response_dict = {
|
||||
"urgent": [],
|
||||
"high": [],
|
||||
"medium": [],
|
||||
"low": [],
|
||||
"None": [],
|
||||
}
|
||||
if sub_group_by == "priority":
|
||||
main_responsive_dict = {
|
||||
"urgent": {},
|
||||
"high": {},
|
||||
"medium": {},
|
||||
"low": {},
|
||||
"none": {},
|
||||
}
|
||||
|
||||
for value in results_data:
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
if isinstance(group_attribute, list):
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in response_dict:
|
||||
response_dict[str(attrib)].append(value)
|
||||
else:
|
||||
response_dict[str(attrib)] = []
|
||||
response_dict[str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in response_dict:
|
||||
response_dict[str(None)].append(value)
|
||||
for value in results_data:
|
||||
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list):
|
||||
if len(main_group_attribute):
|
||||
for attrib in main_group_attribute:
|
||||
if str(attrib) not in main_responsive_dict:
|
||||
main_responsive_dict[str(attrib)] = {}
|
||||
if str(group_attribute) in main_responsive_dict[str(attrib)]:
|
||||
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(attrib)][str(group_attribute)] = []
|
||||
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||
else:
|
||||
response_dict[str(None)] = []
|
||||
response_dict[str(None)].append(value)
|
||||
else:
|
||||
if str(group_attribute) in response_dict:
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
else:
|
||||
response_dict[str(group_attribute)] = []
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
if str(None) not in main_responsive_dict:
|
||||
main_responsive_dict[str(None)] = {}
|
||||
|
||||
return response_dict
|
||||
if str(group_attribute) in main_responsive_dict[str(None)]:
|
||||
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(None)][str(group_attribute)] = []
|
||||
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||
|
||||
elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list):
|
||||
if str(main_group_attribute) not in main_responsive_dict:
|
||||
main_responsive_dict[str(main_group_attribute)] = {}
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in main_responsive_dict[str(main_group_attribute)]:
|
||||
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_group_attribute)][str(attrib)] = []
|
||||
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in main_responsive_dict[str(main_group_attribute)]:
|
||||
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_group_attribute)][str(None)] = []
|
||||
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
|
||||
|
||||
elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list):
|
||||
if len(main_group_attribute):
|
||||
for main_attrib in main_group_attribute:
|
||||
if str(main_attrib) not in main_responsive_dict:
|
||||
main_responsive_dict[str(main_attrib)] = {}
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in main_responsive_dict[str(main_attrib)]:
|
||||
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_attrib)][str(attrib)] = []
|
||||
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in main_responsive_dict[str(main_attrib)]:
|
||||
main_responsive_dict[str(main_attrib)][str(None)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_attrib)][str(None)] = []
|
||||
main_responsive_dict[str(main_attrib)][str(None)].append(value)
|
||||
else:
|
||||
if str(None) not in main_responsive_dict:
|
||||
main_responsive_dict[str(None)] = {}
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in main_responsive_dict[str(None)]:
|
||||
main_responsive_dict[str(None)][str(attrib)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(None)][str(attrib)] = []
|
||||
main_responsive_dict[str(None)][str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in main_responsive_dict[str(None)]:
|
||||
main_responsive_dict[str(None)][str(None)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(None)][str(None)] = []
|
||||
main_responsive_dict[str(None)][str(None)].append(value)
|
||||
else:
|
||||
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
|
||||
if str(main_group_attribute) not in main_responsive_dict:
|
||||
main_responsive_dict[str(main_group_attribute)] = {}
|
||||
|
||||
if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]:
|
||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
|
||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
||||
|
||||
return main_responsive_dict
|
||||
|
||||
else:
|
||||
response_dict = dict()
|
||||
|
||||
if group_by == "priority":
|
||||
response_dict = {
|
||||
"urgent": [],
|
||||
"high": [],
|
||||
"medium": [],
|
||||
"low": [],
|
||||
"none": [],
|
||||
}
|
||||
|
||||
for value in results_data:
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
if isinstance(group_attribute, list):
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in response_dict:
|
||||
response_dict[str(attrib)].append(value)
|
||||
else:
|
||||
response_dict[str(attrib)] = []
|
||||
response_dict[str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in response_dict:
|
||||
response_dict[str(None)].append(value)
|
||||
else:
|
||||
response_dict[str(None)] = []
|
||||
response_dict[str(None)].append(value)
|
||||
else:
|
||||
if str(group_attribute) in response_dict:
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
else:
|
||||
response_dict[str(group_attribute)] = []
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
|
||||
return response_dict
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
|
||||
def filter_state(params, filter, method):
|
||||
if method == "GET":
|
||||
states = params.get("state").split(",")
|
||||
@@ -23,7 +24,6 @@ def filter_state_group(params, filter, method):
|
||||
return filter
|
||||
|
||||
|
||||
|
||||
def filter_estimate_point(params, filter, method):
|
||||
if method == "GET":
|
||||
estimate_points = params.get("estimate_point").split(",")
|
||||
@@ -39,25 +39,10 @@ def filter_priority(params, filter, method):
|
||||
if method == "GET":
|
||||
priorities = params.get("priority").split(",")
|
||||
if len(priorities) and "" not in priorities:
|
||||
if len(priorities) == 1 and "null" in priorities:
|
||||
filter["priority__isnull"] = True
|
||||
elif len(priorities) > 1 and "null" in priorities:
|
||||
filter["priority__isnull"] = True
|
||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
||||
else:
|
||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
||||
|
||||
filter["priority__in"] = priorities
|
||||
else:
|
||||
if params.get("priority", None) and len(params.get("priority")):
|
||||
priorities = params.get("priority")
|
||||
if len(priorities) == 1 and "null" in priorities:
|
||||
filter["priority__isnull"] = True
|
||||
elif len(priorities) > 1 and "null" in priorities:
|
||||
filter["priority__isnull"] = True
|
||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
||||
else:
|
||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
||||
|
||||
filter["priority__in"] = params.get("priority")
|
||||
return filter
|
||||
|
||||
|
||||
@@ -229,7 +214,6 @@ def filter_issue_state_type(params, filter, method):
|
||||
return filter
|
||||
|
||||
|
||||
|
||||
def filter_project(params, filter, method):
|
||||
if method == "GET":
|
||||
projects = params.get("project").split(",")
|
||||
@@ -329,7 +313,7 @@ def issue_filters(query_params, method):
|
||||
"module": filter_module,
|
||||
"inbox_status": filter_inbox_status,
|
||||
"sub_issue": filter_sub_issue_toggle,
|
||||
"subscriber": filter_subscribed_issues,
|
||||
"subscriber": filter_subscribed_issues,
|
||||
"start_target_date": filter_start_target_date_issues,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
# base requirements
|
||||
|
||||
Django==4.2.3
|
||||
Django==4.2.5
|
||||
django-braces==1.15.0
|
||||
django-taggit==4.0.0
|
||||
psycopg==3.1.9
|
||||
psycopg==3.1.10
|
||||
django-oauth-toolkit==2.3.0
|
||||
mistune==3.0.1
|
||||
djangorestframework==3.14.0
|
||||
redis==4.6.0
|
||||
django-nested-admin==4.0.2
|
||||
django-cors-headers==4.1.0
|
||||
django-cors-headers==4.2.0
|
||||
whitenoise==6.5.0
|
||||
django-allauth==0.54.0
|
||||
django-allauth==0.55.2
|
||||
faker==18.11.2
|
||||
django-filter==23.2
|
||||
jsonmodels==2.6.0
|
||||
djangorestframework-simplejwt==5.2.2
|
||||
sentry-sdk==1.27.0
|
||||
djangorestframework-simplejwt==5.3.0
|
||||
sentry-sdk==1.30.0
|
||||
django-s3-storage==0.14.0
|
||||
django-crum==0.7.9
|
||||
django-guardian==2.4.0
|
||||
dj_rest_auth==2.2.5
|
||||
google-auth==2.21.0
|
||||
google-api-python-client==2.92.0
|
||||
google-auth==2.22.0
|
||||
google-api-python-client==2.97.0
|
||||
django-redis==5.3.0
|
||||
uvicorn==0.22.0
|
||||
uvicorn==0.23.2
|
||||
channels==4.0.0
|
||||
openai==0.27.8
|
||||
openai==0.28.0
|
||||
slack-sdk==3.21.3
|
||||
celery==5.3.1
|
||||
celery==5.3.4
|
||||
django_celery_beat==2.5.0
|
||||
psycopg-binary==3.1.9
|
||||
psycopg-c==3.1.9
|
||||
psycopg-binary==3.1.10
|
||||
psycopg-c==3.1.10
|
||||
scout-apm==2.26.1
|
||||
openpyxl==3.1.2
|
||||
@@ -1,11 +1,11 @@
|
||||
-r base.txt
|
||||
|
||||
dj-database-url==2.0.0
|
||||
gunicorn==20.1.0
|
||||
dj-database-url==2.1.0
|
||||
gunicorn==21.2.0
|
||||
whitenoise==6.5.0
|
||||
django-storages==1.13.2
|
||||
boto3==1.27.0
|
||||
django-anymail==10.0
|
||||
django-storages==1.14
|
||||
boto3==1.28.40
|
||||
django-anymail==10.1
|
||||
django-debug-toolbar==4.1.0
|
||||
gevent==23.7.0
|
||||
psycogreen==1.0.2
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
|
||||
plane-worker:
|
||||
container_name: planebgworker
|
||||
image: makeplane/plane-worker:latest
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/worker
|
||||
env_file:
|
||||
@@ -99,7 +99,7 @@ services:
|
||||
|
||||
plane-beat-worker:
|
||||
container_name: planebeatworker
|
||||
image: makeplane/plane-worker:latest
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/beat
|
||||
env_file:
|
||||
|
||||
@@ -90,8 +90,6 @@ services:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: ./bin/takeoff
|
||||
ports:
|
||||
- 8000:8000
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"start": "turbo run start",
|
||||
@@ -17,8 +16,12 @@
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint-config-custom": "*",
|
||||
"postcss": "^8.4.29",
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"turbo": "latest"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
|
||||
@@ -16,5 +16,7 @@ module.exports = {
|
||||
"no-duplicate-imports": "error",
|
||||
"arrow-body-style": ["error", "as-needed"],
|
||||
"react/self-closing-comp": ["error", { component: true, html: true }],
|
||||
"@next/next/no-img-element": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn"],
|
||||
},
|
||||
};
|
||||
|
||||
10
packages/tailwind-config-custom/package.json
Normal file
10
packages/tailwind-config-custom/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.0.1",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
}
|
||||
}
|
||||
7
packages/tailwind-config-custom/postcss.config.js
Normal file
7
packages/tailwind-config-custom/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
212
packages/tailwind-config-custom/tailwind.config.js
Normal file
212
packages/tailwind-config-custom/tailwind.config.js
Normal file
@@ -0,0 +1,212 @@
|
||||
const convertToRGB = (variableName) => `rgba(var(${variableName}))`;
|
||||
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: [
|
||||
"./components/**/*.tsx",
|
||||
"./constants/**/*.{js,ts,jsx,tsx}",
|
||||
"./layouts/**/*.tsx",
|
||||
"./pages/**/*.tsx",
|
||||
"./ui/**/*.tsx",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
"custom-shadow-2xs": "var(--color-shadow-2xs)",
|
||||
"custom-shadow-xs": "var(--color-shadow-xs)",
|
||||
"custom-shadow-sm": "var(--color-shadow-sm)",
|
||||
"custom-shadow-rg": "var(--color-shadow-rg)",
|
||||
"custom-shadow-md": "var(--color-shadow-md)",
|
||||
"custom-shadow-lg": "var(--color-shadow-lg)",
|
||||
"custom-shadow-xl": "var(--color-shadow-xl)",
|
||||
"custom-shadow-2xl": "var(--color-shadow-2xl)",
|
||||
"custom-shadow-3xl": "var(--color-shadow-3xl)",
|
||||
"custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)",
|
||||
"custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)",
|
||||
"custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)",
|
||||
"custom-sidebar-shadow-rg": "var(--color-sidebar-shadow-rg)",
|
||||
"custom-sidebar-shadow-md": "var(--color-sidebar-shadow-md)",
|
||||
"custom-sidebar-shadow-lg": "var(--color-sidebar-shadow-lg)",
|
||||
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
|
||||
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
|
||||
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
|
||||
},
|
||||
colors: {
|
||||
custom: {
|
||||
primary: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-primary-10"),
|
||||
20: convertToRGB("--color-primary-20"),
|
||||
30: convertToRGB("--color-primary-30"),
|
||||
40: convertToRGB("--color-primary-40"),
|
||||
50: convertToRGB("--color-primary-50"),
|
||||
60: convertToRGB("--color-primary-60"),
|
||||
70: convertToRGB("--color-primary-70"),
|
||||
80: convertToRGB("--color-primary-80"),
|
||||
90: convertToRGB("--color-primary-90"),
|
||||
100: convertToRGB("--color-primary-100"),
|
||||
200: convertToRGB("--color-primary-200"),
|
||||
300: convertToRGB("--color-primary-300"),
|
||||
400: convertToRGB("--color-primary-400"),
|
||||
500: convertToRGB("--color-primary-500"),
|
||||
600: convertToRGB("--color-primary-600"),
|
||||
700: convertToRGB("--color-primary-700"),
|
||||
800: convertToRGB("--color-primary-800"),
|
||||
900: convertToRGB("--color-primary-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-primary-100"),
|
||||
},
|
||||
background: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-background-10"),
|
||||
20: convertToRGB("--color-background-20"),
|
||||
30: convertToRGB("--color-background-30"),
|
||||
40: convertToRGB("--color-background-40"),
|
||||
50: convertToRGB("--color-background-50"),
|
||||
60: convertToRGB("--color-background-60"),
|
||||
70: convertToRGB("--color-background-70"),
|
||||
80: convertToRGB("--color-background-80"),
|
||||
90: convertToRGB("--color-background-90"),
|
||||
100: convertToRGB("--color-background-100"),
|
||||
200: convertToRGB("--color-background-200"),
|
||||
300: convertToRGB("--color-background-300"),
|
||||
400: convertToRGB("--color-background-400"),
|
||||
500: convertToRGB("--color-background-500"),
|
||||
600: convertToRGB("--color-background-600"),
|
||||
700: convertToRGB("--color-background-700"),
|
||||
800: convertToRGB("--color-background-800"),
|
||||
900: convertToRGB("--color-background-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-background-100"),
|
||||
},
|
||||
text: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-text-10"),
|
||||
20: convertToRGB("--color-text-20"),
|
||||
30: convertToRGB("--color-text-30"),
|
||||
40: convertToRGB("--color-text-40"),
|
||||
50: convertToRGB("--color-text-50"),
|
||||
60: convertToRGB("--color-text-60"),
|
||||
70: convertToRGB("--color-text-70"),
|
||||
80: convertToRGB("--color-text-80"),
|
||||
90: convertToRGB("--color-text-90"),
|
||||
100: convertToRGB("--color-text-100"),
|
||||
200: convertToRGB("--color-text-200"),
|
||||
300: convertToRGB("--color-text-300"),
|
||||
400: convertToRGB("--color-text-400"),
|
||||
500: convertToRGB("--color-text-500"),
|
||||
600: convertToRGB("--color-text-600"),
|
||||
700: convertToRGB("--color-text-700"),
|
||||
800: convertToRGB("--color-text-800"),
|
||||
900: convertToRGB("--color-text-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-text-100"),
|
||||
},
|
||||
border: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
100: convertToRGB("--color-border-100"),
|
||||
200: convertToRGB("--color-border-200"),
|
||||
300: convertToRGB("--color-border-300"),
|
||||
400: convertToRGB("--color-border-400"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-border-200"),
|
||||
},
|
||||
sidebar: {
|
||||
background: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-sidebar-background-10"),
|
||||
20: convertToRGB("--color-sidebar-background-20"),
|
||||
30: convertToRGB("--color-sidebar-background-30"),
|
||||
40: convertToRGB("--color-sidebar-background-40"),
|
||||
50: convertToRGB("--color-sidebar-background-50"),
|
||||
60: convertToRGB("--color-sidebar-background-60"),
|
||||
70: convertToRGB("--color-sidebar-background-70"),
|
||||
80: convertToRGB("--color-sidebar-background-80"),
|
||||
90: convertToRGB("--color-sidebar-background-90"),
|
||||
100: convertToRGB("--color-sidebar-background-100"),
|
||||
200: convertToRGB("--color-sidebar-background-200"),
|
||||
300: convertToRGB("--color-sidebar-background-300"),
|
||||
400: convertToRGB("--color-sidebar-background-400"),
|
||||
500: convertToRGB("--color-sidebar-background-500"),
|
||||
600: convertToRGB("--color-sidebar-background-600"),
|
||||
700: convertToRGB("--color-sidebar-background-700"),
|
||||
800: convertToRGB("--color-sidebar-background-800"),
|
||||
900: convertToRGB("--color-sidebar-background-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-background-100"),
|
||||
},
|
||||
text: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-sidebar-text-10"),
|
||||
20: convertToRGB("--color-sidebar-text-20"),
|
||||
30: convertToRGB("--color-sidebar-text-30"),
|
||||
40: convertToRGB("--color-sidebar-text-40"),
|
||||
50: convertToRGB("--color-sidebar-text-50"),
|
||||
60: convertToRGB("--color-sidebar-text-60"),
|
||||
70: convertToRGB("--color-sidebar-text-70"),
|
||||
80: convertToRGB("--color-sidebar-text-80"),
|
||||
90: convertToRGB("--color-sidebar-text-90"),
|
||||
100: convertToRGB("--color-sidebar-text-100"),
|
||||
200: convertToRGB("--color-sidebar-text-200"),
|
||||
300: convertToRGB("--color-sidebar-text-300"),
|
||||
400: convertToRGB("--color-sidebar-text-400"),
|
||||
500: convertToRGB("--color-sidebar-text-500"),
|
||||
600: convertToRGB("--color-sidebar-text-600"),
|
||||
700: convertToRGB("--color-sidebar-text-700"),
|
||||
800: convertToRGB("--color-sidebar-text-800"),
|
||||
900: convertToRGB("--color-sidebar-text-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-text-100"),
|
||||
},
|
||||
border: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
100: convertToRGB("--color-sidebar-border-100"),
|
||||
200: convertToRGB("--color-sidebar-border-200"),
|
||||
300: convertToRGB("--color-sidebar-border-300"),
|
||||
400: convertToRGB("--color-sidebar-border-400"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-border-200"),
|
||||
},
|
||||
},
|
||||
backdrop: "#131313",
|
||||
},
|
||||
},
|
||||
keyframes: {
|
||||
leftToaster: {
|
||||
"0%": { left: "-20rem" },
|
||||
"100%": { left: "0" },
|
||||
},
|
||||
rightToaster: {
|
||||
"0%": { right: "-20rem" },
|
||||
"100%": { right: "0" },
|
||||
},
|
||||
},
|
||||
typography: ({ theme }) => ({
|
||||
brand: {
|
||||
css: {
|
||||
"--tw-prose-body": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-p": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-headings": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-lead": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-links": convertToRGB("--color-primary-100"),
|
||||
"--tw-prose-bold": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-counters": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-bullets": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-hr": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-quotes": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-quote-borders": convertToRGB("--color-border"),
|
||||
"--tw-prose-code": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-pre-code": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-pre-bg": convertToRGB("--color-background-100"),
|
||||
"--tw-prose-th-borders": convertToRGB("--color-border"),
|
||||
"--tw-prose-td-borders": convertToRGB("--color-border"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
fontFamily: {
|
||||
custom: ["Inter", "sans-serif"],
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
||||
};
|
||||
@@ -17,6 +17,7 @@
|
||||
"next": "12.3.2",
|
||||
"react": "^18.2.0",
|
||||
"tsconfig": "*",
|
||||
"tailwind-config-custom": "*",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/ui/postcss.config.js
Normal file
1
packages/ui/postcss.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require("tailwind-config-custom/postcss.config");
|
||||
1
packages/ui/tailwind.config.js
Normal file
1
packages/ui/tailwind.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require("tailwind-config-custom/tailwind.config");
|
||||
@@ -1,9 +1,5 @@
|
||||
{
|
||||
"extends": "../tsconfig/nextjs.json",
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM"]
|
||||
}
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ fi
|
||||
# Only perform action if $FROM and $TO are different.
|
||||
echo "Replacing all statically built instances of $FROM with this string $TO ."
|
||||
|
||||
grep -R -la "${FROM}" apps/$DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
|
||||
grep -R -la "${FROM}" $DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
rules: {
|
||||
"@next/next/no-img-element": "off",
|
||||
},
|
||||
};
|
||||
|
||||
2
space/additional.d.ts
vendored
Normal file
2
space/additional.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// additional.d.ts
|
||||
/// <reference types="next-images" />
|
||||
@@ -1,9 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
// react hook form
|
||||
import { useForm } from "react-hook-form";
|
||||
// components
|
||||
import { EmailResetPasswordForm } from "./email-reset-password-form";
|
||||
|
||||
@@ -131,7 +131,7 @@ export const OnBoardingForm: React.FC<Props> = observer(({ user }) => {
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none px-3 py-2 text-sm`}
|
||||
>
|
||||
<span className="text-custom-text-400">{value || "Select your role..."}</span>
|
||||
<span className={value ? "" : "text-custom-text-400"}>{value || "Select your role..."}</span>
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</Listbox.Button>
|
||||
|
||||
|
||||
@@ -13,13 +13,12 @@ import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
||||
// images
|
||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.svg";
|
||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
|
||||
|
||||
export const SignInView = observer(() => {
|
||||
const { user: userStore } = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { next_path } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@@ -34,13 +33,15 @@ export const SignInView = observer(() => {
|
||||
const onSignInSuccess = (response: any) => {
|
||||
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
|
||||
|
||||
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/";
|
||||
|
||||
userStore.setCurrentUser(response?.user);
|
||||
|
||||
if (!isOnboarded) {
|
||||
router.push(`/onboarding?next_path=${next_path}`);
|
||||
router.push(`/onboarding?next_path=${nextPath}`);
|
||||
return;
|
||||
}
|
||||
router.push((next_path ?? "/").toString());
|
||||
router.push((nextPath ?? "/").toString());
|
||||
};
|
||||
|
||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||
@@ -111,7 +112,7 @@ export const SignInView = observer(() => {
|
||||
<div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28">
|
||||
<div className="grid place-items-center bg-custom-background-100">
|
||||
<div className="h-[30px] w-[30px]">
|
||||
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
|
||||
<img src={`${imagePrefix}/plane-logos/blue-without-text.png`} alt="Plane Logo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
"use client";
|
||||
|
||||
// helpers
|
||||
import { renderFullDate } from "constants/helpers";
|
||||
import { renderFullDate } from "helpers/date-time.helper";
|
||||
|
||||
export const findHowManyDaysLeft = (date: string | Date) => {
|
||||
const today = new Date();
|
||||
const eventDate = new Date(date);
|
||||
const timeDiff = Math.abs(eventDate.getTime() - today.getTime());
|
||||
|
||||
return Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||
};
|
||||
|
||||
const dueDateIcon = (
|
||||
export const dueDateIconDetails = (
|
||||
date: string,
|
||||
stateGroup: string
|
||||
): {
|
||||
@@ -26,17 +18,24 @@ const dueDateIcon = (
|
||||
className = "";
|
||||
} else {
|
||||
const today = new Date();
|
||||
const dueDate = new Date(date);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const targetDate = new Date(date);
|
||||
targetDate.setHours(0, 0, 0, 0);
|
||||
|
||||
if (dueDate < today) {
|
||||
const timeDifference = targetDate.getTime() - today.getTime();
|
||||
|
||||
if (timeDifference < 0) {
|
||||
iconName = "event_busy";
|
||||
className = "text-red-500";
|
||||
} else if (dueDate > today) {
|
||||
iconName = "calendar_today";
|
||||
className = "";
|
||||
} else {
|
||||
} else if (timeDifference === 0) {
|
||||
iconName = "today";
|
||||
className = "text-red-500";
|
||||
} else if (timeDifference === 24 * 60 * 60 * 1000) {
|
||||
iconName = "event";
|
||||
className = "text-yellow-500";
|
||||
} else {
|
||||
iconName = "calendar_today";
|
||||
className = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +46,7 @@ const dueDateIcon = (
|
||||
};
|
||||
|
||||
export const IssueBlockDueDate = ({ due_date, group }: { due_date: string; group: string }) => {
|
||||
const iconDetails = dueDateIcon(due_date, group);
|
||||
const iconDetails = dueDateIconDetails(due_date, group);
|
||||
|
||||
return (
|
||||
<div className="rounded flex px-2.5 py-1 items-center border-[0.5px] border-custom-border-300 gap-1 text-custom-text-100 text-xs">
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import React, { useState } from "react";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// lib
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { TipTapEditor } from "components/tiptap";
|
||||
import { CommentReactions } from "components/issues/peek-overview";
|
||||
// icons
|
||||
import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { Comment } from "types/issue";
|
||||
// components
|
||||
import { TipTapEditor } from "components/tiptap";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@@ -25,10 +30,13 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
// states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const editorRef = React.useRef<any>(null);
|
||||
|
||||
const showEditorRef = React.useRef<any>(null);
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
} = useForm<any>({
|
||||
defaultValues: { comment_html: comment.comment_html },
|
||||
});
|
||||
@@ -42,6 +50,9 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
if (!workspaceSlug || !issueDetailStore.peekId) return;
|
||||
issueDetailStore.updateIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id, formData);
|
||||
setIsEditing(false);
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -76,7 +87,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
<>Commented {timeAgo(comment.created_at)}</>
|
||||
<>commented {timeAgo(comment.created_at)}</>
|
||||
</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
@@ -91,6 +102,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TipTapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={value}
|
||||
debouncedUpdatesEnabled={false}
|
||||
customClassName="min-h-[50px] p-3 shadow-sm"
|
||||
@@ -120,11 +132,13 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
</form>
|
||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<TipTapEditor
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html}
|
||||
editable={false}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
/>
|
||||
<CommentReactions commentId={comment.id} projectId={comment.project} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,131 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { ReactionSelector, Tooltip } from "components/ui";
|
||||
// helpers
|
||||
import { groupReactions, renderEmoji } from "helpers/emoji.helper";
|
||||
|
||||
type Props = {
|
||||
commentId: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const CommentReactions: React.FC<Props> = observer((props) => {
|
||||
const { commentId, projectId } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspace_slug } = router.query;
|
||||
|
||||
const { issueDetails: issueDetailsStore, user: userStore } = useMobxStore();
|
||||
|
||||
const peekId = issueDetailsStore.peekId;
|
||||
const user = userStore.currentUser;
|
||||
|
||||
const commentReactions = peekId
|
||||
? issueDetailsStore.details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions
|
||||
: [];
|
||||
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
|
||||
|
||||
const userReactions = commentReactions?.filter((r) => r.actor_detail.id === user?.id);
|
||||
|
||||
const handleAddReaction = (reactionHex: string) => {
|
||||
if (!workspace_slug || !projectId || !peekId) return;
|
||||
|
||||
issueDetailsStore.addCommentReaction(
|
||||
workspace_slug.toString(),
|
||||
projectId.toString(),
|
||||
peekId,
|
||||
commentId,
|
||||
reactionHex
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveReaction = (reactionHex: string) => {
|
||||
if (!workspace_slug || !projectId || !peekId) return;
|
||||
|
||||
issueDetailsStore.removeCommentReaction(
|
||||
workspace_slug.toString(),
|
||||
projectId.toString(),
|
||||
peekId,
|
||||
commentId,
|
||||
reactionHex
|
||||
);
|
||||
};
|
||||
|
||||
const handleReactionClick = (reactionHex: string) => {
|
||||
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||
|
||||
if (userReaction) handleRemoveReaction(reactionHex);
|
||||
else handleAddReaction(reactionHex);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-1.5 items-center mt-2">
|
||||
<ReactionSelector
|
||||
onSelect={(value) => {
|
||||
userStore.requiredLogin(() => {
|
||||
handleReactionClick(value);
|
||||
});
|
||||
}}
|
||||
position="top"
|
||||
selected={userReactions?.map((r) => r.reaction)}
|
||||
size="md"
|
||||
/>
|
||||
|
||||
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||
const reactions = groupedReactions?.[reaction] ?? [];
|
||||
const REACTIONS_LIMIT = 1000;
|
||||
|
||||
if (reactions.length > 0)
|
||||
return (
|
||||
<Tooltip
|
||||
key={reaction}
|
||||
tooltipContent={
|
||||
<div>
|
||||
{reactions
|
||||
.map((r) => r.actor_detail.display_name)
|
||||
.splice(0, REACTIONS_LIMIT)
|
||||
.join(", ")}
|
||||
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
userStore.requiredLogin(() => {
|
||||
handleReactionClick(reaction);
|
||||
});
|
||||
}}
|
||||
className={`flex items-center gap-1 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
|
||||
commentReactions?.some(
|
||||
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
|
||||
)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={
|
||||
commentReactions?.some(
|
||||
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
|
||||
)
|
||||
? "text-custom-primary-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{groupedReactions?.[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
3
space/components/issues/peek-overview/comment/index.ts
Normal file
3
space/components/issues/peek-overview/comment/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./add-comment";
|
||||
export * from "./comment-detail-card";
|
||||
export * from "./comment-reactions";
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
@@ -41,7 +43,7 @@ const peekModes: {
|
||||
},
|
||||
];
|
||||
|
||||
export const PeekOverviewHeader: React.FC<Props> = (props) => {
|
||||
export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, issueDetails } = props;
|
||||
|
||||
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||
@@ -137,4 +139,4 @@ export const PeekOverviewHeader: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./comment";
|
||||
export * from "./full-screen-peek-view";
|
||||
export * from "./header";
|
||||
export * from "./issue-activity";
|
||||
@@ -8,5 +9,3 @@ export * from "./side-peek-view";
|
||||
export * from "./issue-reaction";
|
||||
export * from "./issue-vote-reactions";
|
||||
export * from "./issue-emoji-reactions";
|
||||
export * from "./comment-detail-card";
|
||||
export * from "./add-comment";
|
||||
|
||||
@@ -20,18 +20,27 @@ export const IssueEmojiReactions: React.FC = observer(() => {
|
||||
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
|
||||
const groupedReactions = groupReactions(reactions, "reaction");
|
||||
|
||||
const handleReactionSelectClick = (reactionHex: string) => {
|
||||
const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);
|
||||
|
||||
const handleAddReaction = (reactionHex: string) => {
|
||||
if (!workspace_slug || !project_slug || !issueId) return;
|
||||
const userReaction = reactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||
if (userReaction) return;
|
||||
|
||||
issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||
};
|
||||
|
||||
const handleReactionClick = (reactionHex: string) => {
|
||||
const handleRemoveReaction = (reactionHex: string) => {
|
||||
if (!workspace_slug || !project_slug || !issueId) return;
|
||||
|
||||
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||
};
|
||||
|
||||
const handleReactionClick = (reactionHex: string) => {
|
||||
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||
|
||||
if (userReaction) handleRemoveReaction(reactionHex);
|
||||
else handleAddReaction(reactionHex);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) return;
|
||||
userStore.fetchCurrentUser();
|
||||
@@ -42,9 +51,10 @@ export const IssueEmojiReactions: React.FC = observer(() => {
|
||||
<ReactionSelector
|
||||
onSelect={(value) => {
|
||||
userStore.requiredLogin(() => {
|
||||
handleReactionSelectClick(value);
|
||||
handleReactionClick(value);
|
||||
});
|
||||
}}
|
||||
selected={userReactions?.map((r) => r.reaction)}
|
||||
size="md"
|
||||
/>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import useToast from "hooks/use-toast";
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
import { copyTextToClipboard, addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// helpers
|
||||
import { renderDateFormat } from "constants/helpers";
|
||||
import { copyTextToClipboard, addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
import { renderFullDate } from "helpers/date-time.helper";
|
||||
import { dueDateIconDetails } from "../board-views/block-due-date";
|
||||
// types
|
||||
import { IIssue } from "types/issue";
|
||||
import { IPeekMode } from "store/issue_details";
|
||||
@@ -16,35 +17,16 @@ type Props = {
|
||||
mode?: IPeekMode;
|
||||
};
|
||||
|
||||
const validDate = (date: any, state: string): string => {
|
||||
if (date === null || ["backlog", "unstarted", "cancelled"].includes(state))
|
||||
return `bg-gray-500/10 text-gray-500 border-gray-500/50`;
|
||||
else {
|
||||
const today = new Date();
|
||||
const dueDate = new Date(date);
|
||||
|
||||
if (dueDate < today) return `bg-red-500/10 text-red-500 border-red-500/50`;
|
||||
else return `bg-green-500/10 text-green-500 border-green-500/50`;
|
||||
}
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mode }) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const startDate = issueDetails.start_date;
|
||||
const targetDate = issueDetails.target_date;
|
||||
|
||||
const minDate = startDate ? new Date(startDate) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = targetDate ? new Date(targetDate) : null;
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
const state = issueDetails.state_detail;
|
||||
const stateGroup = issueGroupFilter(state.group);
|
||||
|
||||
const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null;
|
||||
|
||||
const dueDateIcon = dueDateIconDetails(issueDetails.target_date, state.group);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const urlToCopy = window.location.href;
|
||||
|
||||
@@ -62,7 +44,6 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
||||
{mode === "full" && (
|
||||
<div className="flex justify-between gap-2 pb-3">
|
||||
<h6 className="flex items-center gap-2 font-medium">
|
||||
{/* {getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)} */}
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||
</h6>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -125,11 +106,11 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
||||
</div>
|
||||
<div>
|
||||
{issueDetails.target_date ? (
|
||||
<div
|
||||
className={`h-[24px] rounded-md flex px-2.5 py-1 items-center border border-custom-border-100 gap-1 text-custom-text-100 text-xs font-medium
|
||||
${validDate(issueDetails.target_date, state)}`}
|
||||
>
|
||||
{renderDateFormat(issueDetails.target_date)}
|
||||
<div className="h-6 rounded flex items-center gap-1 px-2.5 py-1 border border-custom-border-100 text-custom-text-100 text-xs bg-custom-background-80">
|
||||
<span className={`material-symbols-rounded text-sm -my-0.5 ${dueDateIcon.className}`}>
|
||||
{dueDateIcon.iconName}
|
||||
</span>
|
||||
{renderFullDate(issueDetails.target_date)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-custom-text-200">Empty</span>
|
||||
|
||||
@@ -77,14 +77,16 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
{...bubbleMenuProps}
|
||||
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
||||
>
|
||||
<NodeSelector
|
||||
editor={props.editor!}
|
||||
isOpen={isNodeSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
{!props.editor.isActive("table") && (
|
||||
<NodeSelector
|
||||
editor={props.editor!}
|
||||
isOpen={isNodeSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<LinkSelector
|
||||
editor={props.editor!!}
|
||||
isOpen={isLinkSelectorOpen}
|
||||
|
||||
@@ -28,7 +28,10 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
|
||||
name: "Text",
|
||||
icon: TextIcon,
|
||||
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
|
||||
isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"),
|
||||
isActive: () =>
|
||||
editor.isActive("paragraph") &&
|
||||
!editor.isActive("bulletList") &&
|
||||
!editor.isActive("orderedList"),
|
||||
},
|
||||
{
|
||||
name: "H1",
|
||||
@@ -69,7 +72,8 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
|
||||
{
|
||||
name: "Quote",
|
||||
icon: TextQuote,
|
||||
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
|
||||
command: () =>
|
||||
editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
|
||||
isActive: () => editor.isActive("blockquote"),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||
import { lowlight } from "lowlight/lib/core";
|
||||
import SlashCommand from "../slash-command";
|
||||
import { InputRule } from "@tiptap/core";
|
||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||
|
||||
import ts from "highlight.js/lib/languages/typescript";
|
||||
|
||||
@@ -20,6 +21,10 @@ import "highlight.js/styles/github-dark.css";
|
||||
import UniqueID from "@tiptap-pro/extension-unique-id";
|
||||
import UpdatedImage from "./updated-image";
|
||||
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
|
||||
import { CustomTableCell } from "./table/table-cell";
|
||||
import { Table } from "./table/table";
|
||||
import { TableHeader } from "./table/table-header";
|
||||
import { TableRow } from "@tiptap/extension-table-row";
|
||||
|
||||
lowlight.registerLanguage("ts", ts);
|
||||
|
||||
@@ -27,113 +32,122 @@ export const TiptapExtensions = (
|
||||
workspaceSlug: string,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
) => [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc list-outside leading-3 -mt-2",
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc list-outside leading-3 -mt-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal list-outside leading-3 -mt-2",
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal list-outside leading-3 -mt-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
listItem: {
|
||||
HTMLAttributes: {
|
||||
class: "leading-normal -mb-2",
|
||||
listItem: {
|
||||
HTMLAttributes: {
|
||||
class: "leading-normal -mb-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
blockquote: {
|
||||
HTMLAttributes: {
|
||||
class: "border-l-4 border-custom-border-300",
|
||||
blockquote: {
|
||||
HTMLAttributes: {
|
||||
class: "border-l-4 border-custom-border-300",
|
||||
},
|
||||
},
|
||||
},
|
||||
code: {
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||
spellcheck: "false",
|
||||
code: {
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||
spellcheck: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
dropcursor: {
|
||||
color: "#DBEAFE",
|
||||
width: 2,
|
||||
},
|
||||
gapcursor: false,
|
||||
}),
|
||||
CodeBlockLowlight.configure({
|
||||
lowlight,
|
||||
}),
|
||||
HorizontalRule.extend({
|
||||
addInputRules() {
|
||||
return [
|
||||
new InputRule({
|
||||
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
||||
handler: ({ state, range, commands }) => {
|
||||
commands.splitBlock();
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
dropcursor: {
|
||||
color: "rgba(var(--color-text-100))",
|
||||
width: 2,
|
||||
},
|
||||
gapcursor: false,
|
||||
}),
|
||||
CodeBlockLowlight.configure({
|
||||
lowlight,
|
||||
}),
|
||||
HorizontalRule.extend({
|
||||
addInputRules() {
|
||||
return [
|
||||
new InputRule({
|
||||
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
||||
handler: ({ state, range, commands }) => {
|
||||
commands.splitBlock();
|
||||
|
||||
const attributes = {};
|
||||
const { tr } = state;
|
||||
const start = range.from;
|
||||
const end = range.to;
|
||||
// @ts-ignore
|
||||
tr.replaceWith(start - 1, end, this.type.create(attributes));
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
}).configure({
|
||||
HTMLAttributes: {
|
||||
class: "mb-6 border-t border-custom-border-300",
|
||||
},
|
||||
}),
|
||||
TiptapLink.configure({
|
||||
protocols: ["http", "https"],
|
||||
validate: (url) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
UpdatedImage.configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-lg border border-custom-border-300",
|
||||
},
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === "heading") {
|
||||
return `Heading ${node.attrs.level}`;
|
||||
}
|
||||
const attributes = {};
|
||||
const { tr } = state;
|
||||
const start = range.from;
|
||||
const end = range.to;
|
||||
// @ts-ignore
|
||||
tr.replaceWith(start - 1, end, this.type.create(attributes));
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
}).configure({
|
||||
HTMLAttributes: {
|
||||
class: "mb-6 border-t border-custom-border-300",
|
||||
},
|
||||
}),
|
||||
Gapcursor,
|
||||
TiptapLink.configure({
|
||||
protocols: ["http", "https"],
|
||||
validate: (url) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
UpdatedImage.configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-lg border border-custom-border-300",
|
||||
},
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === "heading") {
|
||||
return `Heading ${node.attrs.level}`;
|
||||
}
|
||||
if (node.type.name === "image" || node.type.name === "table") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return "Press '/' for commands...";
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
UniqueID.configure({
|
||||
types: ["image"],
|
||||
}),
|
||||
SlashCommand(workspaceSlug, setIsSubmitting),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
Color,
|
||||
Highlight.configure({
|
||||
multicolor: true,
|
||||
}),
|
||||
TaskList.configure({
|
||||
HTMLAttributes: {
|
||||
class: "not-prose pl-2",
|
||||
},
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "flex items-start my-4",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
}),
|
||||
];
|
||||
return "Press '/' for commands...";
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
UniqueID.configure({
|
||||
types: ["image"],
|
||||
}),
|
||||
SlashCommand(workspaceSlug, setIsSubmitting),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
Color,
|
||||
Highlight.configure({
|
||||
multicolor: true,
|
||||
}),
|
||||
TaskList.configure({
|
||||
HTMLAttributes: {
|
||||
class: "not-prose pl-2",
|
||||
},
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "flex items-start my-4",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
}),
|
||||
Table,
|
||||
TableHeader,
|
||||
CustomTableCell,
|
||||
TableRow,
|
||||
];
|
||||
|
||||
32
space/components/tiptap/extensions/table/table-cell.ts
Normal file
32
space/components/tiptap/extensions/table/table-cell.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { TableCell } from "@tiptap/extension-table-cell";
|
||||
|
||||
export const CustomTableCell = TableCell.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
isHeader: {
|
||||
default: false,
|
||||
parseHTML: (element) => {
|
||||
isHeader: element.tagName === "TD";
|
||||
},
|
||||
renderHTML: (attributes) => {
|
||||
tag: attributes.isHeader ? "th" : "td";
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
if (HTMLAttributes.isHeader) {
|
||||
return [
|
||||
"th",
|
||||
{
|
||||
...HTMLAttributes,
|
||||
class: `relative ${HTMLAttributes.class}`,
|
||||
},
|
||||
["span", { class: "absolute top-0 right-0" }],
|
||||
0,
|
||||
];
|
||||
}
|
||||
return ["td", HTMLAttributes, 0];
|
||||
},
|
||||
});
|
||||
7
space/components/tiptap/extensions/table/table-header.ts
Normal file
7
space/components/tiptap/extensions/table/table-header.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
|
||||
|
||||
const TableHeader = BaseTableHeader.extend({
|
||||
content: "paragraph",
|
||||
});
|
||||
|
||||
export { TableHeader };
|
||||
9
space/components/tiptap/extensions/table/table.ts
Normal file
9
space/components/tiptap/extensions/table/table.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Table as BaseTable } from "@tiptap/extension-table";
|
||||
|
||||
const Table = BaseTable.configure({
|
||||
resizable: true,
|
||||
cellMinWidth: 100,
|
||||
allowTableNodeSelection: true,
|
||||
});
|
||||
|
||||
export { Table };
|
||||
@@ -6,6 +6,7 @@ import { EditorBubbleMenu } from "./bubble-menu";
|
||||
import { TiptapExtensions } from "./extensions";
|
||||
import { TiptapEditorProps } from "./props";
|
||||
import { ImageResizer } from "./extensions/image-resize";
|
||||
import { TableMenu } from "./table-menu";
|
||||
|
||||
export interface ITipTapRichTextEditor {
|
||||
value: string;
|
||||
@@ -37,6 +38,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
||||
borderOnFocus,
|
||||
customClassName,
|
||||
} = props;
|
||||
|
||||
const editor = useEditor({
|
||||
editable: editable ?? true,
|
||||
editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting),
|
||||
@@ -54,12 +56,6 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
editor.commands.setContent(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
@@ -81,8 +77,8 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
||||
|
||||
const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
|
||||
${noBorder ? "" : "border border-custom-border-200"} ${
|
||||
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
||||
} ${customClassName}`;
|
||||
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
||||
} ${customClassName}`;
|
||||
|
||||
if (!editor) return null;
|
||||
editorRef.current = editor;
|
||||
@@ -98,6 +94,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
||||
{editor && <EditorBubbleMenu editor={editor} />}
|
||||
<div className={`${editorContentCustomClassNames}`}>
|
||||
<EditorContent editor={editor} />
|
||||
<TableMenu editor={editor} />
|
||||
{editor?.isActive("image") && <ImageResizer editor={editor} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +1,51 @@
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import fileService from "services/file.service";
|
||||
|
||||
const deleteKey = new PluginKey("delete-image");
|
||||
const IMAGE_NODE_TYPE = "image";
|
||||
|
||||
const TrackImageDeletionPlugin = () =>
|
||||
interface ImageNode extends ProseMirrorNode {
|
||||
attrs: {
|
||||
src: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
const TrackImageDeletionPlugin = (): Plugin =>
|
||||
new Plugin({
|
||||
key: deleteKey,
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||
const newImageSources = new Set();
|
||||
newState.doc.descendants((node) => {
|
||||
if (node.type.name === IMAGE_NODE_TYPE) {
|
||||
newImageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
|
||||
transactions.forEach((transaction) => {
|
||||
if (!transaction.docChanged) return;
|
||||
|
||||
const removedImages: ProseMirrorNode[] = [];
|
||||
const removedImages: ImageNode[] = [];
|
||||
|
||||
oldState.doc.descendants((oldNode, oldPos) => {
|
||||
if (oldNode.type.name !== "image") return;
|
||||
|
||||
if (oldNode.type.name !== IMAGE_NODE_TYPE) return;
|
||||
if (oldPos < 0 || oldPos > newState.doc.content.size) return;
|
||||
if (!newState.doc.resolve(oldPos).parent) return;
|
||||
|
||||
const newNode = newState.doc.nodeAt(oldPos);
|
||||
|
||||
// Check if the node has been deleted or replaced
|
||||
if (!newNode || newNode.type.name !== "image") {
|
||||
// Check if the node still exists elsewhere in the document
|
||||
let nodeExists = false;
|
||||
newState.doc.descendants((node) => {
|
||||
if (node.attrs.id === oldNode.attrs.id) {
|
||||
nodeExists = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!nodeExists) {
|
||||
removedImages.push(oldNode as ProseMirrorNode);
|
||||
if (!newNode || newNode.type.name !== IMAGE_NODE_TYPE) {
|
||||
if (!newImageSources.has(oldNode.attrs.src)) {
|
||||
removedImages.push(oldNode as ImageNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
removedImages.forEach((node) => {
|
||||
removedImages.forEach(async (node) => {
|
||||
const src = node.attrs.src;
|
||||
onNodeDeleted(src);
|
||||
await onNodeDeleted(src);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,10 +55,14 @@ const TrackImageDeletionPlugin = () =>
|
||||
|
||||
export default TrackImageDeletionPlugin;
|
||||
|
||||
async function onNodeDeleted(src: string) {
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
|
||||
if (resStatus === 204) {
|
||||
console.log("Image deleted successfully");
|
||||
async function onNodeDeleted(src: string): Promise<void> {
|
||||
try {
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
|
||||
if (resStatus === 204) {
|
||||
console.log("Image deleted successfully");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting image: ", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-nocheck
|
||||
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
||||
import fileService from "services/file.service";
|
||||
@@ -46,7 +45,11 @@ export default UploadImagesPlugin;
|
||||
|
||||
function findPlaceholder(state: EditorState, id: {}) {
|
||||
const decos = uploadKey.getState(state);
|
||||
const found = decos.find(undefined, undefined, (spec: { id: number | undefined }) => spec.id == id);
|
||||
const found = decos.find(
|
||||
undefined,
|
||||
undefined,
|
||||
(spec: { id: number | undefined }) => spec.id == id
|
||||
);
|
||||
return found.length ? found[0].from : null;
|
||||
}
|
||||
|
||||
@@ -59,8 +62,6 @@ export async function startImageUpload(
|
||||
) {
|
||||
if (!file.type.includes("image/")) {
|
||||
return;
|
||||
} else if (file.size / 1024 / 1024 > 20) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = {};
|
||||
@@ -93,7 +94,9 @@ export async function startImageUpload(
|
||||
const imageSrc = typeof src === "object" ? reader.result : src;
|
||||
|
||||
const node = schema.nodes.image.create({ src: imageSrc });
|
||||
const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } });
|
||||
const transaction = view.state.tr
|
||||
.replaceWith(pos, pos, node)
|
||||
.setMeta(uploadKey, { remove: { id } });
|
||||
view.dispatch(transaction);
|
||||
}
|
||||
|
||||
@@ -107,7 +110,9 @@ const UploadImageHandler = (file: File, workspaceSlug: string): Promise<string>
|
||||
formData.append("attributes", JSON.stringify({}));
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const imageUrl = await fileService.uploadFile(workspaceSlug, formData).then((response) => response.asset);
|
||||
const imageUrl = await fileService
|
||||
.uploadFile(workspaceSlug, formData)
|
||||
.then((response) => response.asset);
|
||||
|
||||
const image = new Image();
|
||||
image.src = imageUrl;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { startImageUpload } from "./plugins/upload-image";
|
||||
import { findTableAncestor } from "./table-menu";
|
||||
|
||||
export function TiptapEditorProps(
|
||||
workspaceSlug: string,
|
||||
@@ -21,6 +22,15 @@ export function TiptapEditorProps(
|
||||
},
|
||||
},
|
||||
handlePaste: (view, event) => {
|
||||
if (typeof window !== "undefined") {
|
||||
const selection: any = window?.getSelection();
|
||||
if (selection.rangeCount !== 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
if (findTableAncestor(range.startContainer)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.clipboardData.files[0];
|
||||
@@ -31,6 +41,15 @@ export function TiptapEditorProps(
|
||||
return false;
|
||||
},
|
||||
handleDrop: (view, event, _slice, moved) => {
|
||||
if (typeof window !== "undefined") {
|
||||
const selection: any = window?.getSelection();
|
||||
if (selection.rangeCount !== 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
if (findTableAncestor(range.startContainer)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer.files[0];
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
MinusSquare,
|
||||
CheckSquare,
|
||||
ImageIcon,
|
||||
Table,
|
||||
} from "lucide-react";
|
||||
import { startImageUpload } from "../plugins/upload-image";
|
||||
import { cn } from "../utils";
|
||||
@@ -46,6 +47,9 @@ const Command = Extension.create({
|
||||
return [
|
||||
Suggestion({
|
||||
editor: this.editor,
|
||||
allow({ editor }) {
|
||||
return !editor.isActive("table");
|
||||
},
|
||||
...this.options.suggestion,
|
||||
}),
|
||||
];
|
||||
@@ -53,7 +57,10 @@ const Command = Extension.create({
|
||||
});
|
||||
|
||||
const getSuggestionItems =
|
||||
(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) =>
|
||||
(
|
||||
workspaceSlug: string,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
) =>
|
||||
({ query }: { query: string }) =>
|
||||
[
|
||||
{
|
||||
@@ -119,6 +126,20 @@ const getSuggestionItems =
|
||||
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Table",
|
||||
description: "Create a Table",
|
||||
searchTerms: ["table", "cell", "db", "data", "tabular"],
|
||||
icon: <Table size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
||||
.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Numbered List",
|
||||
description: "Create a list with numbering.",
|
||||
@@ -134,14 +155,21 @@ const getSuggestionItems =
|
||||
searchTerms: ["blockquote"],
|
||||
icon: <TextQuote size={18} />,
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.toggleNode("paragraph", "paragraph")
|
||||
.toggleBlockquote()
|
||||
.run(),
|
||||
},
|
||||
{
|
||||
title: "Code",
|
||||
description: "Capture a code snippet.",
|
||||
searchTerms: ["codeblock"],
|
||||
icon: <Code size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||
},
|
||||
{
|
||||
title: "Image",
|
||||
@@ -190,7 +218,15 @@ export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
||||
}
|
||||
};
|
||||
|
||||
const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => {
|
||||
const CommandList = ({
|
||||
items,
|
||||
command,
|
||||
}: {
|
||||
items: CommandItemProps[];
|
||||
command: any;
|
||||
editor: any;
|
||||
range: any;
|
||||
}) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = useCallback(
|
||||
|
||||
16
space/components/tiptap/table-menu/InsertBottomTableIcon.tsx
Normal file
16
space/components/tiptap/table-menu/InsertBottomTableIcon.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
const InsertBottomTableIcon = (props: any) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 -960 960 960"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M212.309-152.31q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-375.383q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm535.382-219.998H212.309q-4.616 0-8.463 3.846-3.846 3.846-3.846 8.462V-600q0 4.616 3.846 8.462 3.847 3.847 8.463 3.847h535.382q4.616 0 8.463-3.847Q760-595.384 760-600v-135.383q0-4.616-3.846-8.462-3.847-3.846-8.463-3.846ZM200-587.691v-160 160Z"
|
||||
fill="rgb(var(--color-text-300))"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default InsertBottomTableIcon;
|
||||
15
space/components/tiptap/table-menu/InsertLeftTableIcon.tsx
Normal file
15
space/components/tiptap/table-menu/InsertLeftTableIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
const InsertLeftTableIcon = (props: any) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 -960 960 960"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M224.617-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm147.691-607.69q0-4.616-3.846-8.463-3.846-3.846-8.462-3.846H600q-4.616 0-8.462 3.846-3.847 3.847-3.847 8.463v535.382q0 4.616 3.847 8.463Q595.384-200 600-200h135.383q4.616 0 8.462-3.846 3.846-3.847 3.846-8.463v-535.382ZM587.691-200h160-160Z"
|
||||
fill="rgb(var(--color-text-300))"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default InsertLeftTableIcon;
|
||||
16
space/components/tiptap/table-menu/InsertRightTableIcon.tsx
Normal file
16
space/components/tiptap/table-menu/InsertRightTableIcon.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
const InsertRightTableIcon = (props: any) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 -960 960 960"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M600-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm-375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm-12.308-607.69v535.382q0 4.616 3.846 8.463 3.846 3.846 8.462 3.846H360q4.616 0 8.462-3.846 3.847-3.847 3.847-8.463v-535.382q0-4.616-3.847-8.463Q364.616-760 360-760H224.617q-4.616 0-8.462 3.846-3.846 3.847-3.846 8.463Zm160 547.691h-160 160Z"
|
||||
fill="rgb(var(--color-text-300))"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default InsertRightTableIcon;
|
||||
15
space/components/tiptap/table-menu/InsertTopTableIcon.tsx
Normal file
15
space/components/tiptap/table-menu/InsertTopTableIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
const InsertTopTableIcon = (props: any) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 -960 960 960"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M212.309-527.693q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0 375.383q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-59.999h535.382q4.616 0 8.463-3.846 3.846-3.846 3.846-8.462V-360q0-4.616-3.846-8.462-3.847-3.847-8.463-3.847H212.309q-4.616 0-8.463 3.847Q200-364.616 200-360v135.383q0 4.616 3.846 8.462 3.847 3.846 8.463 3.846Zm-12.309-160v160-160Z"
|
||||
fill="rgb(var(--color-text-300))"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default InsertTopTableIcon;
|
||||
143
space/components/tiptap/table-menu/index.tsx
Normal file
143
space/components/tiptap/table-menu/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Rows, Columns, ToggleRight } from "lucide-react";
|
||||
import { cn } from "../utils";
|
||||
import { Tooltip } from "components/ui";
|
||||
import InsertLeftTableIcon from "./InsertLeftTableIcon";
|
||||
import InsertRightTableIcon from "./InsertRightTableIcon";
|
||||
import InsertTopTableIcon from "./InsertTopTableIcon";
|
||||
import InsertBottomTableIcon from "./InsertBottomTableIcon";
|
||||
|
||||
interface TableMenuItem {
|
||||
command: () => void;
|
||||
icon: any;
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
|
||||
while (node !== null && node.nodeName !== "TABLE") {
|
||||
node = node.parentNode;
|
||||
}
|
||||
return node as HTMLTableElement;
|
||||
};
|
||||
|
||||
export const TableMenu = ({ editor }: { editor: any }) => {
|
||||
const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 });
|
||||
const isOpen = editor?.isActive("table");
|
||||
|
||||
const items: TableMenuItem[] = [
|
||||
{
|
||||
command: () => editor.chain().focus().addColumnBefore().run(),
|
||||
icon: InsertLeftTableIcon,
|
||||
key: "insert-column-left",
|
||||
name: "Insert 1 column left",
|
||||
},
|
||||
{
|
||||
command: () => editor.chain().focus().addColumnAfter().run(),
|
||||
icon: InsertRightTableIcon,
|
||||
key: "insert-column-right",
|
||||
name: "Insert 1 column right",
|
||||
},
|
||||
{
|
||||
command: () => editor.chain().focus().addRowBefore().run(),
|
||||
icon: InsertTopTableIcon,
|
||||
key: "insert-row-above",
|
||||
name: "Insert 1 row above",
|
||||
},
|
||||
{
|
||||
command: () => editor.chain().focus().addRowAfter().run(),
|
||||
icon: InsertBottomTableIcon,
|
||||
key: "insert-row-below",
|
||||
name: "Insert 1 row below",
|
||||
},
|
||||
{
|
||||
command: () => editor.chain().focus().deleteColumn().run(),
|
||||
icon: Columns,
|
||||
key: "delete-column",
|
||||
name: "Delete column",
|
||||
},
|
||||
{
|
||||
command: () => editor.chain().focus().deleteRow().run(),
|
||||
icon: Rows,
|
||||
key: "delete-row",
|
||||
name: "Delete row",
|
||||
},
|
||||
{
|
||||
command: () => editor.chain().focus().toggleHeaderRow().run(),
|
||||
icon: ToggleRight,
|
||||
key: "toggle-header-row",
|
||||
name: "Toggle header row",
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!window) return;
|
||||
|
||||
const handleWindowClick = () => {
|
||||
const selection: any = window?.getSelection();
|
||||
|
||||
if (selection.rangeCount !== 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
const tableNode = findTableAncestor(range.startContainer);
|
||||
|
||||
let parent = tableNode?.parentElement;
|
||||
|
||||
if (tableNode) {
|
||||
const tableRect = tableNode.getBoundingClientRect();
|
||||
const tableCenter = tableRect.left + tableRect.width / 2;
|
||||
const menuWidth = 45;
|
||||
const menuLeft = tableCenter - menuWidth / 2;
|
||||
const tableBottom = tableRect.bottom;
|
||||
|
||||
setTableLocation({ bottom: tableBottom, left: menuLeft });
|
||||
|
||||
while (parent) {
|
||||
if (!parent.classList.contains("disable-scroll"))
|
||||
parent.classList.add("disable-scroll");
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
} else {
|
||||
const scrollDisabledContainers = document.querySelectorAll(".disable-scroll");
|
||||
|
||||
scrollDisabledContainers.forEach((container) => {
|
||||
container.classList.remove("disable-scroll");
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("click", handleWindowClick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", handleWindowClick);
|
||||
};
|
||||
}, [tableLocation, editor]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`fixed left-1/2 transform -translate-x-1/2 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 shadow-custom-shadow-sm p-1 ${
|
||||
isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
style={{
|
||||
bottom: `calc(100vh - ${tableLocation.bottom + 45}px)`,
|
||||
left: `${tableLocation.left}px`,
|
||||
}}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<Tooltip key={index} tooltipContent={item.name}>
|
||||
<button
|
||||
onClick={item.command}
|
||||
className="p-1.5 text-custom-text-200 hover:bg-text-custom-text-100 hover:bg-custom-background-80 active:bg-custom-background-80 rounded"
|
||||
title={item.name}
|
||||
>
|
||||
<item.icon
|
||||
className={cn("h-4 w-4 text-lg", {
|
||||
"text-red-600": item.key.includes("delete"),
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -12,13 +12,14 @@ import { Icon } from "components/ui";
|
||||
const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];
|
||||
|
||||
interface Props {
|
||||
size?: "sm" | "md" | "lg";
|
||||
position?: "top" | "bottom";
|
||||
onSelect: (emoji: string) => void;
|
||||
position?: "top" | "bottom";
|
||||
selected?: string[];
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export const ReactionSelector: React.FC<Props> = (props) => {
|
||||
const { onSelect, position, size } = props;
|
||||
const { onSelect, position, selected = [], size } = props;
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
@@ -51,7 +52,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
||||
position === "top" ? "-top-12" : "-bottom-12"
|
||||
}`}
|
||||
>
|
||||
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1">
|
||||
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
|
||||
<div className="flex gap-x-1">
|
||||
{reactionEmojis.map((emoji) => (
|
||||
<button
|
||||
@@ -61,7 +62,9 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
||||
onSelect(emoji);
|
||||
closePopover();
|
||||
}}
|
||||
className="flex select-none items-center justify-between rounded-md text-sm p-1 hover:bg-custom-sidebar-background-90"
|
||||
className={`grid place-items-center select-none rounded-md text-sm p-1 ${
|
||||
selected.includes(emoji) ? "bg-custom-primary-100/10" : "hover:bg-custom-sidebar-background-80"
|
||||
}`}
|
||||
>
|
||||
{renderEmoji(emoji)}
|
||||
</button>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
export const renderDateFormat = (date: string | Date | null) => {
|
||||
if (!date) return "N/A";
|
||||
|
||||
var d = new Date(date),
|
||||
month = "" + (d.getMonth() + 1),
|
||||
day = "" + d.getDate(),
|
||||
year = d.getFullYear();
|
||||
|
||||
if (month.length < 2) month = "0" + month;
|
||||
if (day.length < 2) day = "0" + day;
|
||||
|
||||
return [year, month, day].join("-");
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Returns date and month, if date is of the current year
|
||||
* @description Returns date, month adn year, if date is of a different year than current
|
||||
* @param {string} date
|
||||
* @example renderFullDate("2023-01-01") // 1 Jan
|
||||
* @example renderFullDate("2021-01-01") // 1 Jan, 2021
|
||||
*/
|
||||
|
||||
export const renderFullDate = (date: string): string => {
|
||||
if (!date) return "";
|
||||
|
||||
const months: string[] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
|
||||
const currentDate: Date = new Date();
|
||||
const [year, month, day]: number[] = date.split("-").map(Number);
|
||||
|
||||
const formattedMonth: string = months[month - 1];
|
||||
const formattedDay: string = day < 10 ? `0${day}` : day.toString();
|
||||
|
||||
if (currentDate.getFullYear() === year) return `${formattedDay} ${formattedMonth}`;
|
||||
else return `${formattedDay} ${formattedMonth}, ${year}`;
|
||||
};
|
||||
@@ -12,3 +12,26 @@ export const timeAgo = (time: any) => {
|
||||
time = +new Date();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Returns date and month, if date is of the current year
|
||||
* @description Returns date, month adn year, if date is of a different year than current
|
||||
* @param {string} date
|
||||
* @example renderFullDate("2023-01-01") // 1 Jan
|
||||
* @example renderFullDate("2021-01-01") // 1 Jan, 2021
|
||||
*/
|
||||
|
||||
export const renderFullDate = (date: string): string => {
|
||||
if (!date) return "";
|
||||
|
||||
const months: string[] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
|
||||
const currentDate: Date = new Date();
|
||||
const [year, month, day]: number[] = date.split("-").map(Number);
|
||||
|
||||
const formattedMonth: string = months[month - 1];
|
||||
const formattedDay: string = day < 10 ? `0${day}` : day.toString();
|
||||
|
||||
if (currentDate.getFullYear() === year) return `${formattedDay} ${formattedMonth}`;
|
||||
else return `${formattedDay} ${formattedMonth}, ${year}`;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,10 @@ const nextConfig = {
|
||||
};
|
||||
|
||||
if (parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) {
|
||||
const nextConfigWithNginx = withImages({ basePath: "/spaces", ...nextConfig });
|
||||
const nextConfigWithNginx = withImages({
|
||||
basePath: "/spaces",
|
||||
...nextConfig,
|
||||
});
|
||||
module.exports = nextConfigWithNginx;
|
||||
} else {
|
||||
module.exports = nextConfig;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"@heroicons/react": "^2.0.12",
|
||||
"@mui/icons-material": "^5.14.1",
|
||||
"@mui/material": "^5.14.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tiptap-pro/extension-unique-id": "^2.1.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
||||
"@tiptap/extension-color": "^2.0.4",
|
||||
@@ -62,19 +61,17 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/node": "18.14.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-config-custom": "*",
|
||||
"eslint-config-next": "13.2.1",
|
||||
"postcss": "^8.4.21",
|
||||
"tsconfig": "*",
|
||||
"tailwindcss": "^3.2.7"
|
||||
"tailwind-config-custom": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import useSWR from "swr";
|
||||
import type { GetServerSideProps } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
/// layouts
|
||||
import ProjectLayout from "layouts/project-layout";
|
||||
// components
|
||||
@@ -39,12 +40,4 @@ const WorkspaceProjectPage = (props: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
// export const getServerSideProps: GetServerSideProps<any> = async ({ query: { workspace_slug, project_slug } }) => {
|
||||
// const res = await fetch(
|
||||
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/settings/`
|
||||
// );
|
||||
// const project_settings = await res.json();
|
||||
// return { props: { project_settings } };
|
||||
// };
|
||||
|
||||
export default WorkspaceProjectPage;
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import React, { useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
// assets
|
||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.svg";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import authenticationService from "services/authentication.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { OnBoardingForm } from "components/accounts/onboarding-form";
|
||||
|
||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
|
||||
|
||||
const OnBoardingPage = () => {
|
||||
const { user: userStore } = useMobxStore();
|
||||
|
||||
const user = userStore?.currentUser;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const user = userStore?.currentUser;
|
||||
|
||||
@@ -34,7 +27,7 @@ const OnBoardingPage = () => {
|
||||
<div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0 z-10" />
|
||||
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 py-5 left-2 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12 z-10">
|
||||
<div className="h-[30px] w-[30px]">
|
||||
<Image src={BluePlaneLogoWithoutText} alt="Plane logo" />
|
||||
<img src={`${imagePrefix}/plane-logos/blue-without-text.png`} alt="Plane logo" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute sm:fixed text-custom-text-100 text-sm font-medium right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
module.exports = require("tailwind-config-custom/postcss.config");
|
||||
|
||||
BIN
space/public/plane-logos/blue-without-text.png
Normal file
BIN
space/public/plane-logos/blue-without-text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="276.000000pt" height="276.000000pt" viewBox="0 0 276.000000 276.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,276.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M930 2300 l0 -450 460 0 460 0 0 -460 0 -460 450 0 450 0 0 910 0
|
||||
910 -910 0 -910 0 0 -450z"/>
|
||||
<path d="M10 1380 l0 -450 450 0 450 0 0 450 0 450 -450 0 -450 0 0 -450z"/>
|
||||
<path d="M930 460 l0 -450 450 0 450 0 0 450 0 450 -450 0 -450 0 0 -450z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 690 B |
@@ -93,16 +93,6 @@ class IssueService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async getCommentsReactions(workspaceSlug: string, projectId: string, commentId: string): Promise<any> {
|
||||
return this.get(
|
||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise<any> {
|
||||
return this.post(
|
||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`,
|
||||
@@ -140,6 +130,39 @@ class IssueService extends APIService {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async createCommentReaction(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
data: {
|
||||
reaction: string;
|
||||
}
|
||||
): Promise<any> {
|
||||
return this.post(
|
||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteCommentReaction(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
reactionHex: string
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/${reactionHex}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default IssueService;
|
||||
|
||||
@@ -32,6 +32,20 @@ export interface IIssueDetailStore {
|
||||
data: any
|
||||
) => Promise<any>;
|
||||
deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void;
|
||||
addCommentReaction: (
|
||||
workspaceId: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
reactionHex: string
|
||||
) => void;
|
||||
removeCommentReaction: (
|
||||
workspaceId: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
reactionHex: string
|
||||
) => void;
|
||||
// issue reactions
|
||||
addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
||||
removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
||||
@@ -61,8 +75,17 @@ class IssueDetailStore implements IssueDetailStore {
|
||||
details: observable.ref,
|
||||
// actions
|
||||
setPeekId: action,
|
||||
fetchIssueDetails: action,
|
||||
setPeekMode: action,
|
||||
fetchIssueDetails: action,
|
||||
addIssueComment: action,
|
||||
updateIssueComment: action,
|
||||
deleteIssueComment: action,
|
||||
addCommentReaction: action,
|
||||
removeCommentReaction: action,
|
||||
addIssueReaction: action,
|
||||
removeIssueReaction: action,
|
||||
addIssueVote: action,
|
||||
removeIssueVote: action,
|
||||
});
|
||||
this.issueService = new IssueService();
|
||||
this.rootStore = _rootStore;
|
||||
@@ -131,29 +154,32 @@ class IssueDetailStore implements IssueDetailStore {
|
||||
data: any
|
||||
) => {
|
||||
try {
|
||||
const issueCommentUpdateResponse = await this.issueService.updateIssueComment(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
commentId,
|
||||
data
|
||||
);
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
comments: this.details[issueId].comments.map((c) => ({
|
||||
...c,
|
||||
...(c.id === commentId ? data : {}),
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (issueCommentUpdateResponse) {
|
||||
const remainingComments = this.details[issueId].comments.filter((com) => com.id != commentId);
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
comments: [...remainingComments, issueCommentUpdateResponse],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
return issueCommentUpdateResponse;
|
||||
await this.issueService.updateIssueComment(workspaceSlug, projectId, issueId, commentId, data);
|
||||
} catch (error) {
|
||||
console.log("Failed to add issue comment");
|
||||
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
comments: issueComments,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -175,6 +201,94 @@ class IssueDetailStore implements IssueDetailStore {
|
||||
}
|
||||
};
|
||||
|
||||
addCommentReaction = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
reactionHex: string
|
||||
) => {
|
||||
const newReaction = {
|
||||
id: uuidv4(),
|
||||
comment: commentId,
|
||||
reaction: reactionHex,
|
||||
actor_detail: this.rootStore.user.currentActor,
|
||||
};
|
||||
const newComments = this.details[issueId].comments.map((comment) => ({
|
||||
...comment,
|
||||
comment_reactions:
|
||||
comment.id === commentId ? [...comment.comment_reactions, newReaction] : comment.comment_reactions,
|
||||
}));
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
comments: [...newComments],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.issueService.createCommentReaction(workspaceSlug, projectId, commentId, {
|
||||
reaction: reactionHex,
|
||||
});
|
||||
} catch (error) {
|
||||
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
comments: issueComments,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
removeCommentReaction = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
reactionHex: string
|
||||
) => {
|
||||
try {
|
||||
const comment = this.details[issueId].comments.find((c) => c.id === commentId);
|
||||
const newCommentReactions = comment?.comment_reactions.filter((r) => r.reaction !== reactionHex) ?? [];
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
comments: this.details[issueId].comments.map((c) => ({
|
||||
...c,
|
||||
comment_reactions: c.id === commentId ? newCommentReactions : c.comment_reactions,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.issueService.deleteCommentReaction(workspaceSlug, projectId, commentId, reactionHex);
|
||||
} catch (error) {
|
||||
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
comments: issueComments,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
|
||||
@@ -62,17 +62,13 @@ class UserStore implements IUserStore {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPath = window.location.pathname + window.location.search;
|
||||
this.fetchCurrentUser()
|
||||
.then(() => {
|
||||
if (!this.currentUser) {
|
||||
const currentPath = window.location.pathname;
|
||||
window.location.href = `/?next_path=${currentPath}`;
|
||||
} else callback();
|
||||
if (!this.currentUser) window.location.href = `/?next_path=${currentPath}`;
|
||||
else callback();
|
||||
})
|
||||
.catch(() => {
|
||||
const currentPath = window.location.pathname;
|
||||
window.location.href = `/?next_path=${currentPath}`;
|
||||
});
|
||||
.catch(() => (window.location.href = `/?next_path=${currentPath}`));
|
||||
};
|
||||
|
||||
fetchCurrentUser = async () => {
|
||||
|
||||
@@ -1,203 +1 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
const convertToRGB = (variableName) => `rgba(var(${variableName}))`;
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
"./app/**/*.{js,ts,jsx,tsx}",
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
"./layouts/**/*.tsx",
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
"./constants/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
"custom-shadow-2xs": "var(--color-shadow-2xs)",
|
||||
"custom-shadow-xs": "var(--color-shadow-xs)",
|
||||
"custom-shadow-sm": "var(--color-shadow-sm)",
|
||||
"custom-shadow-rg": "var(--color-shadow-rg)",
|
||||
"custom-shadow-md": "var(--color-shadow-md)",
|
||||
"custom-shadow-lg": "var(--color-shadow-lg)",
|
||||
"custom-shadow-xl": "var(--color-shadow-xl)",
|
||||
"custom-shadow-2xl": "var(--color-shadow-2xl)",
|
||||
"custom-shadow-3xl": "var(--color-shadow-3xl)",
|
||||
"custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)",
|
||||
"custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)",
|
||||
"custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)",
|
||||
"custom-sidebar-shadow-rg": "var(--color-sidebar-shadow-rg)",
|
||||
"custom-sidebar-shadow-md": "var(--color-sidebar-shadow-md)",
|
||||
"custom-sidebar-shadow-lg": "var(--color-sidebar-shadow-lg)",
|
||||
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
|
||||
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
|
||||
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
|
||||
},
|
||||
colors: {
|
||||
custom: {
|
||||
primary: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-primary-10"),
|
||||
20: convertToRGB("--color-primary-20"),
|
||||
30: convertToRGB("--color-primary-30"),
|
||||
40: convertToRGB("--color-primary-40"),
|
||||
50: convertToRGB("--color-primary-50"),
|
||||
60: convertToRGB("--color-primary-60"),
|
||||
70: convertToRGB("--color-primary-70"),
|
||||
80: convertToRGB("--color-primary-80"),
|
||||
90: convertToRGB("--color-primary-90"),
|
||||
100: convertToRGB("--color-primary-100"),
|
||||
200: convertToRGB("--color-primary-200"),
|
||||
300: convertToRGB("--color-primary-300"),
|
||||
400: convertToRGB("--color-primary-400"),
|
||||
500: convertToRGB("--color-primary-500"),
|
||||
600: convertToRGB("--color-primary-600"),
|
||||
700: convertToRGB("--color-primary-700"),
|
||||
800: convertToRGB("--color-primary-800"),
|
||||
900: convertToRGB("--color-primary-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-primary-100"),
|
||||
},
|
||||
background: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-background-10"),
|
||||
20: convertToRGB("--color-background-20"),
|
||||
30: convertToRGB("--color-background-30"),
|
||||
40: convertToRGB("--color-background-40"),
|
||||
50: convertToRGB("--color-background-50"),
|
||||
60: convertToRGB("--color-background-60"),
|
||||
70: convertToRGB("--color-background-70"),
|
||||
80: convertToRGB("--color-background-80"),
|
||||
90: convertToRGB("--color-background-90"),
|
||||
100: convertToRGB("--color-background-100"),
|
||||
200: convertToRGB("--color-background-200"),
|
||||
300: convertToRGB("--color-background-300"),
|
||||
400: convertToRGB("--color-background-400"),
|
||||
500: convertToRGB("--color-background-500"),
|
||||
600: convertToRGB("--color-background-600"),
|
||||
700: convertToRGB("--color-background-700"),
|
||||
800: convertToRGB("--color-background-800"),
|
||||
900: convertToRGB("--color-background-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-background-100"),
|
||||
},
|
||||
text: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-text-10"),
|
||||
20: convertToRGB("--color-text-20"),
|
||||
30: convertToRGB("--color-text-30"),
|
||||
40: convertToRGB("--color-text-40"),
|
||||
50: convertToRGB("--color-text-50"),
|
||||
60: convertToRGB("--color-text-60"),
|
||||
70: convertToRGB("--color-text-70"),
|
||||
80: convertToRGB("--color-text-80"),
|
||||
90: convertToRGB("--color-text-90"),
|
||||
100: convertToRGB("--color-text-100"),
|
||||
200: convertToRGB("--color-text-200"),
|
||||
300: convertToRGB("--color-text-300"),
|
||||
400: convertToRGB("--color-text-400"),
|
||||
500: convertToRGB("--color-text-500"),
|
||||
600: convertToRGB("--color-text-600"),
|
||||
700: convertToRGB("--color-text-700"),
|
||||
800: convertToRGB("--color-text-800"),
|
||||
900: convertToRGB("--color-text-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-text-100"),
|
||||
},
|
||||
border: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
100: convertToRGB("--color-border-100"),
|
||||
200: convertToRGB("--color-border-200"),
|
||||
300: convertToRGB("--color-border-300"),
|
||||
400: convertToRGB("--color-border-400"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-border-200"),
|
||||
},
|
||||
sidebar: {
|
||||
background: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-sidebar-background-10"),
|
||||
20: convertToRGB("--color-sidebar-background-20"),
|
||||
30: convertToRGB("--color-sidebar-background-30"),
|
||||
40: convertToRGB("--color-sidebar-background-40"),
|
||||
50: convertToRGB("--color-sidebar-background-50"),
|
||||
60: convertToRGB("--color-sidebar-background-60"),
|
||||
70: convertToRGB("--color-sidebar-background-70"),
|
||||
80: convertToRGB("--color-sidebar-background-80"),
|
||||
90: convertToRGB("--color-sidebar-background-90"),
|
||||
100: convertToRGB("--color-sidebar-background-100"),
|
||||
200: convertToRGB("--color-sidebar-background-200"),
|
||||
300: convertToRGB("--color-sidebar-background-300"),
|
||||
400: convertToRGB("--color-sidebar-background-400"),
|
||||
500: convertToRGB("--color-sidebar-background-500"),
|
||||
600: convertToRGB("--color-sidebar-background-600"),
|
||||
700: convertToRGB("--color-sidebar-background-700"),
|
||||
800: convertToRGB("--color-sidebar-background-800"),
|
||||
900: convertToRGB("--color-sidebar-background-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-background-100"),
|
||||
},
|
||||
text: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
10: convertToRGB("--color-sidebar-text-10"),
|
||||
20: convertToRGB("--color-sidebar-text-20"),
|
||||
30: convertToRGB("--color-sidebar-text-30"),
|
||||
40: convertToRGB("--color-sidebar-text-40"),
|
||||
50: convertToRGB("--color-sidebar-text-50"),
|
||||
60: convertToRGB("--color-sidebar-text-60"),
|
||||
70: convertToRGB("--color-sidebar-text-70"),
|
||||
80: convertToRGB("--color-sidebar-text-80"),
|
||||
90: convertToRGB("--color-sidebar-text-90"),
|
||||
100: convertToRGB("--color-sidebar-text-100"),
|
||||
200: convertToRGB("--color-sidebar-text-200"),
|
||||
300: convertToRGB("--color-sidebar-text-300"),
|
||||
400: convertToRGB("--color-sidebar-text-400"),
|
||||
500: convertToRGB("--color-sidebar-text-500"),
|
||||
600: convertToRGB("--color-sidebar-text-600"),
|
||||
700: convertToRGB("--color-sidebar-text-700"),
|
||||
800: convertToRGB("--color-sidebar-text-800"),
|
||||
900: convertToRGB("--color-sidebar-text-900"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-text-100"),
|
||||
},
|
||||
border: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
100: convertToRGB("--color-sidebar-border-100"),
|
||||
200: convertToRGB("--color-sidebar-border-200"),
|
||||
300: convertToRGB("--color-sidebar-border-300"),
|
||||
400: convertToRGB("--color-sidebar-border-400"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
DEFAULT: convertToRGB("--color-sidebar-border-200"),
|
||||
},
|
||||
},
|
||||
backdrop: "#131313",
|
||||
},
|
||||
},
|
||||
typography: ({ theme }) => ({
|
||||
brand: {
|
||||
css: {
|
||||
"--tw-prose-body": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-p": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-headings": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-lead": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-links": convertToRGB("--color-primary-100"),
|
||||
"--tw-prose-bold": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-counters": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-bullets": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-hr": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-quotes": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-quote-borders": convertToRGB("--color-border"),
|
||||
"--tw-prose-code": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-pre-code": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-pre-bg": convertToRGB("--color-background-100"),
|
||||
"--tw-prose-th-borders": convertToRGB("--color-border"),
|
||||
"--tw-prose-td-borders": convertToRGB("--color-border"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
fontFamily: {
|
||||
custom: ["Inter", "sans-serif"],
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
};
|
||||
module.exports = require("tailwind-config-custom/tailwind.config");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": "tsconfig/nextjs.json",
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
|
||||
@@ -68,25 +68,30 @@ export interface IVote {
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
id: string;
|
||||
actor_detail: ActorDetail;
|
||||
issue_detail: IssueDetail;
|
||||
project_detail: ProjectDetail;
|
||||
workspace_detail: WorkspaceDetail;
|
||||
comment_reactions: any[];
|
||||
is_member: boolean;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
comment_stripped: string;
|
||||
comment_html: string;
|
||||
attachments: any[];
|
||||
access: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
workspace: string;
|
||||
issue: string;
|
||||
actor: string;
|
||||
attachments: any[];
|
||||
comment_html: string;
|
||||
comment_reactions: {
|
||||
actor_detail: ActorDetail;
|
||||
comment: string;
|
||||
id: string;
|
||||
reaction: string;
|
||||
}[];
|
||||
comment_stripped: string;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
id: string;
|
||||
is_member: boolean;
|
||||
issue: string;
|
||||
issue_detail: IssueDetail;
|
||||
project: string;
|
||||
project_detail: ProjectDetail;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
workspace: string;
|
||||
workspace_detail: WorkspaceDetail;
|
||||
}
|
||||
|
||||
export interface IIssueReaction {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
rules: {
|
||||
"@next/next/no-img-element": "off",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// nivo
|
||||
import { BarDatum } from "@nivo/bar";
|
||||
// icons
|
||||
import { getPriorityIcon } from "components/icons";
|
||||
import { PriorityIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// helpers
|
||||
import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics";
|
||||
|
||||
@@ -53,7 +53,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{params.segment === "priority" ? (
|
||||
getPriorityIcon(key)
|
||||
<PriorityIcon priority={key as TIssuePriorities} />
|
||||
) : (
|
||||
<span
|
||||
className="h-3 w-3 flex-shrink-0 rounded"
|
||||
@@ -91,7 +91,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
||||
}`}
|
||||
>
|
||||
{params.x_axis === "priority" ? (
|
||||
getPriorityIcon(`${item.name}`)
|
||||
<PriorityIcon priority={item.name as TIssuePriorities} />
|
||||
) : (
|
||||
<span
|
||||
className="h-3 w-3 rounded"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// icons
|
||||
import { PlayIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IDefaultAnalyticsResponse } from "types";
|
||||
import { IDefaultAnalyticsResponse, TStateGroups } from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
@@ -27,7 +27,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<span
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group],
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
|
||||
}}
|
||||
/>
|
||||
<h6 className="capitalize">{group.state_group}</h6>
|
||||
@@ -42,7 +42,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
className="absolute top-0 left-0 h-1 rounded duration-300"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group],
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
|
||||
import { SelectMonthModal } from "components/automation";
|
||||
// icons
|
||||
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
// constants
|
||||
@@ -46,7 +46,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
||||
query: state.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
@@ -140,14 +140,19 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedOption ? (
|
||||
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
|
||||
<StateGroupIcon
|
||||
stateGroup={selectedOption.group}
|
||||
color={selectedOption.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
) : currentDefaultState ? (
|
||||
getStateGroupIcon(
|
||||
currentDefaultState.group,
|
||||
"16",
|
||||
"16",
|
||||
currentDefaultState.color
|
||||
)
|
||||
<StateGroupIcon
|
||||
stateGroup={currentDefaultState.group}
|
||||
color={currentDefaultState.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
) : (
|
||||
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Dispatch, SetStateAction, useCallback } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// cmdk
|
||||
@@ -7,12 +9,12 @@ import { Command } from "cmdk";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
import { PRIORITIES } from "constants/project";
|
||||
// icons
|
||||
import { CheckIcon, getPriorityIcon } from "components/icons";
|
||||
import { CheckIcon, PriorityIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -54,7 +56,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
|
||||
[workspaceSlug, issueId, projectId, user]
|
||||
);
|
||||
|
||||
const handleIssueState = (priority: string | null) => {
|
||||
const handleIssueState = (priority: TIssuePriorities) => {
|
||||
submitChanges({ priority });
|
||||
setIsPaletteOpen(false);
|
||||
};
|
||||
@@ -68,7 +70,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
{getPriorityIcon(priority)}
|
||||
<PriorityIcon priority={priority} />
|
||||
<span className="capitalize">{priority ?? "None"}</span>
|
||||
</div>
|
||||
<div>{priority === issue.priority && <CheckIcon className="h-3 w-3" />}</div>
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Dispatch, SetStateAction, useCallback } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// cmdk
|
||||
import { Command } from "cmdk";
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import stateService from "services/state.service";
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
// icons
|
||||
import { CheckIcon, StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
// fetch keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
|
||||
// icons
|
||||
import { CheckIcon, getStateGroupIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -82,7 +84,12 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||
<StateGroupIcon
|
||||
stateGroup={state.group}
|
||||
color={state.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
<p>{state.name}</p>
|
||||
</div>
|
||||
<div>{state.id === issue.state && <CheckIcon className="h-3 w-3" />}</div>
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// react-datepicker
|
||||
import DatePicker from "react-datepicker";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// components
|
||||
import { DateFilterSelect } from "./date-filter-select";
|
||||
// ui
|
||||
@@ -23,8 +19,10 @@ import { IIssueFilterOptions } from "types";
|
||||
type Props = {
|
||||
title: string;
|
||||
field: keyof IIssueFilterOptions;
|
||||
isOpen: boolean;
|
||||
filters: IIssueFilterOptions;
|
||||
handleClose: () => void;
|
||||
isOpen: boolean;
|
||||
onSelect: (option: any) => void;
|
||||
};
|
||||
|
||||
type TFormValues = {
|
||||
@@ -39,12 +37,14 @@ const defaultValues: TFormValues = {
|
||||
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
|
||||
};
|
||||
|
||||
export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleClose }) => {
|
||||
const { filters, setFilters } = useIssuesView();
|
||||
|
||||
const router = useRouter();
|
||||
const { viewId } = router.query;
|
||||
|
||||
export const DateFilterModal: React.FC<Props> = ({
|
||||
title,
|
||||
field,
|
||||
filters,
|
||||
handleClose,
|
||||
isOpen,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { handleSubmit, watch, control } = useForm<TFormValues>({
|
||||
defaultValues,
|
||||
});
|
||||
@@ -53,10 +53,10 @@ export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleC
|
||||
const { filterType, date1, date2 } = formData;
|
||||
|
||||
if (filterType === "range") {
|
||||
setFilters(
|
||||
{ [field]: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
|
||||
!Boolean(viewId)
|
||||
);
|
||||
onSelect({
|
||||
key: field,
|
||||
value: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`],
|
||||
});
|
||||
} else {
|
||||
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
|
||||
if (item?.includes(filterType)) return false;
|
||||
@@ -66,17 +66,12 @@ export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleC
|
||||
|
||||
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
|
||||
if (filterOne)
|
||||
setFilters(
|
||||
{ [field]: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
|
||||
!Boolean(viewId)
|
||||
);
|
||||
onSelect({ key: field, value: [filterOne, `${renderDateFormat(date1)};${filterType}`] });
|
||||
else
|
||||
setFilters(
|
||||
{
|
||||
[field]: [`${renderDateFormat(date1)};${filterType}`],
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
onSelect({
|
||||
key: field,
|
||||
value: [`${renderDateFormat(date1)};${filterType}`],
|
||||
});
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||
// ui
|
||||
import { Avatar } from "components/ui";
|
||||
// helpers
|
||||
@@ -71,12 +71,10 @@ export const FiltersList: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{getStateGroupIcon(
|
||||
state?.group ?? "backlog",
|
||||
"12",
|
||||
"12",
|
||||
state?.color
|
||||
)}
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
color={state?.color}
|
||||
/>
|
||||
</span>
|
||||
<span>{state?.name ?? ""}</span>
|
||||
<span
|
||||
@@ -105,7 +103,9 @@ export const FiltersList: React.FC<Props> = ({
|
||||
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
|
||||
}}
|
||||
>
|
||||
<span>{getStateGroupIcon(group, "16", "16")}</span>
|
||||
<span>
|
||||
<StateGroupIcon stateGroup={group} color={undefined} />
|
||||
</span>
|
||||
<span>{group}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
@@ -136,7 +136,9 @@ export const FiltersList: React.FC<Props> = ({
|
||||
: "bg-custom-background-90 text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<span>{getPriorityIcon(priority)}</span>
|
||||
<span>
|
||||
<PriorityIcon priority={priority} />
|
||||
</span>
|
||||
<span>{priority === "null" ? "None" : priority}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
|
||||
@@ -58,16 +58,8 @@ export const IssuesFilterView: React.FC = () => {
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const {
|
||||
issueView,
|
||||
setIssueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
showSubIssues,
|
||||
setShowSubIssues,
|
||||
setShowEmptyGroups,
|
||||
displayFilters,
|
||||
setDisplayFilters,
|
||||
filters,
|
||||
setFilters,
|
||||
resetFilterToDefault,
|
||||
@@ -96,11 +88,11 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
issueView === option.type
|
||||
displayFilters.layout === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setIssueView(option.type)}
|
||||
onClick={() => setDisplayFilters({ layout: option.type })}
|
||||
>
|
||||
<option.Icon
|
||||
sx={{
|
||||
@@ -174,28 +166,30 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||
<div className="relative divide-y-2 divide-custom-border-200">
|
||||
<div className="space-y-4 pb-3 text-xs">
|
||||
{issueView !== "calendar" &&
|
||||
issueView !== "spreadsheet" &&
|
||||
issueView !== "gantt_chart" && (
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
displayFilters.layout !== "gantt_chart" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
GROUP_BY_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.group_by
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (displayFilters.layout === "kanban" && option.key === null)
|
||||
return null;
|
||||
if (option.key === "project") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
onClick={() => setDisplayFilters({ group_by: option.key })}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
@@ -205,41 +199,45 @@ export const IssuesFilterView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) =>
|
||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.order_by
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) =>
|
||||
displayFilters.group_by === "priority" &&
|
||||
option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setDisplayFilters({ order_by: option.key });
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
||||
?.name ?? "Select"
|
||||
FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.type
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
@@ -248,7 +246,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
setDisplayFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
@@ -260,33 +258,40 @@ export const IssuesFilterView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={showSubIssues}
|
||||
onChange={() => setShowSubIssues(!showSubIssues)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{issueView !== "calendar" &&
|
||||
issueView !== "spreadsheet" &&
|
||||
issueView !== "gantt_chart" && (
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={showEmptyGroups}
|
||||
onChange={() => setShowEmptyGroups(!showEmptyGroups)}
|
||||
value={displayFilters.sub_issue ?? true}
|
||||
onChange={() =>
|
||||
setDisplayFilters({ sub_issue: !displayFilters.sub_issue })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{issueView !== "calendar" &&
|
||||
issueView !== "spreadsheet" &&
|
||||
issueView !== "gantt_chart" && (
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
displayFilters.layout !== "gantt_chart" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={displayFilters.show_empty_groups ?? true}
|
||||
onChange={() =>
|
||||
setDisplayFilters({
|
||||
show_empty_groups: !displayFilters.show_empty_groups,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
displayFilters.layout !== "gantt_chart" && (
|
||||
<div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
@@ -302,7 +307,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{issueView !== "gantt_chart" && (
|
||||
{displayFilters.layout !== "gantt_chart" && (
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||
@@ -310,7 +315,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
if (
|
||||
issueView === "spreadsheet" &&
|
||||
displayFilters.layout === "spreadsheet" &&
|
||||
(key === "attachment_count" ||
|
||||
key === "link" ||
|
||||
key === "sub_issue_count")
|
||||
@@ -318,7 +323,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
return null;
|
||||
|
||||
if (
|
||||
issueView !== "spreadsheet" &&
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
(key === "created_on" || key === "updated_on")
|
||||
)
|
||||
return null;
|
||||
|
||||
@@ -58,7 +58,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
||||
);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
const { issueView, params } = useIssuesView();
|
||||
const { displayFilters, params } = useIssuesView();
|
||||
const { params: calendarParams } = useCalendarIssuesView();
|
||||
const { order_by, group_by, ...viewGanttParams } = params;
|
||||
|
||||
@@ -126,8 +126,8 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
||||
message: "Issues deleted successfully!",
|
||||
});
|
||||
|
||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
||||
else if (issueView === "gantt_chart") mutate(ganttFetchKey);
|
||||
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||
else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
|
||||
else {
|
||||
if (cycleId) {
|
||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
|
||||
|
||||
@@ -212,7 +212,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
workspace level
|
||||
Workspace Level
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
||||
position === "top" ? "-top-12" : "-bottom-12"
|
||||
}`}
|
||||
>
|
||||
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1">
|
||||
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
|
||||
<div className="flex gap-x-1">
|
||||
{reactionEmojis.map((emoji) => (
|
||||
<button
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TAssigneesDistribution,
|
||||
TCompletionChartDistribution,
|
||||
TLabelsDistribution,
|
||||
TStateGroups,
|
||||
} from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
@@ -215,7 +216,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full "
|
||||
style={{
|
||||
backgroundColor: STATE_GROUP_COLORS[group],
|
||||
backgroundColor: STATE_GROUP_COLORS[group as TStateGroups],
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs capitalize">{group}</span>
|
||||
|
||||
@@ -80,7 +80,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
const { user } = useUser();
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
|
||||
const { groupedIssues, isEmpty, issueView } = viewProps;
|
||||
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
@@ -117,11 +117,11 @@ export const AllViews: React.FC<Props> = ({
|
||||
</StrictModeDroppable>
|
||||
{groupedIssues ? (
|
||||
!isEmpty ||
|
||||
issueView === "kanban" ||
|
||||
issueView === "calendar" ||
|
||||
issueView === "gantt_chart" ? (
|
||||
displayFilters?.layout === "kanban" ||
|
||||
displayFilters?.layout === "calendar" ||
|
||||
displayFilters?.layout === "gantt_chart" ? (
|
||||
<>
|
||||
{issueView === "list" ? (
|
||||
{displayFilters?.layout === "list" ? (
|
||||
<AllLists
|
||||
states={states}
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
@@ -134,7 +134,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
userAuth={memberRole}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
) : issueView === "kanban" ? (
|
||||
) : displayFilters?.layout === "kanban" ? (
|
||||
<AllBoards
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
disableUserActions={disableUserActions}
|
||||
@@ -149,7 +149,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
userAuth={memberRole}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
) : issueView === "calendar" ? (
|
||||
) : displayFilters?.layout === "calendar" ? (
|
||||
<CalendarView
|
||||
handleIssueAction={handleIssueAction}
|
||||
addIssueToDate={addIssueToDate}
|
||||
@@ -157,7 +157,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
user={user}
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : issueView === "spreadsheet" ? (
|
||||
) : displayFilters?.layout === "spreadsheet" ? (
|
||||
<SpreadsheetView
|
||||
handleIssueAction={handleIssueAction}
|
||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
@@ -166,7 +166,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : (
|
||||
issueView === "gantt_chart" && <GanttChartView />
|
||||
displayFilters?.layout === "gantt_chart" && <GanttChartView />
|
||||
)}
|
||||
</>
|
||||
) : router.pathname.includes("archived-issues") ? (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// components
|
||||
import { SingleBoard } from "components/core/views/board-view/single-board";
|
||||
// icons
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
@@ -36,7 +36,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -44,9 +44,12 @@ export const AllBoards: React.FC<Props> = ({
|
||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
|
||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
displayFilters?.group_by === "state"
|
||||
? states?.find((s) => s.id === singleGroup)
|
||||
: null;
|
||||
|
||||
if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
|
||||
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<SingleBoard
|
||||
@@ -67,13 +70,15 @@ export const AllBoards: React.FC<Props> = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!showEmptyGroups && (
|
||||
{!displayFilters?.show_empty_groups && (
|
||||
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
|
||||
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
||||
<div className="space-y-3">
|
||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
displayFilters?.group_by === "state"
|
||||
? states?.find((s) => s.id === singleGroup)
|
||||
: null;
|
||||
|
||||
if (groupedIssues[singleGroup].length === 0)
|
||||
return (
|
||||
@@ -82,10 +87,16 @@ export const AllBoards: React.FC<Props> = ({
|
||||
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{currentState &&
|
||||
getStateGroupIcon(currentState.group, "16", "16", currentState.color)}
|
||||
{currentState && (
|
||||
<StateGroupIcon
|
||||
stateGroup={currentState.group}
|
||||
color={currentState.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
)}
|
||||
<h4 className="text-sm capitalize">
|
||||
{selectedGroup === "state"
|
||||
{displayFilters?.group_by === "state"
|
||||
? addSpaceIfCamelCase(currentState?.name ?? "")
|
||||
: addSpaceIfCamelCase(singleGroup)}
|
||||
</h4>
|
||||
|
||||
@@ -13,14 +13,16 @@ import useProjects from "hooks/use-projects";
|
||||
import { Avatar, Icon } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import { IIssueViewProps, IState } from "types";
|
||||
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
type Props = {
|
||||
currentState?: IState | null;
|
||||
@@ -46,22 +48,28 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
console.log("dF", displayFilters);
|
||||
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId && selectedGroup === "labels"
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && selectedGroup === "labels"
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
|
||||
workspaceSlug &&
|
||||
projectId &&
|
||||
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
|
||||
? PROJECT_MEMBERS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
|
||||
workspaceSlug &&
|
||||
projectId &&
|
||||
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
|
||||
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
@@ -71,7 +79,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
const getGroupTitle = () => {
|
||||
let title = addSpaceIfCamelCase(groupTitle);
|
||||
|
||||
switch (selectedGroup) {
|
||||
switch (displayFilters?.group_by) {
|
||||
case "state":
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
@@ -95,16 +103,29 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
const getGroupIcon = () => {
|
||||
let icon;
|
||||
|
||||
switch (selectedGroup) {
|
||||
switch (displayFilters?.group_by) {
|
||||
case "state":
|
||||
icon =
|
||||
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||
icon = currentState && (
|
||||
<StateGroupIcon
|
||||
stateGroup={currentState.group}
|
||||
color={currentState.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "state_detail.group":
|
||||
icon = getStateGroupIcon(groupTitle as any, "16", "16");
|
||||
icon = (
|
||||
<StateGroupIcon
|
||||
stateGroup={groupTitle as TStateGroups}
|
||||
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "priority":
|
||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||
icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
|
||||
break;
|
||||
case "project":
|
||||
const project = projects?.find((p) => p.id === groupTitle);
|
||||
@@ -152,7 +173,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
<span className="flex items-center">{getGroupIcon()}</span>
|
||||
<h2
|
||||
className={`text-lg font-semibold truncate ${
|
||||
selectedGroup === "created_by" ? "" : "capitalize"
|
||||
displayFilters?.group_by === "created_by" ? "" : "capitalize"
|
||||
}`}
|
||||
style={{
|
||||
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
|
||||
@@ -183,7 +204,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
|
||||
)}
|
||||
</button>
|
||||
{!disableAddIssue && !disableUserActions && selectedGroup !== "created_by" && (
|
||||
{!disableAddIssue && !disableUserActions && displayFilters?.group_by !== "created_by" && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
||||
|
||||
@@ -50,7 +50,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
// collapse/expand
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
const { groupedIssues, groupByProperty: selectedGroup, orderBy, properties } = viewProps;
|
||||
const { displayFilters, groupedIssues, properties } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
@@ -80,14 +80,14 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`relative h-full ${
|
||||
orderBy !== "sort_order" && snapshot.isDraggingOver
|
||||
displayFilters?.order_by !== "sort_order" && snapshot.isDraggingOver
|
||||
? "bg-custom-background-100/20"
|
||||
: ""
|
||||
} ${!isCollapsed ? "hidden" : "flex flex-col"}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{orderBy !== "sort_order" && (
|
||||
{displayFilters?.order_by !== "sort_order" && (
|
||||
<>
|
||||
<div
|
||||
className={`absolute ${
|
||||
@@ -101,7 +101,11 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
>
|
||||
This board is ordered by{" "}
|
||||
{replaceUnderscoreIfSnakeCase(
|
||||
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
|
||||
displayFilters?.order_by
|
||||
? displayFilters?.order_by[0] === "-"
|
||||
? displayFilters?.order_by.slice(1)
|
||||
: displayFilters?.order_by
|
||||
: "created_at"
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
@@ -145,13 +149,13 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
))}
|
||||
<span
|
||||
style={{
|
||||
display: orderBy === "sort_order" ? "inline" : "none",
|
||||
display: displayFilters?.order_by === "sort_order" ? "inline" : "none",
|
||||
}}
|
||||
>
|
||||
{provided.placeholder}
|
||||
</span>
|
||||
</div>
|
||||
{selectedGroup !== "created_by" && (
|
||||
{displayFilters?.group_by !== "created_by" && (
|
||||
<div>
|
||||
{type === "issue"
|
||||
? !disableAddIssueOption && (
|
||||
|
||||
@@ -93,7 +93,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
|
||||
const { displayFilters, properties, mutateIssues } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
@@ -131,9 +131,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
handleIssuesMutation(
|
||||
formData,
|
||||
groupTitle ?? "",
|
||||
selectedGroup,
|
||||
displayFilters?.group_by ?? null,
|
||||
index,
|
||||
orderBy,
|
||||
displayFilters?.order_by ?? "-created_at",
|
||||
prevData
|
||||
),
|
||||
false
|
||||
@@ -149,24 +149,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
});
|
||||
},
|
||||
[
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
groupTitle,
|
||||
index,
|
||||
selectedGroup,
|
||||
mutateIssues,
|
||||
orderBy,
|
||||
user,
|
||||
]
|
||||
[displayFilters, workspaceSlug, cycleId, moduleId, groupTitle, index, mutateIssues, user]
|
||||
);
|
||||
|
||||
const getStyle = (
|
||||
style: DraggingStyle | NotDraggingStyle | undefined,
|
||||
snapshot: DraggableStateSnapshot
|
||||
) => {
|
||||
if (orderBy === "sort_order") return style;
|
||||
if (displayFilters?.order_by === "sort_order") return style;
|
||||
if (!snapshot.isDragging) return {};
|
||||
if (!snapshot.isDropAnimating) return style;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user